diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fa4b1439..f0bb9d61 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,6 +32,10 @@
+
+
diff --git a/app/src/main/java/me/vanpetegem/accentor/Accentor.kt b/app/src/main/java/me/vanpetegem/accentor/Accentor.kt
index 3c5e5bd8..2a715f77 100644
--- a/app/src/main/java/me/vanpetegem/accentor/Accentor.kt
+++ b/app/src/main/java/me/vanpetegem/accentor/Accentor.kt
@@ -2,13 +2,27 @@ package me.vanpetegem.accentor
import android.app.Application
import com.github.kittinunf.fuel.core.FuelManager
+import com.google.android.exoplayer2.database.ExoDatabaseProvider
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
+import com.google.android.exoplayer2.upstream.cache.SimpleCache
+import java.io.File
class Accentor : Application() {
override fun onCreate() {
super.onCreate()
userAgent = "Accentor/${applicationContext.packageManager.getPackageInfo(packageName, 0).versionName}"
FuelManager.instance.baseHeaders = mapOf("User-Agent" to userAgent)
+
+ audioDatabaseProvider = ExoDatabaseProvider(this)
+ audioCache = SimpleCache(
+ File(cacheDir, "audio"),
+ LeastRecentlyUsedCacheEvictor(10L * 1024L * 1024L * 1024L),
+ audioDatabaseProvider
+ )
+
}
}
-lateinit var userAgent: String
\ No newline at end of file
+lateinit var userAgent: String
+lateinit var audioCache: SimpleCache
+lateinit var audioDatabaseProvider: ExoDatabaseProvider
\ No newline at end of file
diff --git a/app/src/main/java/me/vanpetegem/accentor/media/MusicService.kt b/app/src/main/java/me/vanpetegem/accentor/media/MusicService.kt
index 93b740b4..603b1704 100644
--- a/app/src/main/java/me/vanpetegem/accentor/media/MusicService.kt
+++ b/app/src/main/java/me/vanpetegem/accentor/media/MusicService.kt
@@ -8,7 +8,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.net.Uri
-import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.PowerManager
import android.os.ResultReceiver
@@ -25,27 +24,29 @@ import androidx.media.MediaBrowserServiceCompat
import com.bumptech.glide.Glide
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
-import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
+import com.google.android.exoplayer2.offline.DownloadRequest
+import com.google.android.exoplayer2.offline.DownloadService
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSink
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
-import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
-import com.google.android.exoplayer2.upstream.cache.SimpleCache
+import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory
+import com.google.android.exoplayer2.upstream.cache.CacheUtil
+import me.vanpetegem.accentor.audioCache
import me.vanpetegem.accentor.data.tracks.Track
import me.vanpetegem.accentor.ui.main.MainActivity
import me.vanpetegem.accentor.userAgent
+import me.vanpetegem.accentor.media.extensions.isCached
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
-import java.io.File
+import java.util.*
+import kotlin.collections.ArrayList
class MusicService : MediaBrowserServiceCompat() {
companion object {
@@ -71,15 +72,7 @@ class MusicService : MediaBrowserServiceCompat() {
.build()
private val exoPlayer: ExoPlayer by lazy {
- SimpleExoPlayer.Builder(this).setLoadControl(
- // TODO: This is ugly and should be done in a better way. See https://github.com/google/ExoPlayer/issues/6204
- DefaultLoadControl.Builder().setBufferDurationsMs(
- Int.MAX_VALUE,
- Int.MAX_VALUE,
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
- ).createDefaultLoadControl()
- ).build().apply {
+ SimpleExoPlayer.Builder(this).build().apply {
setAudioAttributes(accentorAudioAttributes, true)
}
}
@@ -91,7 +84,12 @@ class MusicService : MediaBrowserServiceCompat() {
setSessionActivity(packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent ->
sessionIntent.flags = sessionIntent.flags or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
sessionIntent.putExtra(MainActivity.INTENT_EXTRA_OPEN_PLAYER, true)
- PendingIntent.getActivity(this@MusicService, 0, sessionIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ PendingIntent.getActivity(
+ this@MusicService,
+ 0,
+ sessionIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
})
isActive = true
}
@@ -112,32 +110,28 @@ class MusicService : MediaBrowserServiceCompat() {
it.setQueueEditor(
object : MediaSessionConnector.QueueEditor {
var count: Long = 0
- val factory =
- ProgressiveMediaSource.Factory(object : DataSource.Factory {
- val base = DefaultDataSourceFactory(
- this@MusicService.application,
- DefaultHttpDataSourceFactory(userAgent, 0, 0, false)
- )
+ val dataSourceFactory: DataSource.Factory = object : DataSource.Factory {
+ val base = DefaultDataSourceFactory(
+ this@MusicService.application,
+ DefaultHttpDataSourceFactory(userAgent, 0, 0, false)
+ )
- val cache = SimpleCache(
- File(this@MusicService.application.cacheDir, "audio"),
- LeastRecentlyUsedCacheEvictor(10L * 1024L * 1024L * 1024L),
- ExoDatabaseProvider(this@MusicService.application)
+ override fun createDataSource(): DataSource {
+ return CacheDataSource(
+ audioCache,
+ base.createDataSource(),
+ FileDataSource(),
+ null,
+ (CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR),
+ null
)
+ }
+ }
- override fun createDataSource(): DataSource {
- return CacheDataSource(
- cache,
- base.createDataSource(),
- FileDataSource(),
- CacheDataSink(cache, C.LENGTH_UNSET.toLong()),
- (CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR),
- null
- )
- }
- }, DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true))
-
- override fun onRemoveQueueItem(player: Player, description: MediaDescriptionCompat) {
+ override fun onRemoveQueueItem(
+ player: Player,
+ description: MediaDescriptionCompat
+ ) {
for (i in 0..queue.size) {
if (queue[i].description.mediaId == description.mediaId) {
queue.removeAt(i)
@@ -148,12 +142,24 @@ class MusicService : MediaBrowserServiceCompat() {
}
}
- override fun onAddQueueItem(player: Player, description: MediaDescriptionCompat) {
+ override fun onAddQueueItem(
+ player: Player,
+ description: MediaDescriptionCompat
+ ) {
onAddQueueItem(player, description, queue.size)
}
- override fun onAddQueueItem(player: Player, description: MediaDescriptionCompat, index: Int) {
- mediaSource.addMediaSource(index, factory.createMediaSource(description.mediaUri))
+ override fun onAddQueueItem(
+ player: Player,
+ description: MediaDescriptionCompat,
+ index: Int
+ ) {
+ mediaSource.addMediaSource(
+ index,
+ ProgressiveMediaSource.Factory(dataSourceFactory)
+ .setCustomCacheKey(description.mediaId)
+ .createMediaSource(description.mediaUri)
+ )
queue.add(
index,
MediaSessionCompat.QueueItem(description, count++)
@@ -214,18 +220,29 @@ class MusicService : MediaBrowserServiceCompat() {
val extras = item.extras!!
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, item.mediaId)
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.title.toString())
- builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, item.subtitle.toString())
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_ALBUM,
+ item.subtitle.toString()
+ )
builder.putString(
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
extras.getString(Track.ALBUMARTIST)
)
- builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, extras.getString(Track.ARTIST))
- builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, extras.getString(Track.YEAR))
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_ARTIST,
+ extras.getString(Track.ARTIST)
+ )
+ builder.putString(
+ MediaMetadataCompat.METADATA_KEY_DATE,
+ extras.getString(Track.YEAR)
+ )
if (item.iconUri != null) {
try {
builder.putBitmap(
MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
- Glide.with(this@MusicService).load(item.iconUri).onlyRetrieveFromCache(true).submit().get().toBitmap()
+ Glide.with(this@MusicService).load(item.iconUri).onlyRetrieveFromCache(
+ true
+ ).submit().get().toBitmap()
)
} catch (e: Exception) {
doAsync {
@@ -235,8 +252,6 @@ class MusicService : MediaBrowserServiceCompat() {
}
}
}
- } else {
-
}
}
builder.build()
@@ -258,39 +273,92 @@ class MusicService : MediaBrowserServiceCompat() {
super.onDestroy()
}
- override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? =
+ override fun onGetRoot(
+ clientPackageName: String,
+ clientUid: Int,
+ rootHints: Bundle?
+ ): BrowserRoot? =
BrowserRoot("empty", null)
- override fun onLoadChildren(parentId: String, result: Result>) =
+ override fun onLoadChildren(
+ parentId: String,
+ result: Result>
+ ) =
result.sendResult(null)
private fun removeNotification() = notificationManager.cancel(NOW_PLAYING_NOTIFICATION)
private inner class MediaControllerCallback : MediaControllerCompat.Callback() {
- val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
- val wifiLock: WifiManager.WifiLock =
- wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Accentor:WifiLock")
val wakeLock: PowerManager.WakeLock =
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Accentor:WakeLock")
+ var activeQueueItemId: Long? = null
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
mediaController.playbackState?.let { updateNotification(it) }
}
+ override fun onQueueChanged(queue: MutableList?) {
+ resetDownloader()
+ }
+
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
state?.let { updateNotification(it) }
state?.let { updateLocks(it) }
+ state?.let {
+ if (activeQueueItemId != it.activeQueueItemId) {
+ resetDownloader()
+ activeQueueItemId = it.activeQueueItemId
+ }
+ }
+ }
+
+ private fun resetDownloader() {
+ DownloadService.sendRemoveAllDownloads(
+ this@MusicService,
+ PlayQueueDownloadService::class.java,
+ true
+ )
+ mediaController.queue ?: return
+ var currentPos = mediaController.queue.indexOfFirst { it.queueId == activeQueueItemId }
+ if (currentPos == -1) {
+ currentPos = 0
+ }
+ for (i in currentPos until mediaController.queue.size) {
+ addDownload(mediaController.queue[i].description)
+ }
+ for (i in 0 until currentPos) {
+ addDownload(mediaController.queue[i].description)
+ }
+ }
+
+ private fun addDownload(description: MediaDescriptionCompat) {
+ if (!isCached(description.mediaId!!)) {
+ DownloadService.sendAddDownload(
+ this@MusicService,
+ PlayQueueDownloadService::class.java,
+ DownloadRequest(
+ description.mediaId!!,
+ DownloadRequest.TYPE_PROGRESSIVE,
+ description.mediaUri!!,
+ Collections.emptyList(),
+ description.mediaId,
+ null
+ ),
+ true
+ )
+ }
}
private fun updateNotification(state: PlaybackStateCompat) {
val updatedState = state.state
- val notification = if (mediaController.metadata != null && updatedState != PlaybackStateCompat.STATE_NONE)
- notificationBuilder.buildNotification(mediaSession.sessionToken)
- else
- null
+ val notification =
+ if (mediaController.metadata != null && updatedState != PlaybackStateCompat.STATE_NONE)
+ notificationBuilder.buildNotification(mediaSession.sessionToken)
+ else
+ null
when (updatedState) {
PlaybackStateCompat.STATE_BUFFERING, PlaybackStateCompat.STATE_PLAYING -> {
@@ -332,15 +400,12 @@ class MusicService : MediaBrowserServiceCompat() {
@SuppressLint("WakelockTimeout", "Wakelock")
private fun updateLocks(state: PlaybackStateCompat) {
-
when (state.state) {
PlaybackStateCompat.STATE_BUFFERING, PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.STATE_CONNECTING -> {
if (!wakeLock.isHeld) wakeLock.acquire()
- if (!wifiLock.isHeld) wifiLock.acquire()
}
else -> {
if (wakeLock.isHeld) wakeLock.release()
- if (wifiLock.isHeld) wifiLock.release()
}
}
}
diff --git a/app/src/main/java/me/vanpetegem/accentor/media/PlayQueueDownloadService.kt b/app/src/main/java/me/vanpetegem/accentor/media/PlayQueueDownloadService.kt
new file mode 100644
index 00000000..dec386cf
--- /dev/null
+++ b/app/src/main/java/me/vanpetegem/accentor/media/PlayQueueDownloadService.kt
@@ -0,0 +1,55 @@
+package me.vanpetegem.accentor.media
+
+import android.app.Notification
+import android.content.Intent
+import android.support.v4.media.session.MediaSessionCompat
+import androidx.core.app.NotificationCompat
+import com.google.android.exoplayer2.offline.Download
+import com.google.android.exoplayer2.offline.DownloadManager
+import com.google.android.exoplayer2.offline.DownloadService
+import com.google.android.exoplayer2.scheduler.Scheduler
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
+import me.vanpetegem.accentor.R
+import me.vanpetegem.accentor.audioCache
+import me.vanpetegem.accentor.audioDatabaseProvider
+import me.vanpetegem.accentor.userAgent
+
+const val PLAY_QUEUE_DOWNLOAD_NOTIFICATION: Int = 0xb440
+const val PLAY_QUEUE_DOWNLOAD_CHANNEL: String =
+ "me.vanpetegem.accentor.media.PLAY_QUEUE_DOWNLOAD_CHANNEL"
+
+
+class PlayQueueDownloadService : DownloadService(
+ PLAY_QUEUE_DOWNLOAD_NOTIFICATION,
+ DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
+ PLAY_QUEUE_DOWNLOAD_CHANNEL,
+ R.string.play_queue_download_notification_channel_name,
+ R.string.play_queue_download_notification_channel_description
+) {
+
+ override fun getDownloadManager() = DownloadManager(
+ applicationContext,
+ audioDatabaseProvider,
+ audioCache,
+ DefaultHttpDataSourceFactory(userAgent, 0, 0, false)
+ ).apply {
+ maxParallelDownloads = 1
+ }
+
+ override fun getForegroundNotification(downloads: MutableList): Notification =
+ NotificationCompat.Builder(
+ this,
+ PLAY_QUEUE_DOWNLOAD_CHANNEL
+ )
+ .setSmallIcon(R.drawable.ic_download)
+ .setContentTitle(this.getString(R.string.downloading_play_queue))
+ .setContentText(
+ resources.getQuantityString(
+ R.plurals.downloading_n_tracks, downloads.size, downloads.size
+ )
+ )
+ .build()
+
+ override fun getScheduler(): Scheduler? = null
+
+}
diff --git a/app/src/main/java/me/vanpetegem/accentor/media/extensions/CacheUtilExt.kt b/app/src/main/java/me/vanpetegem/accentor/media/extensions/CacheUtilExt.kt
new file mode 100644
index 00000000..5a04972f
--- /dev/null
+++ b/app/src/main/java/me/vanpetegem/accentor/media/extensions/CacheUtilExt.kt
@@ -0,0 +1,16 @@
+package me.vanpetegem.accentor.media.extensions
+
+import android.util.Log
+import com.google.android.exoplayer2.C
+import com.google.android.exoplayer2.upstream.cache.ContentMetadata.KEY_CONTENT_LENGTH
+import me.vanpetegem.accentor.audioCache
+
+fun isCached(mediaId: String): Boolean {
+ val length =
+ audioCache.getContentMetadata(mediaId).get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET.toLong())
+ Log.d("CACHE_UTIL", "$mediaId length: $length")
+ if (length == C.LENGTH_UNSET.toLong()) return false
+ Log.d("CACHE_UTIL", "$mediaId cached: ${audioCache.getCachedLength(mediaId, 0, length)}")
+ return length == audioCache.getCachedLength(mediaId, 0, length)
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml
new file mode 100644
index 00000000..9273a74d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_download.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6782a051..9d91b804 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,4 +32,11 @@
Play
Next
Shuffle
+ Play queue downloads
+ Shows when accentor is downloading tracks for the current play queue
+ Downloading play queue
+
+ - Downloading %d track
+ - Downloading %d tracks
+