-
Notifications
You must be signed in to change notification settings - Fork 3.7k
/
MusicSource.kt
211 lines (193 loc) · 7.69 KB
/
MusicSource.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.uamp.media.library
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.IntDef
import com.example.android.uamp.media.MusicService
import com.example.android.uamp.media.extensions.album
import com.example.android.uamp.media.extensions.albumArtist
import com.example.android.uamp.media.extensions.artist
import com.example.android.uamp.media.extensions.containsCaseInsensitive
import com.example.android.uamp.media.extensions.genre
import com.example.android.uamp.media.extensions.title
/**
* Interface used by [MusicService] for looking up [MediaMetadataCompat] objects.
*
* Because Kotlin provides methods such as [Iterable.find] and [Iterable.filter],
* this is a convenient interface to have on sources.
*/
interface MusicSource : Iterable<MediaMetadataCompat> {
/**
* Begins loading the data for this music source.
*/
suspend fun load()
/**
* Method which will perform a given action after this [MusicSource] is ready to be used.
*
* @param performAction A lambda expression to be called with a boolean parameter when
* the source is ready. `true` indicates the source was successfully prepared, `false`
* indicates an error occurred.
*/
fun whenReady(performAction: (Boolean) -> Unit): Boolean
fun search(query: String, extras: Bundle): List<MediaMetadataCompat>
}
@IntDef(
STATE_CREATED,
STATE_INITIALIZING,
STATE_INITIALIZED,
STATE_ERROR
)
@Retention(AnnotationRetention.SOURCE)
annotation class State
/**
* State indicating the source was created, but no initialization has performed.
*/
const val STATE_CREATED = 1
/**
* State indicating initialization of the source is in progress.
*/
const val STATE_INITIALIZING = 2
/**
* State indicating the source has been initialized and is ready to be used.
*/
const val STATE_INITIALIZED = 3
/**
* State indicating an error has occurred.
*/
const val STATE_ERROR = 4
/**
* Base class for music sources in UAMP.
*/
abstract class AbstractMusicSource : MusicSource {
@State
var state: Int = STATE_CREATED
set(value) {
if (value == STATE_INITIALIZED || value == STATE_ERROR) {
synchronized(onReadyListeners) {
field = value
onReadyListeners.forEach { listener ->
listener(state == STATE_INITIALIZED)
}
}
} else {
field = value
}
}
private val onReadyListeners = mutableListOf<(Boolean) -> Unit>()
/**
* Performs an action when this MusicSource is ready.
*
* This method is *not* threadsafe. Ensure actions and state changes are only performed
* on a single thread.
*/
override fun whenReady(performAction: (Boolean) -> Unit): Boolean =
when (state) {
STATE_CREATED, STATE_INITIALIZING -> {
onReadyListeners += performAction
false
}
else -> {
performAction(state != STATE_ERROR)
true
}
}
/**
* Handles searching a [MusicSource] from a focused voice search, often coming
* from the Google Assistant.
*/
override fun search(query: String, extras: Bundle): List<MediaMetadataCompat> {
// First attempt to search with the "focus" that's provided in the extras.
val focusSearchResult = when (extras[MediaStore.EXTRA_MEDIA_FOCUS]) {
MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> {
// For a Genre focused search, only genre is set.
val genre = extras[EXTRA_MEDIA_GENRE]
Log.d(TAG, "Focused genre search: '$genre'")
filter { song ->
song.genre == genre
}
}
MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> {
// For an Artist focused search, only the artist is set.
val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]
Log.d(TAG, "Focused artist search: '$artist'")
filter { song ->
(song.artist == artist || song.albumArtist == artist)
}
}
MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE -> {
// For an Album focused search, album and artist are set.
val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]
val album = extras[MediaStore.EXTRA_MEDIA_ALBUM]
Log.d(TAG, "Focused album search: album='$album' artist='$artist")
filter { song ->
(song.artist == artist || song.albumArtist == artist) && song.album == album
}
}
MediaStore.Audio.Media.ENTRY_CONTENT_TYPE -> {
// For a Song (aka Media) focused search, title, album, and artist are set.
val title = extras[MediaStore.EXTRA_MEDIA_TITLE]
val album = extras[MediaStore.EXTRA_MEDIA_ALBUM]
val artist = extras[MediaStore.EXTRA_MEDIA_ARTIST]
Log.d(TAG, "Focused media search: title='$title' album='$album' artist='$artist")
filter { song ->
(song.artist == artist || song.albumArtist == artist) && song.album == album
&& song.title == title
}
}
else -> {
// There isn't a focus, so no results yet.
emptyList()
}
}
// If there weren't any results from the focused search (or if there wasn't a focus
// to begin with), try to find any matches given the 'query' provided, searching against
// a few of the fields.
// In this sample, we're just checking a few fields with the provided query, but in a
// more complex app, more logic could be used to find fuzzy matches, etc...
if (focusSearchResult.isEmpty()) {
return if (query.isNotBlank()) {
Log.d(TAG, "Unfocused search for '$query'")
filter { song ->
song.title.containsCaseInsensitive(query)
|| song.genre.containsCaseInsensitive(query)
}
} else {
// If the user asked to "play music", or something similar, the query will also
// be blank. Given the small catalog of songs in the sample, just return them
// all, shuffled, as something to play.
Log.d(TAG, "Unfocused search without keyword")
return shuffled()
}
} else {
return focusSearchResult
}
}
/**
* [MediaStore.EXTRA_MEDIA_GENRE] is missing on API 19. Hide this fact by using our
* own version of it.
*/
private val EXTRA_MEDIA_GENRE
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaStore.EXTRA_MEDIA_GENRE
} else {
"android.intent.extra.genre"
}
}
private const val TAG = "MusicSource"