Added basic management of downloads and downloaded tracks.

This commit is contained in:
Antoine POPINEAU 2020-06-13 16:09:48 +02:00
parent 2dfabf74e9
commit 00fb833cfa
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
16 changed files with 317 additions and 7 deletions

View File

@ -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" />

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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?)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -11,6 +11,6 @@
<item
android:id="@+id/track_pin"
android:title="Pin to cache" />
android:title="@string/playback_queue_download" />
</menu>

View File

@ -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"

View File

@ -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>

View File

@ -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>