diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3c6e2e1..350d19b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ + diff --git a/app/src/main/java/com/github/apognu/otter/activities/DownloadsActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/DownloadsActivity.kt new file mode 100644 index 0000000..bfb71d1 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/activities/DownloadsActivity.kt @@ -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()?.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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt index 2913230..01f6f8a 100644 --- a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt +++ b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt @@ -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) } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/DownloadsAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/DownloadsAdapter.kt new file mode 100644 index 0000000..4ca065d --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/adapters/DownloadsAdapter.kt @@ -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() { + interface OnRefreshListener { + fun refresh() + } + + var downloads: MutableList = 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/playback/PinService.kt b/app/src/main/java/com/github/apognu/otter/playback/PinService.kt index 3568423..901ee17 100644 --- a/app/src/main/java/com/github/apognu/otter/playback/PinService.kt +++ b/app/src/main/java/com/github/apognu/otter/playback/PinService.kt @@ -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?): 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() } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt b/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt index 568d6dc..79eefbd 100644 --- a/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt +++ b/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt @@ -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) - - DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, null).also { - DownloadService.sendAddDownload(this@PlayerService, PinService::class.java, it, false) + 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, data).also { + sendAddDownload(this@PlayerService, PinService::class.java, it, false) } } } diff --git a/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt b/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt index b617e90..bc8aca6 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt @@ -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? = 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) : Response() class CurrentTrack(val track: Track?) : Response() + class Downloads(val cursor: DownloadCursor) : Response() } object EventBus { diff --git a/app/src/main/java/com/github/apognu/otter/utils/Models.kt b/app/src/main/java/com/github/apognu/otter/utils/Models.kt index 06e3518..5f6aabc 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Models.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Models.kt @@ -1,5 +1,6 @@ package com.github.apognu.otter.utils +import com.google.android.exoplayer2.offline.Download import com.preference.PowerPreference sealed class CacheItem(val data: List) @@ -145,4 +146,10 @@ data class Radio( var radio_type: String, val name: String, val description: String -) \ No newline at end of file +) + +data class DownloadInfo( + val id: String, + val title: String, + val artist: String, + var download: Download?) \ No newline at end of file diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..39e64d6 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/downloads.xml b/app/src/main/res/drawable/downloads.xml new file mode 100644 index 0000000..261c312 --- /dev/null +++ b/app/src/main/res/drawable/downloads.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_downloads.xml b/app/src/main/res/layout/activity_downloads.xml new file mode 100644 index 0000000..2a0ab89 --- /dev/null +++ b/app/src/main/res/layout/activity_downloads.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/row_download.xml b/app/src/main/res/layout/row_download.xml new file mode 100644 index 0000000..6aee3ea --- /dev/null +++ b/app/src/main/res/layout/row_download.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/row_track.xml b/app/src/main/res/menu/row_track.xml index 6f1c2f4..4f1f0ac 100644 --- a/app/src/main/res/menu/row_track.xml +++ b/app/src/main/res/menu/row_track.xml @@ -11,6 +11,6 @@ + android:title="@string/playback_queue_download" /> \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar.xml b/app/src/main/res/menu/toolbar.xml index 689c1d7..8677eeb 100644 --- a/app/src/main/res/menu/toolbar.xml +++ b/app/src/main/res/menu/toolbar.xml @@ -27,6 +27,12 @@ android:title="@string/only_my_music" app:showAsAction="never" /> + + Cela ne semble pas être un nom d\hôte valide Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS Rechercher + Téléchargements Paramètres Licences open source Recherchez des artistes, albums ou morceaux @@ -59,6 +60,7 @@ Retirer Ajouter à la liste de lecture Prochaine écoute + Télécharger Ajouter aux favoris Lecture / pause Piste précédente diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb6c01c..29a0ec7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ This could not be understood as a valid URL The Funkwhale hostname should be secure through HTTPS Search + Downloads Settings Open source licences Search artists, albums and tracks @@ -60,6 +61,7 @@ Remove Add to queue Play next + Download Add to favorites Toggle playback Previous track