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