• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DataModel(Context context)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 
onQueryByFolder(String parentId, Result<List<MediaItem>> result)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 
onQueryByAlbum(String parentId, Result<List<MediaItem>> result)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 
onQueryByArtist(String parentId, Result<List<MediaItem>> result)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 
onQueryByGenre(String parentId, Result<List<MediaItem>> result)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 
queryInBackground(Result<List<MediaItem>> result, AsyncTask<Void, Void, Void> task)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 
getQueue()149     public List<QueueItem> getQueue() {
150         return mQueue;
151     }
152 
getMetadata(String key)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      */
onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result)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 
FilesystemListTask(Result<List<MediaItem>> result, Uri[] uris, ContentResolver resolver)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
doInBackground(Void... voids)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 
QueryTask(Builder builder)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
doInBackground(Void... voids)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 
setColumns(String[] columns)415             public Builder setColumns(String[] columns) {
416                 mColumns = columns;
417                 return this;
418             }
419 
setWhereClause(String whereClause)420             public Builder setWhereClause(String whereClause) {
421                 mWhereClause = whereClause;
422                 return this;
423             }
424 
setWhereArgs(String[] whereArgs)425             public Builder setWhereArgs(String[] whereArgs) {
426                 mWhereArgs = whereArgs;
427                 return this;
428             }
429 
setUri(Uri[] uris)430             public Builder setUri(Uri[] uris) {
431                 mUris = uris;
432                 return this;
433             }
434 
setKeyColumn(String keyColumn)435             public Builder setKeyColumn(String keyColumn) {
436                 mKeyColumn = keyColumn;
437                 return this;
438             }
439 
setTitleColumn(String titleColumn)440             public Builder setTitleColumn(String titleColumn) {
441                 mTitleColumn = titleColumn;
442                 return this;
443             }
444 
setSubtitleColumn(String subtitleColumn)445             public Builder setSubtitleColumn(String subtitleColumn) {
446                 mSubtitleColumn = subtitleColumn;
447                 return this;
448             }
449 
setFlags(int flags)450             public Builder setFlags(int flags) {
451                 mFlags = flags;
452                 return this;
453             }
454 
setResult(Result<List<MediaItem>> result)455             public Builder setResult(Result<List<MediaItem>> result) {
456                 mResult = result;
457                 return this;
458             }
459 
setResolver(ContentResolver resolver)460             public Builder setResolver(ContentResolver resolver) {
461                 mResolver = resolver;
462                 return this;
463             }
464 
setQueue(List<QueueItem> queue)465             public Builder setQueue(List<QueueItem> queue) {
466                 mQueue = queue;
467                 return this;
468             }
469 
build()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