DataModel.java revision 7e37ae05a9c7c9ecf8569a5bfe752a54f660a6ba
1a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono/*
2a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Copyright (c) 2016, The Android Open Source Project
3a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono *
4a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Licensed under the Apache License, Version 2.0 (the "License");
5a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * you may not use this file except in compliance with the License.
6a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * You may obtain a copy of the License at
7a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono *
8a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono *     http://www.apache.org/licenses/LICENSE-2.0
9a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono *
10a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Unless required by applicable law or agreed to in writing, software
11a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * distributed under the License is distributed on an "AS IS" BASIS,
12a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * See the License for the specific language governing permissions and
14a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * limitations under the License.
15a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono */
16a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironopackage com.android.car.media.localmediaplayer;
17a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
18a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.ContentResolver;
19a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.ContentUris;
20a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.Context;
21a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.database.Cursor;
22a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.database.sqlite.SQLiteException;
23a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.MediaDescription;
24a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.MediaMetadata;
25a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.browse.MediaBrowser.MediaItem;
26f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.media.session.MediaSession.QueueItem;
27f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.net.Uri;
28f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.os.AsyncTask;
29a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.os.Bundle;
30a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore;
31a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore.Audio.AlbumColumns;
32a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore.Audio.AudioColumns;
33a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.service.media.MediaBrowserService.Result;
34a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.util.Log;
35a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
36a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.File;
37a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.FileNotFoundException;
38a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.IOException;
39a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.InputStream;
40a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.ArrayList;
41a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.HashSet;
42a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.List;
43f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport java.util.Set;
44a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
45f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironopublic class DataModel {
46f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private static final String TAG = "LMBDataModel";
47f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
48a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private static final Uri[] ALL_AUDIO_URI = new Uri[] {
49a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
50f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
51f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    };
52f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
53a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private static final Uri[] ALBUMS_URI = new Uri[] {
54a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            MediaStore.Audio.Albums.INTERNAL_CONTENT_URI,
55f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
56f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    };
57f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
58a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private static final Uri[] ARTISTS_URI = new Uri[] {
59a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            MediaStore.Audio.Artists.INTERNAL_CONTENT_URI,
60f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI
61f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    };
62f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
63f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private static final Uri[] GENRES_URI = new Uri[] {
64a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
65a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI
66f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    };
67f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
68f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private static final String QUERY_BY_KEY_WHERE_CLAUSE =
69f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            AudioColumns.ALBUM_KEY + "= ? or "
70a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    + AudioColumns.ARTIST_KEY + " = ? or "
71a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    + AudioColumns.TITLE_KEY + " = ? or "
72f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                    + AudioColumns.DATA + " like ?";
73f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
74f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private static final String EXTERNAL = "external";
75a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private static final String INTERNAL = "internal";
76a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
77f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private static final Uri ART_BASE_URI = Uri.parse("content://media/external/audio/albumart");
78f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    // Need a context to create this constant so it can't be static.
79f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private final String DEFAULT_ALBUM_ART_URI;
80a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
81a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public static final String PATH_KEY = "PATH";
82a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
83a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private Context mContext;
84a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private ContentResolver mResolver;
85f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private AsyncTask mPendingTask;
86f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
87f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    private List<QueueItem> mQueue = new ArrayList<>();
88f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
89f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    public DataModel(Context context) {
90f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono        mContext = context;
91f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono        mResolver = context.getContentResolver();
92f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono        DEFAULT_ALBUM_ART_URI =
93f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                Utils.getUriForResource(context, R.drawable.ic_sd_storage_black).toString();
94f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    }
95f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
96f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono    public void onQueryByFolder(String parentId, Result<List<MediaItem>> result) {
97f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono        FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver);
98a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        queryInBackground(result, query);
99a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
100a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
101a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public void onQueryByAlbum(String parentId, Result<List<MediaItem>> result) {
102a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        QueryTask query = new QueryTask.Builder()
103a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResolver(mResolver)
104a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResult(result)
105a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setUri(ALBUMS_URI)
106a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setKeyColumn(AudioColumns.ALBUM_KEY)
107a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setTitleColumn(AudioColumns.ALBUM)
108a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setFlags(MediaItem.FLAG_BROWSABLE)
109a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .build();
110a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        queryInBackground(result, query);
111a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
112a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
113a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public void onQueryByArtist(String parentId, Result<List<MediaItem>> result) {
114a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        QueryTask query = new QueryTask.Builder()
115a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResolver(mResolver)
116a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResult(result)
117a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setUri(ARTISTS_URI)
118a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setKeyColumn(AudioColumns.ARTIST_KEY)
119a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setTitleColumn(AudioColumns.ARTIST)
120a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setFlags(MediaItem.FLAG_BROWSABLE)
121a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .build();
122a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        queryInBackground(result, query);
123a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
124a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
125a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public void onQueryByGenre(String parentId, Result<List<MediaItem>> result) {
126a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        QueryTask query = new QueryTask.Builder()
127a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResolver(mResolver)
128a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResult(result)
129a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setUri(GENRES_URI)
130a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setKeyColumn(MediaStore.Audio.Genres._ID)
131a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setTitleColumn(MediaStore.Audio.Genres.NAME)
132a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setFlags(MediaItem.FLAG_BROWSABLE)
133a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .build();
134a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        queryInBackground(result, query);
135a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
136a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
137a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private void queryInBackground(Result<List<MediaItem>> result,
138a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            AsyncTask<Void, Void, Void> task) {
139a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        result.detach();
140a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
141a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        if (mPendingTask != null) {
142a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            mPendingTask.cancel(true);
143a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        }
144a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
145a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        mPendingTask = task;
146a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        task.execute();
147a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
148a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
149a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public List<QueueItem> getQueue() {
150a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        return mQueue;
151a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
152a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
153a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public MediaMetadata getMetadata(String key) {
154a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        Cursor cursor = null;
155a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        MediaMetadata.Builder metadata = new MediaMetadata.Builder();
156a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        try {
157a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            for (Uri uri : ALL_AUDIO_URI) {
158a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                cursor = mResolver.query(uri, null, AudioColumns.TITLE_KEY + " = ?",
159a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        new String[]{ key }, null);
160a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                if (cursor != null) {
161a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    int title = cursor.getColumnIndex(AudioColumns.TITLE);
162a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    int artist = cursor.getColumnIndex(AudioColumns.ARTIST);
163a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    int album = cursor.getColumnIndex(AudioColumns.ALBUM);
164a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID);
165a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    int duration = cursor.getColumnIndex(AudioColumns.DURATION);
166a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
167a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    while (cursor.moveToNext()) {
168a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        metadata.putString(MediaMetadata.METADATA_KEY_TITLE,
169a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                cursor.getString(title));
170a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        metadata.putString(MediaMetadata.METADATA_KEY_ARTIST,
171a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                cursor.getString(artist));
172a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        metadata.putString(MediaMetadata.METADATA_KEY_ALBUM,
173a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                cursor.getString(album));
174a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        metadata.putLong(MediaMetadata.METADATA_KEY_DURATION,
175a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                cursor.getLong(duration));
176a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
177a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        String albumArt = DEFAULT_ALBUM_ART_URI;
178a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI,
179a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                cursor.getLong(albumId));
180a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        try {
181a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            InputStream dummy = mResolver.openInputStream(albumArtUri);
182a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            albumArt = albumArtUri.toString();
183a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            dummy.close();
184a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        } catch (IOException e) {
185a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            // Ignored because the albumArt is intialized correctly anyway.
186a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        }
187a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt);
188a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        break;
189a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    }
190a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                }
191a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            }
192a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        } finally {
193a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            if (cursor != null) {
194a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                cursor.close();
195a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            }
196a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        }
197a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
198a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        return metadata.build();
199a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
200a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
201a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    /**
202a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono     * Note: This clears out the queue. You should have a local copy of the queue before calling
203a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono     * this method.
204a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono     */
205a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    public void onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result) {
206a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        mQueue.clear();
207a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
208a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        QueryTask.Builder query = new QueryTask.Builder()
209a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResolver(mResolver)
210a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setResult(result);
211a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
212a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        Uri[] uri = null;
213a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        if (lastCategory.equals(LocalMediaBrowserService.GENRES_ID)) {
214a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            // Genres come from a different table and don't use the where clause from the
215a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            // usual media table so we need to have this condition.
216a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            try {
217a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                long id = Long.parseLong(parentId);
218a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                query.setUri(new Uri[] {
219a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id),
220a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) });
221a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            } catch (NumberFormatException e) {
222a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                // This should never happen.
223a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result");
224a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                result.sendResult(new ArrayList<MediaItem>());
225a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                return;
226a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            }
227a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        } else {
228a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            query.setUri(ALL_AUDIO_URI)
229a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    .setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE)
230a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    .setWhereArgs(new String[] { parentId, parentId, parentId, parentId });
231a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        }
232a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
233a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        query.setKeyColumn(AudioColumns.TITLE_KEY)
234a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setTitleColumn(AudioColumns.TITLE)
235a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setSubtitleColumn(AudioColumns.ALBUM)
236a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setFlags(MediaItem.FLAG_PLAYABLE)
237a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                .setQueue(mQueue);
238a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        queryInBackground(result, query.build());
239a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    }
240a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
241a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    // This async task is similar enough to all the others that it feels like it can be unified
242a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    // but is different enough that unifying it makes the code for both cases look really weird
243a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    // and over paramterized so at the risk of being a little more verbose, this is separated out
244a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    // in the name of understandability.
245a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono    private static class FilesystemListTask extends AsyncTask<Void, Void, Void> {
246a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        private static final String[] COLUMNS = { AudioColumns.DATA };
247a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        private Result<List<MediaItem>> mResult;
248a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        private Uri[] mUris;
249a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        private ContentResolver mResolver;
250a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
251a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        public FilesystemListTask(Result<List<MediaItem>> result, Uri[] uris,
252a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                ContentResolver resolver) {
253a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            mResult = result;
254a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            mUris = uris;
255a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            mResolver = resolver;
256a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        }
257a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
258a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        @Override
259a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono        protected Void doInBackground(Void... voids) {
260a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            Set<String> paths = new HashSet<String>();
261a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
262a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            Cursor cursor = null;
263a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono            for (Uri uri : mUris) {
264a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                try {
265a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    cursor = mResolver.query(uri, COLUMNS, null , null, null);
266a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    if (cursor != null) {
267a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
268a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
269a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        while (cursor.moveToNext()) {
270a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            // We want to de-dupe paths of each of the songs so we get just a list
271a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            // of containing directories.
272a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            String fullPath = cursor.getString(pathColumn);
273a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            int fileNameStart = fullPath.lastIndexOf(File.separator);
274a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            if (fileNameStart < 0) {
275a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                                continue;
276a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            }
277a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono
278a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            String dirPath = fullPath.substring(0, fileNameStart);
279a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                            paths.add(dirPath);
280a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        }
281a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    }
282a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                } catch (SQLiteException e) {
283a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                    Log.e(TAG, "Failed to execute query " + e);  // Stack trace is noisy.
284a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                } finally {
285f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                    if (cursor != null) {
286f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                        cursor.close();
287f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                    }
288f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                }
289f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            }
290f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono
291f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            // Take the list of deduplicated directories and put them into the results list with
292f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            // the full directory path as the key so we can match on it later.
293f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            List<MediaItem> results = new ArrayList<>();
294f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono            for (String path : paths) {
295f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                int dirNameStart = path.lastIndexOf(File.separator) + 1;
296f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                String dirName = path.substring(dirNameStart, path.length());
297f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono                MediaDescription description = new MediaDescription.Builder()
298a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        .setMediaId(path + "%")  // Used in a like query.
299a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono                        .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