Added basic management of downloads and downloaded tracks.
This commit is contained in:
parent
2dfabf74e9
commit
00fb833cfa
|
@ -40,6 +40,7 @@
|
|||
<activity
|
||||
android:name="com.github.apognu.otter.activities.SearchActivity"
|
||||
android:launchMode="singleTop" />
|
||||
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
|
||||
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
|
||||
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package com.github.apognu.otter.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.DownloadsAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.activity_downloads.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DownloadsActivity : AppCompatActivity() {
|
||||
lateinit var adapter: DownloadsAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_downloads)
|
||||
|
||||
adapter = DownloadsAdapter(this, RefreshListener()).also {
|
||||
downloads.layoutManager = LinearLayoutManager(this)
|
||||
downloads.adapter = it
|
||||
}
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
while (true) {
|
||||
refresh()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
GlobalScope.launch(Main) {
|
||||
RequestBus.send(Request.GetDownloads).wait<Response.Downloads>()?.let { response ->
|
||||
adapter.downloads.clear()
|
||||
|
||||
while (response.cursor.moveToNext()) {
|
||||
val download = response.cursor.download
|
||||
|
||||
Gson().fromJson(String(download.request.data), DownloadInfo::class.java)?.let { info ->
|
||||
adapter.downloads.add(info.apply {
|
||||
this.download = download
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class RefreshListener : DownloadsAdapter.OnRefreshListener {
|
||||
override fun refresh() {
|
||||
this@DownloadsActivity.refresh()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import com.github.apognu.otter.utils.*
|
|||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.gson.Gson
|
||||
import com.preference.PowerPreference
|
||||
import com.squareup.picasso.Picasso
|
||||
|
@ -78,6 +79,7 @@ class MainActivity : AppCompatActivity() {
|
|||
super.onResume()
|
||||
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
DownloadService.start(this, PinService::class.java)
|
||||
|
||||
now_playing_toggle.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
|
@ -163,6 +165,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
EventBus.send(Event.ListingsChanged)
|
||||
}
|
||||
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
|
||||
R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.github.apognu.otter.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.playback.PinService
|
||||
import com.github.apognu.otter.utils.DownloadInfo
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import kotlinx.android.synthetic.main.row_download.view.*
|
||||
|
||||
class DownloadsAdapter(private val context: Context, private val listener: OnRefreshListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
|
||||
interface OnRefreshListener {
|
||||
fun refresh()
|
||||
}
|
||||
|
||||
var downloads: MutableList<DownloadInfo> = mutableListOf()
|
||||
|
||||
override fun getItemCount() = downloads.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_download, parent, false)
|
||||
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val download = downloads[position]
|
||||
|
||||
holder.title.text = download.title
|
||||
holder.artist.text = download.artist
|
||||
|
||||
download.download?.let { state ->
|
||||
when (state.isTerminalState) {
|
||||
true -> {
|
||||
holder.progress.visibility = View.GONE
|
||||
holder.toggle.visibility = View.GONE
|
||||
}
|
||||
|
||||
false -> {
|
||||
holder.progress.visibility = View.VISIBLE
|
||||
holder.toggle.visibility = View.VISIBLE
|
||||
holder.progress.progress = state.percentDownloaded.toInt()
|
||||
|
||||
when (state.state) {
|
||||
Download.STATE_REMOVING -> {
|
||||
holder.progress.visibility = View.GONE
|
||||
holder.toggle.visibility = View.GONE
|
||||
}
|
||||
|
||||
Download.STATE_STOPPED -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.play))
|
||||
else -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.pause))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.toggle.setOnClickListener {
|
||||
if (state.state == Download.STATE_DOWNLOADING) {
|
||||
DownloadService.sendSetStopReason(context, PinService::class.java, download.id, 1, false)
|
||||
} else {
|
||||
DownloadService.sendSetStopReason(context, PinService::class.java, download.id, Download.STOP_REASON_NONE, false)
|
||||
}
|
||||
|
||||
listener.refresh()
|
||||
}
|
||||
|
||||
holder.delete.setOnClickListener {
|
||||
DownloadService.sendRemoveDownload(context, PinService::class.java, download.id, false)
|
||||
|
||||
listener.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val title = view.title
|
||||
val artist = view.artist
|
||||
val progress = view.progress
|
||||
val toggle = view.toggle
|
||||
val delete = view.delete
|
||||
}
|
||||
}
|
|
@ -1,12 +1,20 @@
|
|||
package com.github.apognu.otter.playback
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Request
|
||||
import com.github.apognu.otter.utils.RequestBus
|
||||
import com.github.apognu.otter.utils.Response
|
||||
import com.google.android.exoplayer2.offline.*
|
||||
import com.google.android.exoplayer2.scheduler.Scheduler
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||
private val manager by lazy {
|
||||
|
@ -17,13 +25,27 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
|||
DownloadManager(this, DefaultDownloadIndex(database), DefaultDownloaderFactory(helper))
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
buildResumeDownloadsIntent(this, PinService::class.java, true)
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
RequestBus.get().collect { request ->
|
||||
when (request) {
|
||||
is Request.GetDownloads -> request.channel?.offer(Response.Downloads(getDownloads()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun getDownloadManager() = manager
|
||||
|
||||
override fun getScheduler(): Scheduler? = null
|
||||
|
||||
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
|
||||
return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.ottershape, null, null, downloads)
|
||||
return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.downloads, null, "Hello, world", downloads)
|
||||
}
|
||||
|
||||
fun getDownloads() = manager.downloadIndex.getDownloads()
|
||||
private fun getDownloads() = manager.downloadIndex.getDownloads()
|
||||
}
|
|
@ -19,10 +19,10 @@ import com.github.apognu.otter.utils.*
|
|||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.android.exoplayer2.offline.DownloadService.sendAddDownload
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -196,9 +196,17 @@ class PlayerService : Service() {
|
|||
is Command.PinTrack -> {
|
||||
message.track.bestUpload()?.let { upload ->
|
||||
val url = mustNormalizeUrl(upload.listen_url)
|
||||
val data = Gson().toJson(
|
||||
DownloadInfo(
|
||||
url,
|
||||
message.track.title,
|
||||
message.track.artist.name,
|
||||
null
|
||||
)
|
||||
).toByteArray()
|
||||
|
||||
DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, null).also {
|
||||
DownloadService.sendAddDownload(this@PlayerService, PinService::class.java, it, false)
|
||||
DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, data).also {
|
||||
sendAddDownload(this@PlayerService, PinService::class.java, it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.apognu.otter.utils
|
||||
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -51,12 +52,14 @@ sealed class Request(var channel: Channel<Response>? = null) {
|
|||
object GetState : Request()
|
||||
object GetQueue : Request()
|
||||
object GetCurrentTrack : Request()
|
||||
object GetDownloads : Request()
|
||||
}
|
||||
|
||||
sealed class Response {
|
||||
class State(val playing: Boolean) : Response()
|
||||
class Queue(val queue: List<Track>) : Response()
|
||||
class CurrentTrack(val track: Track?) : Response()
|
||||
class Downloads(val cursor: DownloadCursor) : Response()
|
||||
}
|
||||
|
||||
object EventBus {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.apognu.otter.utils
|
||||
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.preference.PowerPreference
|
||||
|
||||
sealed class CacheItem<D : Any>(val data: List<D>)
|
||||
|
@ -145,4 +146,10 @@ data class Radio(
|
|||
var radio_type: String,
|
||||
val name: String,
|
||||
val description: String
|
||||
)
|
||||
)
|
||||
|
||||
data class DownloadInfo(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
var download: Download?)
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
|
||||
</vector>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/surface"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/AppTheme.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/title_downloads" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloads"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_download" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
tools:showIn="@layout/activity_downloads">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/AppTheme.ItemTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Track title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Artist name" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/toggle"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/pause" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/delete"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/delete"
|
||||
android:tint="@color/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/track_pin"
|
||||
android:title="Pin to cache" />
|
||||
android:title="@string/playback_queue_download" />
|
||||
|
||||
</menu>
|
|
@ -27,6 +27,12 @@
|
|||
android:title="@string/only_my_music"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_downloads"
|
||||
android:icon="@drawable/downloads"
|
||||
android:title="@string/title_downloads"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/settings"
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<string name="login_error_hostname">Cela ne semble pas être un nom d\hôte valide</string>
|
||||
<string name="login_error_hostname_https">Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS</string>
|
||||
<string name="toolbar_search">Rechercher</string>
|
||||
<string name="title_downloads">Téléchargements</string>
|
||||
<string name="title_settings">Paramètres</string>
|
||||
<string name="title_oss_licences">Licences open source</string>
|
||||
<string name="search_placeholder">Recherchez des artistes, albums ou morceaux</string>
|
||||
|
@ -59,6 +60,7 @@
|
|||
<string name="playback_queue_remove_item">Retirer</string>
|
||||
<string name="playback_queue_add_item">Ajouter à la liste de lecture</string>
|
||||
<string name="playback_queue_play_next">Prochaine écoute</string>
|
||||
<string name="playback_queue_download">Télécharger</string>
|
||||
<string name="manage_add_to_favorites">Ajouter aux favoris</string>
|
||||
<string name="control_toggle">Lecture / pause</string>
|
||||
<string name="control_previous">Piste précédente</string>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="login_error_hostname">This could not be understood as a valid URL</string>
|
||||
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
|
||||
<string name="toolbar_search">Search</string>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="title_oss_licences">Open source licences</string>
|
||||
<string name="search_placeholder">Search artists, albums and tracks</string>
|
||||
|
@ -60,6 +61,7 @@
|
|||
<string name="playback_queue_remove_item">Remove</string>
|
||||
<string name="playback_queue_add_item">Add to queue</string>
|
||||
<string name="playback_queue_play_next">Play next</string>
|
||||
<string name="playback_queue_download">Download</string>
|
||||
<string name="manage_add_to_favorites">Add to favorites</string>
|
||||
<string name="control_toggle">Toggle playback</string>
|
||||
<string name="control_previous">Previous track</string>
|
||||
|
|
Loading…
Reference in New Issue