Home | History | Annotate | Download | only in localmediaplayer
      1 /*
      2  * Copyright (c) 2016, 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 package com.android.car.media.localmediaplayer;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentUris;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.database.sqlite.SQLiteException;
     23 import android.media.MediaDescription;
     24 import android.media.MediaMetadata;
     25 import android.media.browse.MediaBrowser.MediaItem;
     26 import android.media.session.MediaSession.QueueItem;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.provider.MediaStore;
     31 import android.provider.MediaStore.Audio.AlbumColumns;
     32 import android.provider.MediaStore.Audio.AudioColumns;
     33 import android.service.media.MediaBrowserService.Result;
     34 import android.util.Log;
     35 
     36 import java.io.File;
     37 import java.io.FileNotFoundException;
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 import java.util.ArrayList;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Set;
     44 
     45 public class DataModel {
     46     private static final String TAG = "LMBDataModel";
     47 
     48     private static final Uri[] ALL_AUDIO_URI = new Uri[] {
     49             MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
     50             MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
     51     };
     52 
     53     private static final Uri[] ALBUMS_URI = new Uri[] {
     54             MediaStore.Audio.Albums.INTERNAL_CONTENT_URI,
     55             MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
     56     };
     57 
     58     private static final Uri[] ARTISTS_URI = new Uri[] {
     59             MediaStore.Audio.Artists.INTERNAL_CONTENT_URI,
     60             MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI
     61     };
     62 
     63     private static final Uri[] GENRES_URI = new Uri[] {
     64         MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
     65         MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI
     66     };
     67 
     68     private static final String QUERY_BY_KEY_WHERE_CLAUSE =
     69             AudioColumns.ALBUM_KEY + "= ? or "
     70                     + AudioColumns.ARTIST_KEY + " = ? or "
     71                     + AudioColumns.TITLE_KEY + " = ? or "
     72                     + AudioColumns.DATA + " like ?";
     73 
     74     private static final String EXTERNAL = "external";
     75     private static final String INTERNAL = "internal";
     76 
     77     private static final Uri ART_BASE_URI = Uri.parse("content://media/external/audio/albumart");
     78     // Need a context to create this constant so it can't be static.
     79     private final String DEFAULT_ALBUM_ART_URI;
     80 
     81     public static final String PATH_KEY = "PATH";
     82 
     83     private Context mContext;
     84     private ContentResolver mResolver;
     85     private AsyncTask mPendingTask;
     86 
     87     private List<QueueItem> mQueue = new ArrayList<>();
     88 
     89     public DataModel(Context context) {
     90         mContext = context;
     91         mResolver = context.getContentResolver();
     92         DEFAULT_ALBUM_ART_URI =
     93                 Utils.getUriForResource(context, R.drawable.ic_sd_storage_black).toString();
     94     }
     95 
     96     public void onQueryByFolder(String parentId, Result<List<MediaItem>> result) {
     97         FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver);
     98         queryInBackground(result, query);
     99     }
    100 
    101     public void onQueryByAlbum(String parentId, Result<List<MediaItem>> result) {
    102         QueryTask query = new QueryTask.Builder()
    103                 .setResolver(mResolver)
    104                 .setResult(result)
    105                 .setUri(ALBUMS_URI)
    106                 .setKeyColumn(AudioColumns.ALBUM_KEY)
    107                 .setTitleColumn(AudioColumns.ALBUM)
    108                 .setFlags(MediaItem.FLAG_BROWSABLE)
    109                 .build();
    110         queryInBackground(result, query);
    111     }
    112 
    113     public void onQueryByArtist(String parentId, Result<List<MediaItem>> result) {
    114         QueryTask query = new QueryTask.Builder()
    115                 .setResolver(mResolver)
    116                 .setResult(result)
    117                 .setUri(ARTISTS_URI)
    118                 .setKeyColumn(AudioColumns.ARTIST_KEY)
    119                 .setTitleColumn(AudioColumns.ARTIST)
    120                 .setFlags(MediaItem.FLAG_BROWSABLE)
    121                 .build();
    122         queryInBackground(result, query);
    123     }
    124 
    125     public void onQueryByGenre(String parentId, Result<List<MediaItem>> result) {
    126         QueryTask query = new QueryTask.Builder()
    127                 .setResolver(mResolver)
    128                 .setResult(result)
    129                 .setUri(GENRES_URI)
    130                 .setKeyColumn(MediaStore.Audio.Genres._ID)
    131                 .setTitleColumn(MediaStore.Audio.Genres.NAME)
    132                 .setFlags(MediaItem.FLAG_BROWSABLE)
    133                 .build();
    134         queryInBackground(result, query);
    135     }
    136 
    137     private void queryInBackground(Result<List<MediaItem>> result,
    138             AsyncTask<Void, Void, Void> task) {
    139         result.detach();
    140 
    141         if (mPendingTask != null) {
    142             mPendingTask.cancel(true);
    143         }
    144 
    145         mPendingTask = task;
    146         task.execute();
    147     }
    148 
    149     public List<QueueItem> getQueue() {
    150         return mQueue;
    151     }
    152 
    153     public MediaMetadata getMetadata(String key) {
    154         Cursor cursor = null;
    155         MediaMetadata.Builder metadata = new MediaMetadata.Builder();
    156         try {
    157             for (Uri uri : ALL_AUDIO_URI) {
    158                 cursor = mResolver.query(uri, null, AudioColumns.TITLE_KEY + " = ?",
    159                         new String[]{ key }, null);
    160                 if (cursor != null) {
    161                     int title = cursor.getColumnIndex(AudioColumns.TITLE);
    162                     int artist = cursor.getColumnIndex(AudioColumns.ARTIST);
    163                     int album = cursor.getColumnIndex(AudioColumns.ALBUM);
    164                     int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID);
    165                     int duration = cursor.getColumnIndex(AudioColumns.DURATION);
    166 
    167                     while (cursor.moveToNext()) {
    168                         metadata.putString(MediaMetadata.METADATA_KEY_TITLE,
    169                                 cursor.getString(title));
    170                         metadata.putString(MediaMetadata.METADATA_KEY_ARTIST,
    171                                 cursor.getString(artist));
    172                         metadata.putString(MediaMetadata.METADATA_KEY_ALBUM,
    173                                 cursor.getString(album));
    174                         metadata.putLong(MediaMetadata.METADATA_KEY_DURATION,
    175                                 cursor.getLong(duration));
    176 
    177                         String albumArt = DEFAULT_ALBUM_ART_URI;
    178                         Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI,
    179                                 cursor.getLong(albumId));
    180                         try {
    181                             InputStream dummy = mResolver.openInputStream(albumArtUri);
    182                             albumArt = albumArtUri.toString();
    183                             dummy.close();
    184                         } catch (IOException e) {
    185                             // Ignored because the albumArt is intialized correctly anyway.
    186                         }
    187                         metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt);
    188                         break;
    189                     }
    190                 }
    191             }
    192         } finally {
    193             if (cursor != null) {
    194                 cursor.close();
    195             }
    196         }
    197 
    198         return metadata.build();
    199     }
    200 
    201     /**
    202      * Note: This clears out the queue. You should have a local copy of the queue before calling
    203      * this method.
    204      */
    205     public void onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result) {
    206         mQueue.clear();
    207 
    208         QueryTask.Builder query = new QueryTask.Builder()
    209                 .setResolver(mResolver)
    210                 .setResult(result);
    211 
    212         Uri[] uri = null;
    213         if (lastCategory.equals(LocalMediaBrowserService.GENRES_ID)) {
    214             // Genres come from a different table and don't use the where clause from the
    215             // usual media table so we need to have this condition.
    216             try {
    217                 long id = Long.parseLong(parentId);
    218                 query.setUri(new Uri[] {
    219                     MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id),
    220                     MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) });
    221             } catch (NumberFormatException e) {
    222                 // This should never happen.
    223                 Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result");
    224                 result.sendResult(new ArrayList<MediaItem>());
    225                 return;
    226             }
    227         } else {
    228             query.setUri(ALL_AUDIO_URI)
    229                     .setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE)
    230                     .setWhereArgs(new String[] { parentId, parentId, parentId, parentId });
    231         }
    232 
    233         query.setKeyColumn(AudioColumns.TITLE_KEY)
    234                 .setTitleColumn(AudioColumns.TITLE)
    235                 .setSubtitleColumn(AudioColumns.ALBUM)
    236                 .setFlags(MediaItem.FLAG_PLAYABLE)
    237                 .setQueue(mQueue);
    238         queryInBackground(result, query.build());
    239     }
    240 
    241     // This async task is similar enough to all the others that it feels like it can be unified
    242     // but is different enough that unifying it makes the code for both cases look really weird
    243     // and over paramterized so at the risk of being a little more verbose, this is separated out
    244     // in the name of understandability.
    245     private static class FilesystemListTask extends AsyncTask<Void, Void, Void> {
    246         private static final String[] COLUMNS = { AudioColumns.DATA };
    247         private Result<List<MediaItem>> mResult;
    248         private Uri[] mUris;
    249         private ContentResolver mResolver;
    250 
    251         public FilesystemListTask(Result<List<MediaItem>> result, Uri[] uris,
    252                 ContentResolver resolver) {
    253             mResult = result;
    254             mUris = uris;
    255             mResolver = resolver;
    256         }
    257 
    258         @Override
    259         protected Void doInBackground(Void... voids) {
    260             Set<String> paths = new HashSet<String>();
    261 
    262             Cursor cursor = null;
    263             for (Uri uri : mUris) {
    264                 try {
    265                     cursor = mResolver.query(uri, COLUMNS, null , null, null);
    266                     if (cursor != null) {
    267                         int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
    268 
    269                         while (cursor.moveToNext()) {
    270                             // We want to de-dupe paths of each of the songs so we get just a list
    271                             // of containing directories.
    272                             String fullPath = cursor.getString(pathColumn);
    273                             int fileNameStart = fullPath.lastIndexOf(File.separator);
    274                             if (fileNameStart < 0) {
    275                                 continue;
    276                             }
    277 
    278                             String dirPath = fullPath.substring(0, fileNameStart);
    279                             paths.add(dirPath);
    280                         }
    281                     }
    282                 } catch (SQLiteException e) {
    283                     Log.e(TAG, "Failed to execute query " + e);  // Stack trace is noisy.
    284                 } finally {
    285                     if (cursor != null) {
    286                         cursor.close();
    287                     }
    288                 }
    289             }
    290 
    291             // Take the list of deduplicated directories and put them into the results list with
    292             // the full directory path as the key so we can match on it later.
    293             List<MediaItem> results = new ArrayList<>();
    294             for (String path : paths) {
    295                 int dirNameStart = path.lastIndexOf(File.separator) + 1;
    296                 String dirName = path.substring(dirNameStart, path.length());
    297                 MediaDescription description = new MediaDescription.Builder()
    298                         .setMediaId(path + "%")  // Used in a like query.
    299                         .setTitle(dirName)
    300                         .setSubtitle(path)
    301                         .build();
    302                 results.add(new MediaItem(description, MediaItem.FLAG_BROWSABLE));
    303             }
    304             mResult.sendResult(results);
    305             return null;
    306         }
    307     }
    308 
    309     private static class QueryTask extends AsyncTask<Void, Void, Void> {
    310         private Result<List<MediaItem>> mResult;
    311         private String[] mColumns;
    312         private String mWhereClause;
    313         private String[] mWhereArgs;
    314         private String mKeyColumn;
    315         private String mTitleColumn;
    316         private String mSubtitleColumn;
    317         private Uri[] mUris;
    318         private int mFlags;
    319         private ContentResolver mResolver;
    320         private List<QueueItem> mQueue;
    321 
    322         private QueryTask(Builder builder) {
    323             mColumns = builder.mColumns;
    324             mWhereClause = builder.mWhereClause;
    325             mWhereArgs = builder.mWhereArgs;
    326             mKeyColumn = builder.mKeyColumn;
    327             mTitleColumn = builder.mTitleColumn;
    328             mUris = builder.mUris;
    329             mFlags = builder.mFlags;
    330             mResolver = builder.mResolver;
    331             mResult = builder.mResult;
    332             mQueue = builder.mQueue;
    333             mSubtitleColumn = builder.mSubtitleColumn;
    334         }
    335 
    336         @Override
    337         protected Void doInBackground(Void... voids) {
    338             List<MediaItem> results = new ArrayList<>();
    339 
    340             long idx = 0;
    341 
    342             Cursor cursor = null;
    343             for (Uri uri : mUris) {
    344                 try {
    345                     cursor = mResolver.query(uri, mColumns, mWhereClause, mWhereArgs, null);
    346                     if (cursor != null) {
    347                         int keyColumn = cursor.getColumnIndex(mKeyColumn);
    348                         int titleColumn = cursor.getColumnIndex(mTitleColumn);
    349                         int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
    350                         int subtitleColumn = -1;
    351                         if (mSubtitleColumn != null) {
    352                             subtitleColumn = cursor.getColumnIndex(mSubtitleColumn);
    353                         }
    354 
    355                         while (cursor.moveToNext()) {
    356                             Bundle path = new Bundle();
    357                             if (pathColumn != -1) {
    358                                 path.putString(PATH_KEY, cursor.getString(pathColumn));
    359                             }
    360 
    361                             MediaDescription.Builder builder = new MediaDescription.Builder()
    362                                     .setMediaId(cursor.getString(keyColumn))
    363                                     .setTitle(cursor.getString(titleColumn))
    364                                     .setExtras(path);
    365 
    366                             if (subtitleColumn != -1) {
    367                                 builder.setSubtitle(cursor.getString(subtitleColumn));
    368                             }
    369 
    370                             MediaDescription description = builder.build();
    371                             results.add(new MediaItem(description, mFlags));
    372 
    373                             // We rebuild the queue here so if the user selects the item then we
    374                             // can immediately use this queue.
    375                             if (mQueue != null) {
    376                                 mQueue.add(new QueueItem(description, idx));
    377                             }
    378                             idx++;
    379                         }
    380                     }
    381                 } catch (SQLiteException e) {
    382                     // Sometimes tables don't exist if the media scanner hasn't seen data of that
    383                     // type yet. For example, the genres table doesn't seem to exist at all until
    384                     // the first time a song with a genre is encountered. If we hit an exception,
    385                     // the result is never sent causing the other end to hang up, which is a bad
    386                     // thing. We can instead just be resilient and return an empty list.
    387                     Log.i(TAG, "Failed to execute query " + e);  // Stack trace is noisy.
    388                 } finally {
    389                     if (cursor != null) {
    390                         cursor.close();
    391                     }
    392                 }
    393             }
    394 
    395             mResult.sendResult(results);
    396             return null;  // Ignored.
    397         }
    398 
    399         //
    400         // Boilerplate Alert!
    401         //
    402         public static class Builder {
    403             private Result<List<MediaItem>> mResult;
    404             private String[] mColumns;
    405             private String mWhereClause;
    406             private String[] mWhereArgs;
    407             private String mKeyColumn;
    408             private String mTitleColumn;
    409             private String mSubtitleColumn;
    410             private Uri[] mUris;
    411             private int mFlags;
    412             private ContentResolver mResolver;
    413             private List<QueueItem> mQueue;
    414 
    415             public Builder setColumns(String[] columns) {
    416                 mColumns = columns;
    417                 return this;
    418             }
    419 
    420             public Builder setWhereClause(String whereClause) {
    421                 mWhereClause = whereClause;
    422                 return this;
    423             }
    424 
    425             public Builder setWhereArgs(String[] whereArgs) {
    426                 mWhereArgs = whereArgs;
    427                 return this;
    428             }
    429 
    430             public Builder setUri(Uri[] uris) {
    431                 mUris = uris;
    432                 return this;
    433             }
    434 
    435             public Builder setKeyColumn(String keyColumn) {
    436                 mKeyColumn = keyColumn;
    437                 return this;
    438             }
    439 
    440             public Builder setTitleColumn(String titleColumn) {
    441                 mTitleColumn = titleColumn;
    442                 return this;
    443             }
    444 
    445             public Builder setSubtitleColumn(String subtitleColumn) {
    446                 mSubtitleColumn = subtitleColumn;
    447                 return this;
    448             }
    449 
    450             public Builder setFlags(int flags) {
    451                 mFlags = flags;
    452                 return this;
    453             }
    454 
    455             public Builder setResult(Result<List<MediaItem>> result) {
    456                 mResult = result;
    457                 return this;
    458             }
    459 
    460             public Builder setResolver(ContentResolver resolver) {
    461                 mResolver = resolver;
    462                 return this;
    463             }
    464 
    465             public Builder setQueue(List<QueueItem> queue) {
    466                 mQueue = queue;
    467                 return this;
    468             }
    469 
    470             public QueryTask build() {
    471                 if (mUris == null || mKeyColumn == null || mResolver == null ||
    472                         mResult == null || mTitleColumn == null) {
    473                     throw new IllegalStateException(
    474                             "uri, keyColumn, resolver, result and titleColumn are required.");
    475                 }
    476                 return new QueryTask(this);
    477             }
    478         }
    479     }
    480 }
    481