MediaProvider.java revision f414462bdd8715a39e24a7eaee0863e5b29302ce
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
17package com.android.providers.media;
18
19import android.app.SearchManager;
20import android.content.*;
21import android.database.Cursor;
22import android.database.MergeCursor;
23import android.database.SQLException;
24import android.database.sqlite.SQLiteDatabase;
25import android.database.sqlite.SQLiteOpenHelper;
26import android.database.sqlite.SQLiteQueryBuilder;
27import android.graphics.Bitmap;
28import android.graphics.BitmapFactory;
29import android.media.MediaFile;
30import android.media.MediaScanner;
31import android.net.Uri;
32import android.os.Binder;
33import android.os.Environment;
34import android.os.FileUtils;
35import android.os.Handler;
36import android.os.Looper;
37import android.os.Message;
38import android.os.ParcelFileDescriptor;
39import android.os.Process;
40import android.provider.BaseColumns;
41import android.provider.MediaStore;
42import android.provider.MediaStore.Audio;
43import android.provider.MediaStore.Images;
44import android.provider.MediaStore.MediaColumns;
45import android.provider.MediaStore.Video;
46import android.provider.MediaStore.Images.ImageColumns;
47import android.text.TextUtils;
48import android.util.Config;
49import android.util.Log;
50
51import java.io.File;
52import java.io.FileInputStream;
53import java.io.FileNotFoundException;
54import java.io.IOException;
55import java.io.OutputStream;
56import java.text.Collator;
57import java.util.HashMap;
58import java.util.HashSet;
59import java.util.Iterator;
60import java.util.List;
61
62/**
63 * Media content provider. See {@link android.provider.MediaStore} for details.
64 * Separate databases are kept for each external storage card we see (using the
65 * card's ID as an index).  The content visible at content://media/external/...
66 * changes with the card.
67 */
68public class MediaProvider extends ContentProvider {
69    private static final Uri MEDIA_URI = Uri.parse("content://media");
70    private static final Uri ALBUMART_URI = Uri.parse("content://media/external/audio/albumart");
71    private static final Uri ALBUMART_THUMB_URI = Uri.parse("content://media/external/audio/albumart_thumb");
72
73    private static final HashMap<String, String> sArtistAlbumsMap = new HashMap<String, String>();
74
75    private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() {
76        @Override
77        public void onReceive(Context context, Intent intent) {
78            if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) {
79                // Remove the external volume and then notify all cursors backed by
80                // data on that volume
81                detachVolume(Uri.parse("content://media/external"));
82            }
83        }
84    };
85
86    /**
87     * Wrapper class for a specific database (associated with one particular
88     * external card, or with internal storage).  Can open the actual database
89     * on demand, create and upgrade the schema, etc.
90     */
91    private static final class DatabaseHelper extends SQLiteOpenHelper {
92        final Context mContext;
93        final boolean mInternal;  // True if this is the internal database
94
95        // In memory caches of artist and album data.
96        HashMap<String, Long> mArtistCache = new HashMap<String, Long>();
97        HashMap<String, Long> mAlbumCache = new HashMap<String, Long>();
98
99        public DatabaseHelper(Context context, String name, boolean internal) {
100            super(context, name, null, DATABASE_VERSION);
101            mContext = context;
102            mInternal = internal;
103        }
104
105        /**
106         * Creates database the first time we try to open it.
107         */
108        @Override
109        public void onCreate(final SQLiteDatabase db) {
110            updateDatabase(db, mInternal, 0, DATABASE_VERSION);
111        }
112
113        /**
114         * Updates the database format when a new content provider is used
115         * with an older database format.
116         */
117        @Override
118        public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
119            updateDatabase(db, mInternal, oldV, newV);
120        }
121
122        /**
123         * Touch this particular database and garbage collect old databases.
124         * An LRU cache system is used to clean up databases for old external
125         * storage volumes.
126         */
127        @Override
128        public void onOpen(SQLiteDatabase db) {
129            if (mInternal) return;  // The internal database is kept separately.
130
131            // touch the database file to show it is most recently used
132            File file = new File(db.getPath());
133            long now = System.currentTimeMillis();
134            file.setLastModified(now);
135
136            // delete least recently used databases if we are over the limit
137            String[] databases = mContext.databaseList();
138            int count = databases.length;
139            int limit = MAX_EXTERNAL_DATABASES;
140
141            // delete external databases that have not been used in the past two months
142            long twoMonthsAgo = now - OBSOLETE_DATABASE_DB;
143            for (int i = 0; i < databases.length; i++) {
144                File other = mContext.getDatabasePath(databases[i]);
145                if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) {
146                    databases[i] = null;
147                    count--;
148                    if (file.equals(other)) {
149                        // reduce limit to account for the existence of the database we
150                        // are about to open, which we removed from the list.
151                        limit--;
152                    }
153                } else {
154                    long time = other.lastModified();
155                    if (time < twoMonthsAgo) {
156                        if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]);
157                        mContext.deleteDatabase(databases[i]);
158                        databases[i] = null;
159                        count--;
160                    }
161                }
162            }
163
164            // delete least recently used databases until
165            // we are no longer over the limit
166            while (count > limit) {
167                int lruIndex = -1;
168                long lruTime = 0;
169
170                for (int i = 0; i < databases.length; i++) {
171                    if (databases[i] != null) {
172                        long time = mContext.getDatabasePath(databases[i]).lastModified();
173                        if (lruTime == 0 || time < lruTime) {
174                            lruIndex = i;
175                            lruTime = time;
176                        }
177                    }
178                }
179
180                // delete least recently used database
181                if (lruIndex != -1) {
182                    if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]);
183                    mContext.deleteDatabase(databases[lruIndex]);
184                    databases[lruIndex] = null;
185                    count--;
186                }
187            }
188        }
189    }
190
191    @Override
192    public boolean onCreate() {
193        sArtistAlbumsMap.put(MediaStore.Audio.Albums._ID, "audio.album_id AS " +
194                MediaStore.Audio.Albums._ID);
195        sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM, "album");
196        sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_KEY, "album_key");
197        sArtistAlbumsMap.put(MediaStore.Audio.Albums.FIRST_YEAR, "MIN(year) AS " +
198                MediaStore.Audio.Albums.FIRST_YEAR);
199        sArtistAlbumsMap.put(MediaStore.Audio.Albums.LAST_YEAR, "MAX(year) AS " +
200                MediaStore.Audio.Albums.LAST_YEAR);
201        sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST, "artist");
202        sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_ID, "artist");
203        sArtistAlbumsMap.put(MediaStore.Audio.Media.ARTIST_KEY, "artist_key");
204        sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS, "count(*) AS " +
205                MediaStore.Audio.Albums.NUMBER_OF_SONGS);
206        sArtistAlbumsMap.put(MediaStore.Audio.Albums.ALBUM_ART, "album_art._data AS " +
207                MediaStore.Audio.Albums.ALBUM_ART);
208
209        mDatabases = new HashMap<String, DatabaseHelper>();
210        attachVolume(INTERNAL_VOLUME);
211
212        IntentFilter iFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
213        iFilter.addDataScheme("file");
214        getContext().registerReceiver(mUnmountReceiver, iFilter);
215
216        // open external database if external storage is mounted
217        String state = Environment.getExternalStorageState();
218        if (Environment.MEDIA_MOUNTED.equals(state) ||
219                Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
220            attachVolume(EXTERNAL_VOLUME);
221        }
222
223        mThumbWorker = new Worker("album thumbs");
224        mThumbHandler = new Handler(mThumbWorker.getLooper()) {
225            @Override
226            public void handleMessage(Message msg) {
227                makeThumb((ThumbData)msg.obj);
228            }
229        };
230
231        return true;
232    }
233
234    /**
235     * This method takes care of updating all the tables in the database to the
236     * current version, creating them if necessary.
237     * This method can only update databases at schema 63 or higher, which was
238     * created August 1, 2008. Older database will be cleared and recreated.
239     * @param db Database
240     * @param internal True if this is the internal media database
241     */
242    private static void updateDatabase(SQLiteDatabase db, boolean internal,
243            int fromVersion, int toVersion) {
244
245        // sanity check
246        if (toVersion != DATABASE_VERSION) {
247            Log.e(TAG, "Illegal update request. Got " + toVersion + ", expected " +
248                    DATABASE_VERSION);
249            throw new IllegalArgumentException();
250        }
251
252        if (fromVersion < 63) {
253            // Drop everything and start over.
254            Log.i(TAG, "Upgrading media database from version " +
255                    fromVersion + " to " + toVersion + ", which will destroy all old data");
256            db.execSQL("DROP TABLE IF EXISTS images");
257            db.execSQL("DROP TRIGGER IF EXISTS images_cleanup");
258            db.execSQL("DROP TABLE IF EXISTS thumbnails");
259            db.execSQL("DROP TRIGGER IF EXISTS thumbnails_cleanup");
260            db.execSQL("DROP TABLE IF EXISTS audio_meta");
261            db.execSQL("DROP TABLE IF EXISTS artists");
262            db.execSQL("DROP TABLE IF EXISTS albums");
263            db.execSQL("DROP TABLE IF EXISTS album_art");
264            db.execSQL("DROP VIEW IF EXISTS audio");
265            db.execSQL("DROP TRIGGER IF EXISTS audio_delete");
266            db.execSQL("DROP VIEW IF EXISTS artist_info");
267            db.execSQL("DROP VIEW IF EXISTS album_info");
268            db.execSQL("DROP VIEW IF EXISTS artists_albums_map");
269            db.execSQL("DROP TRIGGER IF EXISTS audio_meta_cleanup");
270            db.execSQL("DROP TABLE IF EXISTS audio_genres");
271            db.execSQL("DROP TABLE IF EXISTS audio_genres_map");
272            db.execSQL("DROP TRIGGER IF EXISTS audio_genres_cleanup");
273            db.execSQL("DROP TABLE IF EXISTS audio_playlists");
274            db.execSQL("DROP TABLE IF EXISTS audio_playlists_map");
275            db.execSQL("DROP TRIGGER IF EXISTS audio_playlists_cleanup");
276            db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup1");
277            db.execSQL("DROP TRIGGER IF EXISTS albumart_cleanup2");
278            db.execSQL("DROP TABLE IF EXISTS video");
279            db.execSQL("DROP TRIGGER IF EXISTS video_cleanup");
280
281            db.execSQL("CREATE TABLE IF NOT EXISTS images (" +
282                    "_id INTEGER PRIMARY KEY," +
283                    "_data TEXT," +
284                    "_size INTEGER," +
285                    "_display_name TEXT," +
286                    "mime_type TEXT," +
287                    "title TEXT," +
288                    "date_added INTEGER," +
289                    "date_modified INTEGER," +
290                    "description TEXT," +
291                    "picasa_id TEXT," +
292                    "isprivate INTEGER," +
293                    "latitude DOUBLE," +
294                    "longitude DOUBLE," +
295                    "datetaken INTEGER," +
296                    "orientation INTEGER," +
297                    "mini_thumb_magic INTEGER," +
298                    "bucket_id TEXT," +
299                    "bucket_display_name TEXT" +
300                   ");");
301
302            db.execSQL("CREATE INDEX IF NOT EXISTS mini_thumb_magic_index on images(mini_thumb_magic);");
303
304            db.execSQL("CREATE TRIGGER IF NOT EXISTS images_cleanup DELETE ON images " +
305                    "BEGIN " +
306                        "DELETE FROM thumbnails WHERE image_id = old._id;" +
307                        "SELECT _DELETE_FILE(old._data);" +
308                    "END");
309
310            db.execSQL("CREATE TABLE IF NOT EXISTS thumbnails (" +
311                       "_id INTEGER PRIMARY KEY," +
312                       "_data TEXT," +
313                       "image_id INTEGER," +
314                       "kind INTEGER," +
315                       "width INTEGER," +
316                       "height INTEGER" +
317                       ");");
318
319            db.execSQL("CREATE INDEX IF NOT EXISTS image_id_index on thumbnails(image_id);");
320
321            db.execSQL("CREATE TRIGGER IF NOT EXISTS thumbnails_cleanup DELETE ON thumbnails " +
322                    "BEGIN " +
323                        "SELECT _DELETE_FILE(old._data);" +
324                    "END");
325
326
327            // Contains meta data about audio files
328            db.execSQL("CREATE TABLE IF NOT EXISTS audio_meta (" +
329                       "_id INTEGER PRIMARY KEY," +
330                       "_data TEXT NOT NULL," +
331                       "_display_name TEXT," +
332                       "_size INTEGER," +
333                       "mime_type TEXT," +
334                       "date_added INTEGER," +
335                       "date_modified INTEGER," +
336                       "title TEXT NOT NULL," +
337                       "title_key TEXT NOT NULL," +
338                       "duration INTEGER," +
339                       "artist_id INTEGER," +
340                       "composer TEXT," +
341                       "album_id INTEGER," +
342                       "track INTEGER," +    // track is an integer to allow proper sorting
343                       "year INTEGER CHECK(year!=0)," +
344                       "is_ringtone INTEGER," +
345                       "is_music INTEGER," +
346                       "is_alarm INTEGER," +
347                       "is_notification INTEGER" +
348                       ");");
349
350            // Contains a sort/group "key" and the preferred display name for artists
351            db.execSQL("CREATE TABLE IF NOT EXISTS artists (" +
352                        "artist_id INTEGER PRIMARY KEY," +
353                        "artist_key TEXT NOT NULL UNIQUE," +
354                        "artist TEXT NOT NULL" +
355                       ");");
356
357            // Contains a sort/group "key" and the preferred display name for albums
358            db.execSQL("CREATE TABLE IF NOT EXISTS albums (" +
359                        "album_id INTEGER PRIMARY KEY," +
360                        "album_key TEXT NOT NULL UNIQUE," +
361                        "album TEXT NOT NULL" +
362                       ");");
363
364            db.execSQL("CREATE TABLE IF NOT EXISTS album_art (" +
365                    "album_id INTEGER PRIMARY KEY," +
366                    "_data TEXT" +
367                   ");");
368
369            // Provides a unified audio/artist/album info view.
370            // Note that views are read-only, so we define a trigger to allow deletes.
371            db.execSQL("CREATE VIEW IF NOT EXISTS audio as SELECT * FROM audio_meta " +
372                        "LEFT OUTER JOIN artists ON audio_meta.artist_id=artists.artist_id " +
373                        "LEFT OUTER JOIN albums ON audio_meta.album_id=albums.album_id;");
374
375            db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_delete INSTEAD OF DELETE ON audio " +
376                    "BEGIN " +
377                        "DELETE from audio_meta where _id=old._id;" +
378                        "DELETE from audio_playlists_map where audio_id=old._id;" +
379                        "DELETE from audio_genres_map where audio_id=old._id;" +
380                    "END");
381
382
383            // Provides some extra info about artists, like the number of tracks
384            // and albums for this artist
385            db.execSQL("CREATE VIEW IF NOT EXISTS artist_info AS " +
386                        "SELECT artist_id AS _id, artist, artist_key, " +
387                        "COUNT(DISTINCT album) AS number_of_albums, " +
388                        "COUNT(*) AS number_of_tracks FROM audio WHERE is_music=1 "+
389                        "GROUP BY artist_key;");
390
391            // Provides extra info albums, such as the number of tracks
392            db.execSQL("CREATE VIEW IF NOT EXISTS album_info AS " +
393                    "SELECT audio.album_id AS _id, album, album_key, " +
394                    "MIN(year) AS minyear, " +
395                    "MAX(year) AS maxyear, artist, artist_id, artist_key, " +
396                    "count(*) AS " + MediaStore.Audio.Albums.NUMBER_OF_SONGS +
397                    ",album_art._data AS album_art" +
398                    " FROM audio LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id" +
399                    " WHERE is_music=1 GROUP BY audio.album_id;");
400
401            // For a given artist_id, provides the album_id for albums on
402            // which the artist appears.
403            db.execSQL("CREATE VIEW IF NOT EXISTS artists_albums_map AS " +
404                    "SELECT DISTINCT artist_id, album_id FROM audio_meta;");
405
406            /*
407             * Only external media volumes can handle genres, playlists, etc.
408             */
409            if (!internal) {
410                // Cleans up when an audio file is deleted
411                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_meta_cleanup DELETE ON audio_meta " +
412                           "BEGIN " +
413                               "DELETE FROM audio_genres_map WHERE audio_id = old._id;" +
414                               "DELETE FROM audio_playlists_map WHERE audio_id = old._id;" +
415                           "END");
416
417                // Contains audio genre definitions
418                db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres (" +
419                           "_id INTEGER PRIMARY KEY," +
420                           "name TEXT NOT NULL" +
421                           ");");
422
423                // Contiains mappings between audio genres and audio files
424                db.execSQL("CREATE TABLE IF NOT EXISTS audio_genres_map (" +
425                           "_id INTEGER PRIMARY KEY," +
426                           "audio_id INTEGER NOT NULL," +
427                           "genre_id INTEGER NOT NULL" +
428                           ");");
429
430                // Cleans up when an audio genre is delete
431                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_genres_cleanup DELETE ON audio_genres " +
432                           "BEGIN " +
433                               "DELETE FROM audio_genres_map WHERE genre_id = old._id;" +
434                           "END");
435
436                // Contains audio playlist definitions
437                db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists (" +
438                           "_id INTEGER PRIMARY KEY," +
439                           "_data TEXT," +  // _data is path for file based playlists, or null
440                           "name TEXT NOT NULL," +
441                           "date_added INTEGER," +
442                           "date_modified INTEGER" +
443                           ");");
444
445                // Contains mappings between audio playlists and audio files
446                db.execSQL("CREATE TABLE IF NOT EXISTS audio_playlists_map (" +
447                           "_id INTEGER PRIMARY KEY," +
448                           "audio_id INTEGER NOT NULL," +
449                           "playlist_id INTEGER NOT NULL," +
450                           "play_order INTEGER NOT NULL" +
451                           ");");
452
453                // Cleans up when an audio playlist is deleted
454                db.execSQL("CREATE TRIGGER IF NOT EXISTS audio_playlists_cleanup DELETE ON audio_playlists " +
455                           "BEGIN " +
456                               "DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" +
457                               "SELECT _DELETE_FILE(old._data);" +
458                           "END");
459
460                // Cleans up album_art table entry when an album is deleted
461                db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup1 DELETE ON albums " +
462                        "BEGIN " +
463                            "DELETE FROM album_art WHERE album_id = old.album_id;" +
464                        "END");
465
466                // Cleans up album_art when an album is deleted
467                db.execSQL("CREATE TRIGGER IF NOT EXISTS albumart_cleanup2 DELETE ON album_art " +
468                        "BEGIN " +
469                            "SELECT _DELETE_FILE(old._data);" +
470                        "END");
471            }
472
473            // Contains meta data about video files
474            db.execSQL("CREATE TABLE IF NOT EXISTS video (" +
475                       "_id INTEGER PRIMARY KEY," +
476                       "_data TEXT NOT NULL," +
477                       "_display_name TEXT," +
478                       "_size INTEGER," +
479                       "mime_type TEXT," +
480                       "date_added INTEGER," +
481                       "date_modified INTEGER," +
482                       "title TEXT," +
483                       "duration INTEGER," +
484                       "artist TEXT," +
485                       "album TEXT," +
486                       "resolution TEXT," +
487                       "description TEXT," +
488                       "isprivate INTEGER," +   // for YouTube videos
489                       "tags TEXT," +           // for YouTube videos
490                       "category TEXT," +       // for YouTube videos
491                       "language TEXT," +       // for YouTube videos
492                       "mini_thumb_data TEXT," +
493                       "latitude DOUBLE," +
494                       "longitude DOUBLE," +
495                       "datetaken INTEGER," +
496                       "mini_thumb_magic INTEGER" +
497                       ");");
498
499            db.execSQL("CREATE TRIGGER IF NOT EXISTS video_cleanup DELETE ON video " +
500                    "BEGIN " +
501                        "SELECT _DELETE_FILE(old._data);" +
502                    "END");
503        }
504
505        // At this point the database is at least at schema version 63 (it was
506        // either created at version 63 by the code above, or was already at
507        // version 63 or later)
508
509        if (fromVersion < 64) {
510            // create the index that updates the database to schema version 64
511            db.execSQL("CREATE INDEX IF NOT EXISTS sort_index on images(datetaken ASC, _id ASC);");
512        }
513
514        if (fromVersion < 65) {
515            // create the index that updates the database to schema version 65
516            db.execSQL("CREATE INDEX IF NOT EXISTS titlekey_index on audio_meta(title_key);");
517        }
518
519        if (fromVersion < 66) {
520            updateBucketNames(db, "images");
521        }
522
523        if (fromVersion < 67) {
524            // create the indices that update the database to schema version 67
525            db.execSQL("CREATE INDEX IF NOT EXISTS albumkey_index on albums(album_key);");
526            db.execSQL("CREATE INDEX IF NOT EXISTS artistkey_index on artists(artist_key);");
527        }
528
529        if (fromVersion < 68) {
530            // Create bucket_id and bucket_display_name columns for the video table.
531            db.execSQL("ALTER TABLE video ADD COLUMN bucket_id TEXT;");
532            db.execSQL("ALTER TABLE video ADD COLUMN bucket_display_name TEXT");
533            updateBucketNames(db, "video");
534        }
535
536        if (fromVersion < 69) {
537            updateDisplayName(db, "images");
538        }
539
540        if (fromVersion < 70) {
541            // Create bookmark column for the video table.
542            db.execSQL("ALTER TABLE video ADD COLUMN bookmark INTEGER;");
543        }
544
545        if (fromVersion < 71) {
546            // There is no change to the database schema, however a code change
547            // fixed parsing of metadata for certain files bought from the
548            // iTunes music store, so we want to rescan files that might need it.
549            // We do this by clearing the modification date in the database for
550            // those files, so that the media scanner will see them as updated
551            // and rescan them.
552            db.execSQL("UPDATE audio_meta SET date_modified=0 WHERE _id IN (" +
553                    "SELECT _id FROM audio where mime_type='audio/mp4' AND " +
554                    "artist='" + MediaFile.UNKNOWN_STRING + "' AND " +
555                    "album='" + MediaFile.UNKNOWN_STRING + "'" +
556                    ");");
557        }
558    }
559
560    /**
561     * Iterate through the rows of a table in a database, ensuring that the bucket_id and
562     * bucket_display_name columns are correct.
563     * @param db
564     * @param tableName
565     */
566    private static void updateBucketNames(SQLiteDatabase db, String tableName) {
567        // Rebuild the bucket_display_name column using the natural case rather than lower case.
568        db.beginTransaction();
569        try {
570            String[] columns = {BaseColumns._ID, MediaColumns.DATA};
571            Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
572            try {
573                final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
574                final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
575                while (cursor.moveToNext()) {
576                    String data = cursor.getString(dataColumnIndex);
577                    ContentValues values = new ContentValues();
578                    computeBucketValues(data, values);
579                    int rowId = cursor.getInt(idColumnIndex);
580                    db.update(tableName, values, "_id=" + rowId, null);
581                }
582            } finally {
583                cursor.close();
584            }
585            db.setTransactionSuccessful();
586        } finally {
587            db.endTransaction();
588        }
589    }
590
591    /**
592     * Iterate through the rows of a table in a database, ensuring that the
593     * display name column has a value.
594     * @param db
595     * @param tableName
596     */
597    private static void updateDisplayName(SQLiteDatabase db, String tableName) {
598        // Fill in default values for null displayName values
599        db.beginTransaction();
600        try {
601            String[] columns = {BaseColumns._ID, MediaColumns.DATA, MediaColumns.DISPLAY_NAME};
602            Cursor cursor = db.query(tableName, columns, null, null, null, null, null);
603            try {
604                final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
605                final int dataColumnIndex = cursor.getColumnIndex(MediaColumns.DATA);
606                final int displayNameIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);
607                ContentValues values = new ContentValues();
608                while (cursor.moveToNext()) {
609                    String displayName = cursor.getString(displayNameIndex);
610                    if (displayName == null) {
611                        String data = cursor.getString(dataColumnIndex);
612                        values.clear();
613                        computeDisplayName(data, values);
614                        int rowId = cursor.getInt(idColumnIndex);
615                        db.update(tableName, values, "_id=" + rowId, null);
616                    }
617                }
618            } finally {
619                cursor.close();
620            }
621            db.setTransactionSuccessful();
622        } finally {
623            db.endTransaction();
624        }
625    }
626    /**
627     * @param data The input path
628     * @param values the content values, where the bucked id name and bucket display name are updated.
629     *
630     */
631
632    private static void computeBucketValues(String data, ContentValues values) {
633        File parentFile = new File(data).getParentFile();
634        if (parentFile == null) {
635            parentFile = new File("/");
636        }
637
638        // Lowercase the path for hashing. This avoids duplicate buckets if the
639        // filepath case is changed externally.
640        // Keep the original case for display.
641        String path = parentFile.toString().toLowerCase();
642        String name = parentFile.getName();
643
644        // Note: the BUCKET_ID and BUCKET_DISPLAY_NAME attributes are spelled the
645        // same for both images and video. However, for backwards-compatibility reasons
646        // there is no common base class. We use the ImageColumns version here
647        values.put(ImageColumns.BUCKET_ID, path.hashCode());
648        values.put(ImageColumns.BUCKET_DISPLAY_NAME, name);
649    }
650
651    /**
652     * @param data The input path
653     * @param values the content values, where the display name is updated.
654     *
655     */
656    private static void computeDisplayName(String data, ContentValues values) {
657        String s = (data == null ? "" : data.toString());
658        int idx = s.lastIndexOf('/');
659        if (idx >= 0) {
660            s = s.substring(idx + 1);
661        }
662        values.put("_display_name", s);
663    }
664
665    @Override
666    public Cursor query(Uri uri, String[] projectionIn, String selection,
667            String[] selectionArgs, String sort) {
668        int table = URI_MATCHER.match(uri);
669
670        // handle MEDIA_SCANNER before calling getDatabaseForUri()
671        if (table == MEDIA_SCANNER) {
672            if (mMediaScannerVolume == null) {
673                return null;
674            } else {
675                // create a cursor to return volume currently being scanned by the media scanner
676                return new MediaScannerCursor(mMediaScannerVolume);
677            }
678        }
679
680        String groupBy = null;
681        DatabaseHelper database = getDatabaseForUri(uri);
682        if (database == null) {
683            return null;
684        }
685        SQLiteDatabase db = database.getReadableDatabase();
686        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
687
688        switch (table) {
689            case IMAGES_MEDIA:
690                qb.setTables("images");
691                if (uri.getQueryParameter("distinct") != null)
692                    qb.setDistinct(true);
693
694                // set the project map so that data dir is prepended to _data.
695                //qb.setProjectionMap(mImagesProjectionMap, true);
696                break;
697
698            case IMAGES_MEDIA_ID:
699                qb.setTables("images");
700                if (uri.getQueryParameter("distinct") != null)
701                    qb.setDistinct(true);
702
703                // set the project map so that data dir is prepended to _data.
704                //qb.setProjectionMap(mImagesProjectionMap, true);
705                qb.appendWhere("_id = " + uri.getPathSegments().get(3));
706                break;
707
708            case IMAGES_THUMBNAILS:
709                qb.setTables("thumbnails");
710                break;
711
712            case IMAGES_THUMBNAILS_ID:
713                qb.setTables("thumbnails");
714                qb.appendWhere("_id = " + uri.getPathSegments().get(3));
715                break;
716
717            case AUDIO_MEDIA:
718                qb.setTables("audio ");
719                break;
720
721            case AUDIO_MEDIA_ID:
722                qb.setTables("audio");
723                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
724                break;
725
726            case AUDIO_MEDIA_ID_GENRES:
727                qb.setTables("audio_genres");
728                qb.appendWhere("_id IN (SELECT genre_id FROM " +
729                        "audio_genres_map WHERE audio_id = " +
730                        uri.getPathSegments().get(3) + ")");
731                break;
732
733            case AUDIO_MEDIA_ID_GENRES_ID:
734                qb.setTables("audio_genres");
735                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
736                break;
737
738            case AUDIO_MEDIA_ID_PLAYLISTS:
739                qb.setTables("audio_playlists");
740                qb.appendWhere("_id IN (SELECT playlist_id FROM " +
741                        "audio_playlists_map WHERE audio_id = " +
742                        uri.getPathSegments().get(3) + ")");
743                break;
744
745            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
746                qb.setTables("audio_playlists");
747                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
748                break;
749
750            case AUDIO_GENRES:
751                qb.setTables("audio_genres");
752                break;
753
754            case AUDIO_GENRES_ID:
755                qb.setTables("audio_genres");
756                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
757                break;
758
759            case AUDIO_GENRES_ID_MEMBERS:
760                qb.setTables("audio");
761                qb.appendWhere("_id IN (SELECT audio_id FROM " +
762                        "audio_genres_map WHERE genre_id = " +
763                        uri.getPathSegments().get(3) + ")");
764                break;
765
766            case AUDIO_GENRES_ID_MEMBERS_ID:
767                qb.setTables("audio");
768                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
769                break;
770
771            case AUDIO_PLAYLISTS:
772                qb.setTables("audio_playlists");
773                break;
774
775            case AUDIO_PLAYLISTS_ID:
776                qb.setTables("audio_playlists");
777                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
778                break;
779
780            case AUDIO_PLAYLISTS_ID_MEMBERS:
781                for (int i = 0; i < projectionIn.length; i++) {
782                    if (projectionIn[i].equals("_id")) {
783                        projectionIn[i] = "audio_playlists_map._id AS _id";
784                    }
785                }
786                qb.setTables("audio_playlists_map, audio");
787                qb.appendWhere("audio._id = audio_id AND playlist_id = "
788                        + uri.getPathSegments().get(3));
789                break;
790
791            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
792                qb.setTables("audio");
793                qb.appendWhere("_id=" + uri.getPathSegments().get(5));
794                break;
795
796            case VIDEO_MEDIA:
797                qb.setTables("video");
798                break;
799
800            case VIDEO_MEDIA_ID:
801                qb.setTables("video");
802                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
803                break;
804
805            case AUDIO_ARTISTS:
806                qb.setTables("artist_info");
807                break;
808
809            case AUDIO_ARTISTS_ID:
810                qb.setTables("artist_info");
811                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
812                break;
813
814            case AUDIO_ARTISTS_ID_ALBUMS:
815                String aid = uri.getPathSegments().get(3);
816                qb.setTables("audio LEFT OUTER JOIN album_art ON" +
817                        " audio.album_id=album_art.album_id");
818                qb.appendWhere("is_music=1 AND audio.album_id IN (SELECT album_id FROM " +
819                        "artists_albums_map WHERE artist_id = " +
820                         aid + ")");
821                groupBy = "audio.album_id";
822                sArtistAlbumsMap.put(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST,
823                        "count(CASE WHEN artist_id==" + aid + " THEN 'foo' ELSE NULL END) AS " +
824                        MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST);
825                qb.setProjectionMap(sArtistAlbumsMap);
826                break;
827
828            case AUDIO_ALBUMS:
829                qb.setTables("album_info");
830                break;
831
832            case AUDIO_ALBUMS_ID:
833                qb.setTables("album_info");
834                qb.appendWhere("_id=" + uri.getPathSegments().get(3));
835                break;
836
837            case AUDIO_ALBUMART_ID:
838                qb.setTables("album_art");
839                qb.appendWhere("album_id=" + uri.getPathSegments().get(3));
840                break;
841
842            case AUDIO_SEARCH:
843                return doAudioSearch(db, qb, uri, projectionIn, selection, selectionArgs, sort);
844
845            default:
846                throw new IllegalStateException("Unknown URL: " + uri.toString());
847        }
848
849        Cursor c = qb.query(db, projectionIn, selection,
850                selectionArgs, groupBy, null, sort);
851        if (c != null) {
852            c.setNotificationUri(getContext().getContentResolver(), uri);
853        }
854        return c;
855    }
856
857    private Cursor doAudioSearch(SQLiteDatabase db, SQLiteQueryBuilder qb,
858            Uri uri, String[] projectionIn, String selection,
859            String[] selectionArgs, String sort) {
860
861        List<String> l = uri.getPathSegments();
862        String mSearchString = l.size() == 4 ? l.get(3) : "";
863        mSearchString = mSearchString.replaceAll("  ", " ").trim().toLowerCase();
864        Cursor mCursor = null;
865
866        String [] searchWords = mSearchString.length() > 0 ?
867                mSearchString.split(" ") : new String[0];
868        String [] wildcardWords3 = new String[searchWords.length * 3];
869        Collator col = Collator.getInstance();
870        col.setStrength(Collator.PRIMARY);
871        int len = searchWords.length;
872        for (int i = 0; i < len; i++) {
873            // Because we match on individual words here, we need to remove words
874            // like 'a' and 'the' that aren't part of the keys.
875            wildcardWords3[i] = wildcardWords3[i + len] = wildcardWords3[i + len + len] =
876                (searchWords[i].equals("a") || searchWords[i].equals("an") ||
877                        searchWords[i].equals("the")) ? "%" :
878                '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
879        }
880
881        String UQs [] = new String[3];
882        HashSet<String> tablecolumns = new HashSet<String>();
883
884        // Direct match artists
885        {
886            String[] ccols = new String[] {
887                    MediaStore.Audio.Artists._ID,
888                    "'artist' AS " + MediaStore.Audio.Media.MIME_TYPE,
889                    "" + R.drawable.ic_search_category_music_artist + " AS " +
890                        SearchManager.SUGGEST_COLUMN_ICON_1,
891                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
892                    MediaStore.Audio.Artists.ARTIST + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
893                    MediaStore.Audio.Artists.NUMBER_OF_ALBUMS + " AS data1",
894                    MediaStore.Audio.Artists.NUMBER_OF_TRACKS + " AS data2",
895                    MediaStore.Audio.Artists.ARTIST_KEY + " AS ar", //
896                    "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
897                    "'content://media/external/audio/artists/'||" + MediaStore.Audio.Artists._ID +
898                    " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
899                    "'1' AS grouporder",
900                    "artist_key AS itemorder"
901            };
902
903
904            String where = MediaStore.Audio.Artists.ARTIST_KEY + " != ''";
905            for (int i = 0; i < searchWords.length; i++) {
906                where += " AND ar LIKE ?";
907            }
908
909            qb.setTables("artist_info");
910            UQs[0] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
911                    ccols, tablecolumns, 12, "artist", where, null, null, null);
912        }
913
914        // Direct match albums
915        {
916            String[] ccols = new String[] {
917                    MediaStore.Audio.Albums._ID,
918                    "'album' AS " + MediaStore.Audio.Media.MIME_TYPE,
919                    "" + R.drawable.ic_search_category_music_album + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
920                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
921                    MediaStore.Audio.Albums.ALBUM + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
922                    MediaStore.Audio.Media.ARTIST + " AS data1",
923                    "null AS data2",
924                    MediaStore.Audio.Media.ARTIST_KEY +
925                    "||' '||" +
926                    MediaStore.Audio.Media.ALBUM_KEY +
927                    " AS ar_al",
928                    "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
929                    "'content://media/external/audio/albums/'||" + MediaStore.Audio.Albums._ID +
930                    " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
931                    "'2' AS grouporder",
932                    "album_key AS itemorder"
933            };
934
935            String where = MediaStore.Audio.Media.ALBUM_KEY + " != ''";
936            for (int i = 0; i < searchWords.length; i++) {
937                where += " AND ar_al LIKE ?";
938            }
939
940            qb = new SQLiteQueryBuilder();
941            qb.setTables("album_info");
942            UQs[1] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
943                    ccols, tablecolumns, 12, "album", where, null, null, null);
944        }
945
946        // Direct match tracks
947        {
948            String[] ccols = new String[] {
949                    "audio._id AS _id",
950                    MediaStore.Audio.Media.MIME_TYPE,
951                    "" + R.drawable.ic_search_category_music_song + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1,
952                    "0 AS " + SearchManager.SUGGEST_COLUMN_ICON_2,
953                    MediaStore.Audio.Media.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
954                    MediaStore.Audio.Media.ARTIST + " AS data1",
955                    MediaStore.Audio.Media.ALBUM + " AS data2",
956                    MediaStore.Audio.Media.ARTIST_KEY +
957                    "||' '||" +
958                    MediaStore.Audio.Media.ALBUM_KEY +
959                    "||' '||" +
960                    MediaStore.Audio.Media.TITLE_KEY +
961                    " AS ar_al_ti",
962                    "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
963                    "'content://media/external/audio/media/'||audio._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA,
964                    "'3' AS grouporder",
965                    "title_key AS itemorder"
966            };
967
968            String where = MediaStore.Audio.Media.TITLE + " != ''";
969
970            for (int i = 0; i < searchWords.length; i++) {
971                where += " AND ar_al_ti LIKE ?";
972            }
973            qb = new SQLiteQueryBuilder();
974            qb.setTables("audio");
975            UQs[2] = qb.buildUnionSubQuery(MediaStore.Audio.Media.MIME_TYPE,
976                    ccols, tablecolumns, 12, "audio/", where, null, null, null);
977        }
978
979        if (mCursor != null) {
980            mCursor.deactivate();
981            mCursor = null;
982        }
983        if (UQs[0] != null && UQs[1] != null && UQs[2] != null) {
984            String union = qb.buildUnionQuery(UQs, "grouporder,itemorder", null);
985            mCursor = db.rawQuery(union, wildcardWords3);
986        }
987
988        return mCursor;
989    }
990
991    @Override
992    public String getType(Uri url)
993    {
994        switch (URI_MATCHER.match(url)) {
995            case IMAGES_MEDIA_ID:
996            case AUDIO_MEDIA_ID:
997            case AUDIO_GENRES_ID_MEMBERS_ID:
998            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
999            case VIDEO_MEDIA_ID:
1000                Cursor c = query(url, MIME_TYPE_PROJECTION, null, null, null);
1001                if (c != null && c.getCount() == 1) {
1002                    c.moveToFirst();
1003                    String mimeType = c.getString(1);
1004                    c.deactivate();
1005                    return mimeType;
1006                }
1007                break;
1008
1009            case IMAGES_MEDIA:
1010            case IMAGES_THUMBNAILS:
1011                return Images.Media.CONTENT_TYPE;
1012            case IMAGES_THUMBNAILS_ID:
1013                return "image/jpeg";
1014
1015            case AUDIO_MEDIA:
1016            case AUDIO_GENRES_ID_MEMBERS:
1017            case AUDIO_PLAYLISTS_ID_MEMBERS:
1018                return Audio.Media.CONTENT_TYPE;
1019
1020            case AUDIO_GENRES:
1021            case AUDIO_MEDIA_ID_GENRES:
1022                return Audio.Genres.CONTENT_TYPE;
1023            case AUDIO_GENRES_ID:
1024            case AUDIO_MEDIA_ID_GENRES_ID:
1025                return Audio.Genres.ENTRY_CONTENT_TYPE;
1026            case AUDIO_PLAYLISTS:
1027            case AUDIO_MEDIA_ID_PLAYLISTS:
1028                return Audio.Playlists.CONTENT_TYPE;
1029            case AUDIO_PLAYLISTS_ID:
1030            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
1031                return Audio.Playlists.ENTRY_CONTENT_TYPE;
1032
1033            case VIDEO_MEDIA:
1034                return Video.Media.CONTENT_TYPE;
1035        }
1036        throw new IllegalStateException("Unknown URL");
1037    }
1038
1039    /**
1040     * Ensures there is a file in the _data column of values, if one isn't
1041     * present a new file is created.
1042     *
1043     * @param initialValues the values passed to insert by the caller
1044     * @return the new values
1045     */
1046    private ContentValues ensureFile(boolean internal, ContentValues initialValues,
1047            String preferredExtension, String directoryName) {
1048        ContentValues values;
1049        String file = initialValues.getAsString("_data");
1050        if (TextUtils.isEmpty(file)) {
1051            file = generateFileName(internal, preferredExtension, directoryName);
1052            values = new ContentValues(initialValues);
1053            values.put("_data", file);
1054        } else {
1055            values = initialValues;
1056        }
1057
1058        if (!ensureFileExists(file)) {
1059            throw new IllegalStateException("Unable to create new file: " + file);
1060        }
1061        return values;
1062    }
1063
1064    @Override
1065    public int bulkInsert(Uri uri, ContentValues values[]) {
1066        int match = URI_MATCHER.match(uri);
1067        if (match == VOLUMES) {
1068            return super.bulkInsert(uri, values);
1069        }
1070        DatabaseHelper database = getDatabaseForUri(uri);
1071        if (database == null) {
1072            throw new UnsupportedOperationException(
1073                    "Unknown URI: " + uri);
1074        }
1075        SQLiteDatabase db = database.getWritableDatabase();
1076        db.beginTransaction();
1077        int numInserted = 0;
1078        try {
1079            int len = values.length;
1080            for (int i = 0; i < len; i++) {
1081                insertInternal(uri, values[i]);
1082            }
1083            numInserted = len;
1084            db.setTransactionSuccessful();
1085        } finally {
1086            db.endTransaction();
1087        }
1088        getContext().getContentResolver().notifyChange(uri, null);
1089        return numInserted;
1090    }
1091
1092    @Override
1093    public Uri insert(Uri uri, ContentValues initialValues)
1094    {
1095        Uri newUri = insertInternal(uri, initialValues);
1096        if (newUri != null) {
1097            getContext().getContentResolver().notifyChange(uri, null);
1098        }
1099
1100        return newUri;
1101    }
1102
1103    private Uri insertInternal(Uri uri, ContentValues initialValues) {
1104        long rowId;
1105        int match = URI_MATCHER.match(uri);
1106
1107        // handle MEDIA_SCANNER before calling getDatabaseForUri()
1108        if (match == MEDIA_SCANNER) {
1109            mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
1110            return MediaStore.getMediaScannerUri();
1111        }
1112
1113        Uri newUri = null;
1114        DatabaseHelper database = getDatabaseForUri(uri);
1115        if (database == null && match != VOLUMES) {
1116            throw new UnsupportedOperationException(
1117                    "Unknown URI: " + uri);
1118        }
1119        SQLiteDatabase db = (match == VOLUMES ? null : database.getWritableDatabase());
1120
1121        if (initialValues == null) {
1122            initialValues = new ContentValues();
1123        }
1124
1125        switch (match) {
1126            case IMAGES_MEDIA: {
1127                ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/Camera");
1128
1129                values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
1130                String data = values.getAsString(MediaColumns.DATA);
1131                if (! values.containsKey(MediaColumns.DISPLAY_NAME)) {
1132                    computeDisplayName(data, values);
1133                }
1134                computeBucketValues(data, values);
1135                rowId = db.insert("images", "name", values);
1136
1137                if (rowId > 0) {
1138                    newUri = ContentUris.withAppendedId(
1139                            Images.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
1140                }
1141                break;
1142            }
1143
1144            case IMAGES_THUMBNAILS: {
1145                ContentValues values = ensureFile(database.mInternal, initialValues, ".jpg", "DCIM/.thumbnails");
1146                rowId = db.insert("thumbnails", "name", values);
1147                if (rowId > 0) {
1148                    newUri = ContentUris.withAppendedId(Images.Thumbnails.
1149                            getContentUri(uri.getPathSegments().get(0)), rowId);
1150                }
1151                break;
1152            }
1153
1154            case AUDIO_MEDIA: {
1155                // SQLite Views are read-only, so we need to deconstruct this
1156                // insert and do inserts into the underlying tables.
1157                // If doing this here turns out to be a performance bottleneck,
1158                // consider moving this to native code and using triggers on
1159                // the view.
1160                ContentValues values = new ContentValues(initialValues);
1161
1162                // Insert the artist into the artist table and remove it from
1163                // the input values
1164                Object so = values.get("artist");
1165                String s = (so == null ? "" : so.toString());
1166                values.remove("artist");
1167                long artistRowId;
1168                HashMap<String, Long> artistCache = database.mArtistCache;
1169                synchronized(artistCache) {
1170                    Long temp = artistCache.get(s);
1171                    if (temp == null) {
1172                        artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
1173                                s, null, artistCache, uri);
1174                    } else {
1175                        artistRowId = temp.longValue();
1176                    }
1177                }
1178
1179                // Do the same for the album field
1180                so = values.get("album");
1181                s = (so == null ? "" : so.toString());
1182                values.remove("album");
1183                long albumRowId;
1184                HashMap<String, Long> albumCache = database.mAlbumCache;
1185                synchronized(albumCache) {
1186                    Long temp = albumCache.get(s);
1187                    if (temp == null) {
1188                        String path = values.getAsString("_data");
1189                        albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
1190                                s, path, albumCache, uri);
1191                    } else {
1192                        albumRowId = temp;
1193                    }
1194                }
1195
1196                values.put("artist_id", Integer.toString((int)artistRowId));
1197                values.put("album_id", Integer.toString((int)albumRowId));
1198                so = values.getAsString("title");
1199                s = (so == null ? "" : so.toString());
1200                values.put("title_key", MediaStore.Audio.keyFor(s));
1201
1202                computeDisplayName(values.getAsString("_data"), values);
1203                values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
1204
1205                rowId = db.insert("audio_meta", "duration", values);
1206                if (rowId > 0) {
1207                    newUri = ContentUris.withAppendedId(Audio.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
1208                }
1209                break;
1210            }
1211
1212            case AUDIO_MEDIA_ID_GENRES: {
1213                Long audioId = Long.parseLong(uri.getPathSegments().get(2));
1214                ContentValues values = new ContentValues(initialValues);
1215                values.put(Audio.Genres.Members.AUDIO_ID, audioId);
1216                rowId = db.insert("audio_playlists_map", "genre_id", values);
1217                if (rowId > 0) {
1218                    newUri = ContentUris.withAppendedId(uri, rowId);
1219                }
1220                break;
1221            }
1222
1223            case AUDIO_MEDIA_ID_PLAYLISTS: {
1224                Long audioId = Long.parseLong(uri.getPathSegments().get(2));
1225                ContentValues values = new ContentValues(initialValues);
1226                values.put(Audio.Playlists.Members.AUDIO_ID, audioId);
1227                rowId = db.insert("audio_playlists_map", "playlist_id",
1228                        values);
1229                if (rowId > 0) {
1230                    newUri = ContentUris.withAppendedId(uri, rowId);
1231                }
1232                break;
1233            }
1234
1235            case AUDIO_GENRES: {
1236                rowId = db.insert("audio_genres", "audio_id", initialValues);
1237                if (rowId > 0) {
1238                    newUri = ContentUris.withAppendedId(Audio.Genres.getContentUri(uri.getPathSegments().get(0)), rowId);
1239                }
1240                break;
1241            }
1242
1243            case AUDIO_GENRES_ID_MEMBERS: {
1244                Long genreId = Long.parseLong(uri.getPathSegments().get(3));
1245                ContentValues values = new ContentValues(initialValues);
1246                values.put(Audio.Genres.Members.GENRE_ID, genreId);
1247                rowId = db.insert("audio_genres_map", "genre_id", values);
1248                if (rowId > 0) {
1249                    newUri = ContentUris.withAppendedId(uri, rowId);
1250                }
1251                break;
1252            }
1253
1254            case AUDIO_PLAYLISTS: {
1255                ContentValues values = new ContentValues(initialValues);
1256                values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
1257                rowId = db.insert("audio_playlists", "name", initialValues);
1258                if (rowId > 0) {
1259                    newUri = ContentUris.withAppendedId(Audio.Playlists.getContentUri(uri.getPathSegments().get(0)), rowId);
1260                }
1261                break;
1262            }
1263
1264            case AUDIO_PLAYLISTS_ID:
1265            case AUDIO_PLAYLISTS_ID_MEMBERS: {
1266                Long playlistId = Long.parseLong(uri.getPathSegments().get(3));
1267                ContentValues values = new ContentValues(initialValues);
1268                values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId);
1269                rowId = db.insert("audio_playlists_map", "playlist_id",
1270                        values);
1271                if (rowId > 0) {
1272                    newUri = ContentUris.withAppendedId(uri, rowId);
1273                }
1274                break;
1275            }
1276
1277            case VIDEO_MEDIA: {
1278                ContentValues values = ensureFile(database.mInternal, initialValues, ".3gp", "video");
1279                String data = values.getAsString("_data");
1280                computeDisplayName(data, values);
1281                computeBucketValues(data, values);
1282                values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
1283                rowId = db.insert("video", "artist", values);
1284                if (rowId > 0) {
1285                    newUri = ContentUris.withAppendedId(Video.Media.getContentUri(uri.getPathSegments().get(0)), rowId);
1286                }
1287                break;
1288            }
1289
1290            case AUDIO_ALBUMART:
1291                if (database.mInternal) {
1292                    throw new UnsupportedOperationException("no internal album art allowed");
1293                }
1294                ContentValues values = null;
1295                try {
1296                    values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
1297                } catch (IllegalStateException ex) {
1298                    // probably no more room to store albumthumbs
1299                    values = initialValues;
1300                }
1301                rowId = db.insert("album_art", "_data", values);
1302                if (rowId > 0) {
1303                    newUri = ContentUris.withAppendedId(uri, rowId);
1304                }
1305                break;
1306
1307            case VOLUMES:
1308                return attachVolume(initialValues.getAsString("name"));
1309
1310            default:
1311                throw new UnsupportedOperationException("Invalid URI " + uri);
1312        }
1313
1314        return newUri;
1315    }
1316
1317    private String generateFileName(boolean internal, String preferredExtension, String directoryName)
1318    {
1319        // create a random file
1320        String name = String.valueOf(System.currentTimeMillis());
1321
1322        if (internal) {
1323            throw new UnsupportedOperationException("Writing to internal storage is not supported.");
1324//            return Environment.getDataDirectory()
1325//                + "/" + directoryName + "/" + name + preferredExtension;
1326        } else {
1327            return Environment.getExternalStorageDirectory()
1328                + "/" + directoryName + "/" + name + preferredExtension;
1329        }
1330    }
1331
1332    private boolean ensureFileExists(String path) {
1333        File file = new File(path);
1334        if (file.exists()) {
1335            return true;
1336        } else {
1337            // we will not attempt to create the first directory in the path
1338            // (for example, do not create /sdcard if the SD card is not mounted)
1339            int secondSlash = path.indexOf('/', 1);
1340            if (secondSlash < 1) return false;
1341            String directoryPath = path.substring(0, secondSlash);
1342            File directory = new File(directoryPath);
1343            if (!directory.exists())
1344                return false;
1345            file.getParentFile().mkdirs();
1346            try {
1347                return file.createNewFile();
1348            } catch(IOException ioe) {
1349                Log.e(TAG, "File creation failed", ioe);
1350            }
1351            return false;
1352        }
1353    }
1354
1355    private static final class GetTableAndWhereOutParameter {
1356        public String table;
1357        public String where;
1358    }
1359
1360    static final GetTableAndWhereOutParameter sGetTableAndWhereParam =
1361            new GetTableAndWhereOutParameter();
1362
1363    private void getTableAndWhere(Uri uri, int match, String userWhere,
1364            GetTableAndWhereOutParameter out) {
1365        String where = null;
1366        switch (match) {
1367            case IMAGES_MEDIA_ID:
1368                out.table = "images";
1369                where = "_id = " + uri.getPathSegments().get(3);
1370                break;
1371
1372            case AUDIO_MEDIA:
1373                out.table = "audio";
1374                break;
1375
1376            case AUDIO_MEDIA_ID:
1377                out.table = "audio";
1378                where = "_id=" + uri.getPathSegments().get(3);
1379                break;
1380
1381            case AUDIO_MEDIA_ID_GENRES:
1382                out.table = "audio_genres";
1383                where = "audio_id=" + uri.getPathSegments().get(3);
1384                break;
1385
1386            case AUDIO_MEDIA_ID_GENRES_ID:
1387                out.table = "audio_genres";
1388                where = "audio_id=" + uri.getPathSegments().get(3) +
1389                        " AND genre_id=" + uri.getPathSegments().get(5);
1390               break;
1391
1392            case AUDIO_MEDIA_ID_PLAYLISTS:
1393                out.table = "audio_playlists";
1394                where = "audio_id=" + uri.getPathSegments().get(3);
1395                break;
1396
1397            case AUDIO_MEDIA_ID_PLAYLISTS_ID:
1398                out.table = "audio_playlists";
1399                where = "audio_id=" + uri.getPathSegments().get(3) +
1400                        " AND playlists_id=" + uri.getPathSegments().get(5);
1401                break;
1402
1403            case AUDIO_GENRES:
1404                out.table = "audio_genres";
1405                break;
1406
1407            case AUDIO_GENRES_ID:
1408                out.table = "audio_genres";
1409                where = "_id=" + uri.getPathSegments().get(3);
1410                break;
1411
1412            case AUDIO_GENRES_ID_MEMBERS:
1413                out.table = "audio_genres";
1414                where = "genre_id=" + uri.getPathSegments().get(3);
1415                break;
1416
1417            case AUDIO_GENRES_ID_MEMBERS_ID:
1418                out.table = "audio_genres";
1419                where = "genre_id=" + uri.getPathSegments().get(3) +
1420                        " AND audio_id =" + uri.getPathSegments().get(5);
1421                break;
1422
1423            case AUDIO_PLAYLISTS:
1424                out.table = "audio_playlists";
1425                break;
1426
1427            case AUDIO_PLAYLISTS_ID:
1428                out.table = "audio_playlists";
1429                where = "_id=" + uri.getPathSegments().get(3);
1430                break;
1431
1432            case AUDIO_PLAYLISTS_ID_MEMBERS:
1433                out.table = "audio_playlists_map";
1434                where = "playlist_id=" + uri.getPathSegments().get(3);
1435                break;
1436
1437            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
1438                out.table = "audio_playlists_map";
1439                where = "playlist_id=" + uri.getPathSegments().get(3) +
1440                        " AND _id=" + uri.getPathSegments().get(5);
1441                break;
1442
1443            case AUDIO_ALBUMART_ID:
1444                out.table = "album_art";
1445                where = "album_id=" + uri.getPathSegments().get(3);
1446                break;
1447
1448            case VIDEO_MEDIA:
1449                out.table = "video";
1450                break;
1451
1452            case VIDEO_MEDIA_ID:
1453                out.table = "video";
1454                where = "_id=" + uri.getPathSegments().get(3);
1455                break;
1456
1457            default:
1458                throw new UnsupportedOperationException(
1459                        "Unknown or unsupported URL: " + uri.toString());
1460        }
1461
1462        // Add in the user requested WHERE clause, if needed
1463        if (!TextUtils.isEmpty(userWhere)) {
1464            if (!TextUtils.isEmpty(where)) {
1465                out.where = where + " AND (" + userWhere + ")";
1466            } else {
1467                out.where = userWhere;
1468            }
1469        } else {
1470            out.where = where;
1471        }
1472    }
1473
1474    @Override
1475    public int delete(Uri uri, String userWhere, String[] whereArgs) {
1476        int count;
1477        int match = URI_MATCHER.match(uri);
1478
1479        // handle MEDIA_SCANNER before calling getDatabaseForUri()
1480        if (match == MEDIA_SCANNER) {
1481            if (mMediaScannerVolume == null) {
1482                return 0;
1483            }
1484            mMediaScannerVolume = null;
1485            return 1;
1486        }
1487
1488        if (match != VOLUMES_ID) {
1489            DatabaseHelper database = getDatabaseForUri(uri);
1490            if (database == null) {
1491                throw new UnsupportedOperationException(
1492                        "Unknown URI: " + uri);
1493            }
1494            SQLiteDatabase db = database.getWritableDatabase();
1495
1496            synchronized (sGetTableAndWhereParam) {
1497                getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
1498                switch (match) {
1499                    case AUDIO_MEDIA:
1500                    case AUDIO_MEDIA_ID:
1501                        count = db.delete("audio_meta",
1502                                sGetTableAndWhereParam.where, whereArgs);
1503                        break;
1504                    default:
1505                        count = db.delete(sGetTableAndWhereParam.table,
1506                                sGetTableAndWhereParam.where, whereArgs);
1507                        break;
1508                }
1509                getContext().getContentResolver().notifyChange(uri, null);
1510            }
1511        } else {
1512            detachVolume(uri);
1513            count = 1;
1514        }
1515
1516        return count;
1517    }
1518
1519    @Override
1520    public int update(Uri uri, ContentValues initialValues, String userWhere,
1521            String[] whereArgs) {
1522        int count;
1523        int match = URI_MATCHER.match(uri);
1524
1525        DatabaseHelper database = getDatabaseForUri(uri);
1526        if (database == null) {
1527            throw new UnsupportedOperationException(
1528                    "Unknown URI: " + uri);
1529        }
1530        SQLiteDatabase db = database.getWritableDatabase();
1531
1532        synchronized (sGetTableAndWhereParam) {
1533            getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
1534
1535            switch (match) {
1536                case AUDIO_MEDIA:
1537                case AUDIO_MEDIA_ID:
1538                    {
1539                        ContentValues values = new ContentValues(initialValues);
1540                        // Insert the artist into the artist table and remove it from
1541                        // the input values
1542                        String so = values.getAsString("artist");
1543                        if (so != null) {
1544                            String s = so.toString();
1545                            values.remove("artist");
1546                            long artistRowId;
1547                            HashMap<String, Long> artistCache = database.mArtistCache;
1548                            synchronized(artistCache) {
1549                                Long temp = artistCache.get(s);
1550                                if (temp == null) {
1551                                    artistRowId = getKeyIdForName(db, "artists", "artist_key", "artist",
1552                                            s, null, artistCache, uri);
1553                                } else {
1554                                    artistRowId = temp.longValue();
1555                                }
1556                            }
1557                            values.put("artist_id", Integer.toString((int)artistRowId));
1558                        }
1559
1560                        // Do the same for the album field
1561                        so = values.getAsString("album");
1562                        if (so != null) {
1563                            String s = so.toString();
1564                            values.remove("album");
1565                            long albumRowId;
1566                            HashMap<String, Long> albumCache = database.mAlbumCache;
1567                            synchronized(albumCache) {
1568                                Long temp = albumCache.get(s);
1569                                if (temp == null) {
1570                                    albumRowId = getKeyIdForName(db, "albums", "album_key", "album",
1571                                            s, null, albumCache, uri);
1572                                } else {
1573                                    albumRowId = temp.longValue();
1574                                }
1575                            }
1576                            values.put("album_id", Integer.toString((int)albumRowId));
1577                        }
1578
1579                        // don't allow the title_key field to be updated directly
1580                        values.remove("title_key");
1581                        // If the title field is modified, update the title_key
1582                        so = values.getAsString("title");
1583                        if (so != null) {
1584                            String s = so.toString();
1585                            values.put("title_key", MediaStore.Audio.keyFor(s));
1586                        }
1587
1588                        count = db.update("audio_meta", values, sGetTableAndWhereParam.where,
1589                                whereArgs);
1590                    }
1591                    break;
1592                case IMAGES_MEDIA:
1593                case IMAGES_MEDIA_ID:
1594                case VIDEO_MEDIA:
1595                case VIDEO_MEDIA_ID:
1596                    {
1597                        ContentValues values = new ContentValues(initialValues);
1598                        // Don't allow bucket id or display name to be updated directly.
1599                        // The same names are used for both images and table columns, so
1600                        // we use the ImageColumns constants here.
1601                        values.remove(ImageColumns.BUCKET_ID);
1602                        values.remove(ImageColumns.BUCKET_DISPLAY_NAME);
1603                        // If the data is being modified update the bucket values
1604                        String data = values.getAsString(MediaColumns.DATA);
1605                        if (data != null) {
1606                            computeBucketValues(data, values);
1607                        }
1608                        count = db.update(sGetTableAndWhereParam.table, values,
1609                                sGetTableAndWhereParam.where, whereArgs);
1610                    }
1611                    break;
1612                default:
1613                    count = db.update(sGetTableAndWhereParam.table, initialValues,
1614                        sGetTableAndWhereParam.where, whereArgs);
1615                    break;
1616            }
1617        }
1618        if (count > 0) {
1619            getContext().getContentResolver().notifyChange(uri, null);
1620        }
1621        return count;
1622    }
1623
1624    private static final String[] openFileColumns = new String[] {
1625        MediaStore.MediaColumns.DATA,
1626    };
1627
1628    @Override
1629    public ParcelFileDescriptor openFile(Uri uri, String mode)
1630            throws FileNotFoundException {
1631        ParcelFileDescriptor pfd = null;
1632        try {
1633            pfd = openFileHelper(uri, mode);
1634        } catch (FileNotFoundException ex) {
1635            if (URI_MATCHER.match(uri) == AUDIO_ALBUMART_ID) {
1636                // Tried to open an album art file which does not exist. Regenerate.
1637                DatabaseHelper database = getDatabaseForUri(uri);
1638                if (database == null) {
1639                    throw ex;
1640                }
1641                SQLiteDatabase db = database.getReadableDatabase();
1642                SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1643                int albumid = Integer.parseInt(uri.getPathSegments().get(3));
1644                qb.setTables("audio");
1645                qb.appendWhere("album_id=" + albumid);
1646                Cursor c = qb.query(db,
1647                        new String [] {
1648                            MediaStore.Audio.Media.DATA },
1649                        null, null, null, null, null);
1650                c.moveToFirst();
1651                if (!c.isAfterLast()) {
1652                    String audiopath = c.getString(0);
1653                    makeThumb(db, audiopath, albumid, uri);
1654                }
1655                c.close();
1656            }
1657            throw ex;
1658        }
1659        return pfd;
1660    }
1661
1662    private class Worker implements Runnable {
1663        private final Object mLock = new Object();
1664        private Looper mLooper;
1665
1666        Worker(String name) {
1667            Thread t = new Thread(null, this, name);
1668            t.setPriority(Thread.MIN_PRIORITY);
1669            t.start();
1670            synchronized (mLock) {
1671                while (mLooper == null) {
1672                    try {
1673                        mLock.wait();
1674                    } catch (InterruptedException ex) {
1675                    }
1676                }
1677            }
1678        }
1679
1680        public Looper getLooper() {
1681            return mLooper;
1682        }
1683
1684        public void run() {
1685            synchronized (mLock) {
1686                Looper.prepare();
1687                mLooper = Looper.myLooper();
1688                mLock.notifyAll();
1689            }
1690            Looper.loop();
1691        }
1692
1693        public void quit() {
1694            mLooper.quit();
1695        }
1696    }
1697
1698    private class ThumbData {
1699        SQLiteDatabase db;
1700        String path;
1701        long album_id;
1702        Uri albumart_uri;
1703    }
1704
1705    private void makeThumb(SQLiteDatabase db, String path, long album_id,
1706            Uri albumart_uri) {
1707        ThumbData d = new ThumbData();
1708        d.db = db;
1709        d.path = path;
1710        d.album_id = album_id;
1711        d.albumart_uri = albumart_uri;
1712        Message msg = mThumbHandler.obtainMessage();
1713        msg.obj = d;
1714        msg.sendToTarget();
1715    }
1716
1717    private void makeThumb(ThumbData d) {
1718        SQLiteDatabase db = d.db;
1719        String path = d.path;
1720        long album_id = d.album_id;
1721        Uri albumart_uri = d.albumart_uri;
1722
1723        try {
1724            File f = new File(path);
1725            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f,
1726                    ParcelFileDescriptor.MODE_READ_ONLY);
1727
1728            MediaScanner scanner = new MediaScanner(getContext());
1729            byte [] art = scanner.extractAlbumArt(pfd.getFileDescriptor());
1730            pfd.close();
1731
1732            // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file
1733            if (art == null && path != null) {
1734                int lastSlash = path.lastIndexOf('/');
1735                if (lastSlash > 0) {
1736                    String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg";
1737                    File file = new File(artPath);
1738                    if (file.exists()) {
1739                        art = new byte[(int)file.length()];
1740                        FileInputStream stream = null;
1741                        try {
1742                            stream = new FileInputStream(file);
1743                            stream.read(art);
1744                        } catch (IOException ex) {
1745                            art = null;
1746                        } finally {
1747                            if (stream != null) {
1748                                stream.close();
1749                            }
1750                        }
1751                    }
1752                }
1753            }
1754
1755            Bitmap bm = null;
1756            if (art != null) {
1757                try {
1758                    // get the size of the bitmap
1759                    BitmapFactory.Options opts = new BitmapFactory.Options();
1760                    opts.inJustDecodeBounds = true;
1761                    opts.inSampleSize = 1;
1762                    BitmapFactory.decodeByteArray(art, 0, art.length, opts);
1763
1764                    // request a reasonably sized output image
1765                    // TODO: don't hardcode the size
1766                    while (opts.outHeight > 320 || opts.outWidth > 320) {
1767                        opts.outHeight /= 2;
1768                        opts.outWidth /= 2;
1769                        opts.inSampleSize *= 2;
1770                    }
1771
1772                    // get the image for real now
1773                    opts.inJustDecodeBounds = false;
1774                    opts.inPreferredConfig = Bitmap.Config.RGB_565;
1775                    bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts);
1776                } catch (Exception e) {
1777                }
1778            }
1779            if (bm != null && bm.getConfig() == null) {
1780                bm = bm.copy(Bitmap.Config.RGB_565, false);
1781            }
1782            if (bm != null) {
1783                // save bitmap
1784                Uri out = null;
1785                // TODO: this could be done more efficiently with a call to db.replace(), which
1786                // replaces or inserts as needed, making it unnecessary to query() first.
1787                if (albumart_uri != null) {
1788                    Cursor c = query(albumart_uri, new String [] { "_data" },
1789                            null, null, null);
1790                    c.moveToFirst();
1791                    if (!c.isAfterLast()) {
1792                        String albumart_path = c.getString(0);
1793                        if (ensureFileExists(albumart_path)) {
1794                            out = albumart_uri;
1795                        }
1796                    }
1797                    c.close();
1798                } else {
1799                    ContentValues initialValues = new ContentValues();
1800                    initialValues.put("album_id", album_id);
1801                    try {
1802                        ContentValues values = ensureFile(false, initialValues, "", ALBUM_THUMB_FOLDER);
1803                        long rowId = db.insert("album_art", "_data", values);
1804                        if (rowId > 0) {
1805                            out = ContentUris.withAppendedId(ALBUMART_URI, rowId);
1806                        }
1807                    } catch (IllegalStateException ex) {
1808                        Log.e(TAG, "error creating album thumb file");
1809                    }
1810                }
1811                if (out != null) {
1812                    boolean success = false;
1813                    try {
1814                        OutputStream outstream = getContext().getContentResolver().openOutputStream(out);
1815                        success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
1816                        outstream.close();
1817                    } catch (FileNotFoundException ex) {
1818                        Log.e(TAG, "error creating file", ex);
1819                    } catch (IOException ex) {
1820                        Log.e(TAG, "error creating file", ex);
1821                    }
1822                    if (!success) {
1823                        // the thumbnail was not written successfully, delete the entry that refers to it
1824                        getContext().getContentResolver().delete(out, null, null);
1825                    }
1826                }
1827                getContext().getContentResolver().notifyChange(MEDIA_URI, null);
1828            }
1829        } catch (IOException ex) {
1830        }
1831
1832    }
1833
1834    /**
1835     * Look up the artist or album entry for the given name, creating that entry
1836     * if it does not already exists.
1837     * @param db        The database
1838     * @param table     The table to store the key/name pair in.
1839     * @param keyField  The name of the key-column
1840     * @param nameField The name of the name-column
1841     * @param rawName   The name that the calling app was trying to insert into the database
1842     * @param path      The path to the file being inserted in to the audio table
1843     * @param cache     The cache to add this entry to
1844     * @param srcuri    The Uri that prompted the call to this method, used for determining whether this is
1845     *                  the internal or external database
1846     * @return          The row ID for this artist/album, or -1 if the provided name was invalid
1847     */
1848    private long getKeyIdForName(SQLiteDatabase db, String table, String keyField, String nameField,
1849            String rawName, String path, HashMap<String, Long> cache, Uri srcuri) {
1850        long rowId;
1851
1852        if (rawName == null || rawName.length() == 0) {
1853            return -1;
1854        }
1855        String k = MediaStore.Audio.keyFor(rawName);
1856
1857        if (k == null) {
1858            return -1;
1859        }
1860
1861        String [] selargs = { k };
1862        Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null);
1863
1864        try {
1865            switch (c.getCount()) {
1866                case 0: {
1867                        // insert new entry into table
1868                        ContentValues otherValues = new ContentValues();
1869                        otherValues.put(keyField, k);
1870                        otherValues.put(nameField, rawName);
1871                        rowId = db.insert(table, "duration", otherValues);
1872                        if (path != null && table.equals("albums") &&
1873                                ! rawName.equals(MediaFile.UNKNOWN_STRING)) {
1874                            // We just inserted a new album. Now create an album art thumbnail for it.
1875                            makeThumb(db, path, rowId, null);
1876                        }
1877                        if (rowId > 0) {
1878                            String volume = srcuri.toString().substring(16, 24); // extract internal/external
1879                            Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
1880                            getContext().getContentResolver().notifyChange(uri, null);
1881                        }
1882                    }
1883                    break;
1884                case 1: {
1885                        // Use the existing entry
1886                        c.moveToFirst();
1887                        rowId = c.getLong(0);
1888
1889                        // Determine whether the current rawName is better than what's
1890                        // currently stored in the table, and update the table if it is.
1891                        String currentFancyName = c.getString(2);
1892                        String bestName = makeBestName(rawName, currentFancyName);
1893                        if (!bestName.equals(currentFancyName)) {
1894                            // update the table with the new name
1895                            ContentValues newValues = new ContentValues();
1896                            newValues.put(nameField, bestName);
1897                            db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null);
1898                            String volume = srcuri.toString().substring(16, 24); // extract internal/external
1899                            Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId);
1900                            getContext().getContentResolver().notifyChange(uri, null);
1901                        }
1902                    }
1903                    break;
1904                default:
1905                    // corrupt database
1906                    Log.e(TAG, "Multiple entries in table " + table + " for key " + k);
1907                    rowId = -1;
1908                    break;
1909            }
1910        } finally {
1911            if (c != null) c.close();
1912        }
1913
1914        if (cache != null && ! rawName.equals(MediaFile.UNKNOWN_STRING)) {
1915            cache.put(rawName, rowId);
1916        }
1917        return rowId;
1918    }
1919
1920    /**
1921     * Returns the best string to use for display, given two names.
1922     * Note that this function does not necessarily return either one
1923     * of the provided names; it may decide to return a better alternative
1924     * (for example, specifying the inputs "Police" and "Police, The" will
1925     * return "The Police")
1926     *
1927     * The basic assumptions are:
1928     * - longer is better ("The police" is better than "Police")
1929     * - prefix is better ("The Police" is better than "Police, The")
1930     * - accents are better ("Mot&ouml;rhead" is better than "Motorhead")
1931     *
1932     * @param one The first of the two names to consider
1933     * @param two The last of the two names to consider
1934     * @return The actual name to use
1935     */
1936    String makeBestName(String one, String two) {
1937        String name;
1938
1939        // Longer names are usually better.
1940        if (one.length() > two.length()) {
1941            name = one;
1942        } else {
1943            // Names with accents are usually better, and conveniently sort later
1944            if (one.toLowerCase().compareTo(two.toLowerCase()) > 0) {
1945                name = one;
1946            } else {
1947                name = two;
1948            }
1949        }
1950
1951        // Prefixes are better than postfixes.
1952        if (name.endsWith(", the") || name.endsWith(",the") ||
1953            name.endsWith(", an") || name.endsWith(",an") ||
1954            name.endsWith(", a") || name.endsWith(",a")) {
1955            String fix = name.substring(1 + name.lastIndexOf(','));
1956            name = fix.trim() + " " + name.substring(0, name.lastIndexOf(','));
1957        }
1958
1959        // TODO: word-capitalize the resulting name
1960        return name;
1961    }
1962
1963
1964    /**
1965     * Looks up the database based on the given URI.
1966     *
1967     * @param uri The requested URI
1968     * @returns the database for the given URI
1969     */
1970    private DatabaseHelper getDatabaseForUri(Uri uri) {
1971        synchronized (mDatabases) {
1972            if (uri.getPathSegments().size() > 1) {
1973                return mDatabases.get(uri.getPathSegments().get(0));
1974            }
1975        }
1976        return null;
1977    }
1978
1979    /**
1980     * Attach the database for a volume (internal or external).
1981     * Does nothing if the volume is already attached, otherwise
1982     * checks the volume ID and sets up the corresponding database.
1983     *
1984     * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
1985     * @return the content URI of the attached volume.
1986     */
1987    private Uri attachVolume(String volume) {
1988        if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
1989            throw new SecurityException(
1990                    "Opening and closing databases not allowed.");
1991        }
1992
1993        synchronized (mDatabases) {
1994            if (mDatabases.get(volume) != null) {  // Already attached
1995                return Uri.parse("content://media/" + volume);
1996            }
1997
1998            DatabaseHelper db;
1999            if (INTERNAL_VOLUME.equals(volume)) {
2000                db = new DatabaseHelper(getContext(), INTERNAL_DATABASE_NAME, true);
2001            } else if (EXTERNAL_VOLUME.equals(volume)) {
2002                String path = Environment.getExternalStorageDirectory().getPath();
2003                int volumeID = FileUtils.getFatVolumeId(path);
2004                if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID);
2005
2006                // generate database name based on volume ID
2007                String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
2008                db = new DatabaseHelper(getContext(), dbName, false);
2009            } else {
2010                throw new IllegalArgumentException("There is no volume named " + volume);
2011            }
2012
2013            mDatabases.put(volume, db);
2014
2015            if (!db.mInternal) {
2016                // clean up stray album art files: delete every file not in the database
2017                File[] files = new File(
2018                        Environment.getExternalStorageDirectory(),
2019                        ALBUM_THUMB_FOLDER).listFiles();
2020                HashSet<String> fileSet = new HashSet();
2021                for (int i = 0; files != null && i < files.length; i++) {
2022                    fileSet.add(files[i].getPath());
2023                }
2024
2025                Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
2026                        new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null);
2027                try {
2028                    while (cursor != null && cursor.moveToNext()) {
2029                        fileSet.remove(cursor.getString(0));
2030                    }
2031                } finally {
2032                    if (cursor != null) cursor.close();
2033                }
2034
2035                Iterator<String> iterator = fileSet.iterator();
2036                while (iterator.hasNext()) {
2037                    String filename = iterator.next();
2038                    if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename);
2039                    new File(filename).delete();
2040                }
2041            }
2042        }
2043
2044        if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume);
2045        return Uri.parse("content://media/" + volume);
2046    }
2047
2048    /**
2049     * Detach the database for a volume (must be external).
2050     * Does nothing if the volume is already detached, otherwise
2051     * closes the database and sends a notification to listeners.
2052     *
2053     * @param uri The content URI of the volume, as returned by {@link #attachVolume}
2054     */
2055    private void detachVolume(Uri uri) {
2056        if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
2057            throw new SecurityException(
2058                    "Opening and closing databases not allowed.");
2059        }
2060
2061        String volume = uri.getPathSegments().get(0);
2062        if (INTERNAL_VOLUME.equals(volume)) {
2063            throw new UnsupportedOperationException(
2064                    "Deleting the internal volume is not allowed");
2065        } else if (!EXTERNAL_VOLUME.equals(volume)) {
2066            throw new IllegalArgumentException(
2067                    "There is no volume named " + volume);
2068        }
2069
2070        synchronized (mDatabases) {
2071            DatabaseHelper database = mDatabases.get(volume);
2072            if (database == null) return;
2073
2074            try {
2075                // touch the database file to show it is most recently used
2076                File file = new File(database.getReadableDatabase().getPath());
2077                file.setLastModified(System.currentTimeMillis());
2078            } catch (SQLException e) {
2079                Log.e(TAG, "Can't touch database file", e);
2080            }
2081
2082            mDatabases.remove(volume);
2083            database.close();
2084        }
2085
2086        getContext().getContentResolver().notifyChange(uri, null);
2087        if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume);
2088    }
2089
2090    private static String TAG = "MediaProvider";
2091    private static final boolean LOCAL_LOGV = true;
2092    private static final int DATABASE_VERSION = 71;
2093    private static final String INTERNAL_DATABASE_NAME = "internal.db";
2094
2095    // maximum number of cached external databases to keep
2096    private static final int MAX_EXTERNAL_DATABASES = 3;
2097
2098    // Delete databases that have not been used in two months
2099    // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
2100    private static final long OBSOLETE_DATABASE_DB = 5184000000L;
2101
2102    private HashMap<String, DatabaseHelper> mDatabases;
2103
2104    private Worker mThumbWorker;
2105    private Handler mThumbHandler;
2106
2107    // name of the volume currently being scanned by the media scanner (or null)
2108    private String mMediaScannerVolume;
2109
2110    static final String INTERNAL_VOLUME = "internal";
2111    static final String EXTERNAL_VOLUME = "external";
2112    static final String ALBUM_THUMB_FOLDER = "albumthumbs";
2113
2114    // path for writing contents of in memory temp database
2115    private String mTempDatabasePath;
2116
2117    private static final int IMAGES_MEDIA = 1;
2118    private static final int IMAGES_MEDIA_ID = 2;
2119    private static final int IMAGES_THUMBNAILS = 3;
2120    private static final int IMAGES_THUMBNAILS_ID = 4;
2121
2122    private static final int AUDIO_MEDIA = 100;
2123    private static final int AUDIO_MEDIA_ID = 101;
2124    private static final int AUDIO_MEDIA_ID_GENRES = 102;
2125    private static final int AUDIO_MEDIA_ID_GENRES_ID = 103;
2126    private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104;
2127    private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105;
2128    private static final int AUDIO_GENRES = 106;
2129    private static final int AUDIO_GENRES_ID = 107;
2130    private static final int AUDIO_GENRES_ID_MEMBERS = 108;
2131    private static final int AUDIO_GENRES_ID_MEMBERS_ID = 109;
2132    private static final int AUDIO_PLAYLISTS = 110;
2133    private static final int AUDIO_PLAYLISTS_ID = 111;
2134    private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
2135    private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113;
2136    private static final int AUDIO_ARTISTS = 114;
2137    private static final int AUDIO_ARTISTS_ID = 115;
2138    private static final int AUDIO_ALBUMS = 116;
2139    private static final int AUDIO_ALBUMS_ID = 117;
2140    private static final int AUDIO_ARTISTS_ID_ALBUMS = 118;
2141    private static final int AUDIO_ALBUMART = 119;
2142    private static final int AUDIO_ALBUMART_ID = 120;
2143
2144    private static final int VIDEO_MEDIA = 200;
2145    private static final int VIDEO_MEDIA_ID = 201;
2146
2147    private static final int VOLUMES = 300;
2148    private static final int VOLUMES_ID = 301;
2149
2150    private static final int AUDIO_SEARCH = 400;
2151
2152    private static final int MEDIA_SCANNER = 500;
2153
2154    private static final UriMatcher URI_MATCHER =
2155            new UriMatcher(UriMatcher.NO_MATCH);
2156
2157    private static final String[] MIME_TYPE_PROJECTION = new String[] {
2158            MediaStore.MediaColumns._ID, // 0
2159            MediaStore.MediaColumns.MIME_TYPE, // 1
2160    };
2161
2162    private static final String[] EXTERNAL_DATABASE_TABLES = new String[] {
2163        "images",
2164        "thumbnails",
2165        "audio_meta",
2166        "artists",
2167        "albums",
2168        "audio_genres",
2169        "audio_genres_map",
2170        "audio_playlists",
2171        "audio_playlists_map",
2172        "video",
2173    };
2174
2175    static
2176    {
2177        URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
2178        URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);
2179        URI_MATCHER.addURI("media", "*/images/thumbnails", IMAGES_THUMBNAILS);
2180        URI_MATCHER.addURI("media", "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
2181
2182        URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
2183        URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
2184        URI_MATCHER.addURI("media", "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
2185        URI_MATCHER.addURI("media", "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
2186        URI_MATCHER.addURI("media", "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS);
2187        URI_MATCHER.addURI("media", "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID);
2188        URI_MATCHER.addURI("media", "*/audio/genres", AUDIO_GENRES);
2189        URI_MATCHER.addURI("media", "*/audio/genres/#", AUDIO_GENRES_ID);
2190        URI_MATCHER.addURI("media", "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
2191        URI_MATCHER.addURI("media", "*/audio/genres/#/members/#", AUDIO_GENRES_ID_MEMBERS_ID);
2192        URI_MATCHER.addURI("media", "*/audio/playlists", AUDIO_PLAYLISTS);
2193        URI_MATCHER.addURI("media", "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
2194        URI_MATCHER.addURI("media", "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
2195        URI_MATCHER.addURI("media", "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
2196        URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
2197        URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
2198        URI_MATCHER.addURI("media", "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
2199        URI_MATCHER.addURI("media", "*/audio/albums", AUDIO_ALBUMS);
2200        URI_MATCHER.addURI("media", "*/audio/albums/#", AUDIO_ALBUMS_ID);
2201        URI_MATCHER.addURI("media", "*/audio/albumart", AUDIO_ALBUMART);
2202        URI_MATCHER.addURI("media", "*/audio/albumart/#", AUDIO_ALBUMART_ID);
2203
2204        URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
2205        URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
2206
2207        URI_MATCHER.addURI("media", "*/media_scanner", MEDIA_SCANNER);
2208
2209        URI_MATCHER.addURI("media", "*", VOLUMES_ID);
2210        URI_MATCHER.addURI("media", null, VOLUMES);
2211
2212        URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY,
2213                AUDIO_SEARCH);
2214        URI_MATCHER.addURI("media", "*/audio/" + SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
2215                AUDIO_SEARCH);
2216    }
2217}
2218