From 490de25b05fdc0b5c829cb6914074970506a9197 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Sun, 21 Jun 2020 13:36:42 +0200 Subject: [PATCH] Handle radios when logged in anonymously. On top this fix, this commit adds support for "My content" and "Favorites" instance radios (fixes #51), as well as clearly separates instance radios from user radios. Radios were a bit unusable when not logged in with an actual authorized user account, this commit fixes the following elements: * Anonymous users get a transient session cookie when starting a radio session that was not stored and forwarded on playback, meaning no radios would play; * Anonymous users do not have their own own content. Thus, only the "Random" radio makes sense in that context. This commit only display the instance radios that are relevant to your authentication status. "My content" radios needs the user ID to function properly, this commit also adds retrieving it from the /api/v1/users/users/me/ endpoint, which now may be used in the future for other purposes. --- .../apognu/otter/activities/LoginActivity.kt | 11 +- .../apognu/otter/activities/MainActivity.kt | 4 + .../apognu/otter/adapters/RadiosAdapter.kt | 110 ++++++++++++++---- .../apognu/otter/playback/PlayerService.kt | 1 - .../apognu/otter/playback/RadioPlayer.kt | 25 +++- .../otter/repositories/RadiosRepository.kt | 11 +- .../com/github/apognu/otter/utils/Models.kt | 7 +- .../com/github/apognu/otter/utils/Userinfo.kt | 34 ++++++ app/src/main/res/drawable/library.xml | 9 ++ app/src/main/res/layout/row_radio_header.xml | 14 +++ app/src/main/res/values-fr/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/styles.xml | 6 + 13 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt create mode 100644 app/src/main/res/drawable/library.xml create mode 100644 app/src/main/res/layout/row_radio_header.xml diff --git a/app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt index 7d9c5f6..2e81e50 100644 --- a/app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt +++ b/app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt @@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity import com.github.apognu.otter.R import com.github.apognu.otter.fragments.LoginDialog import com.github.apognu.otter.utils.AppContext +import com.github.apognu.otter.utils.Userinfo import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.gson.gsonDeserializerOf @@ -103,9 +104,13 @@ class LoginActivity : AppCompatActivity() { setString("access_token", result.get().token) } - dialog.dismiss() - startActivity(Intent(this@LoginActivity, MainActivity::class.java)) - finish() + Userinfo.get()?.let { + dialog.dismiss() + startActivity(Intent(this@LoginActivity, MainActivity::class.java)) + finish() + } + + throw Exception(getString(R.string.login_error_userinfo)) } is Result.Failure -> { 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 cbab4fd..280475b 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 @@ -88,6 +88,10 @@ class MainActivity : AppCompatActivity() { startService(Intent(this, PlayerService::class.java)) DownloadService.start(this, PinService::class.java) + GlobalScope.launch(IO) { + Userinfo.get() + } + now_playing_toggle.setOnClickListener { CommandBus.send(Command.ToggleState) } diff --git a/app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt index 9ece861..07661a9 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt @@ -7,11 +7,11 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.github.apognu.otter.R import com.github.apognu.otter.fragments.FunkwhaleAdapter -import com.github.apognu.otter.utils.Event -import com.github.apognu.otter.utils.EventBus -import com.github.apognu.otter.utils.Radio +import com.github.apognu.otter.utils.* import com.github.apognu.otter.views.LoadingImageView +import com.preference.PowerPreference import kotlinx.android.synthetic.main.row_radio.view.* +import kotlinx.android.synthetic.main.row_radio_header.view.* import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.collect @@ -22,43 +22,105 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis fun onClick(holder: ViewHolder, radio: Radio) } - override fun getItemCount() = data.size + enum class RowType { + Header, + InstanceRadio, + UserRadio + } + + private val instanceRadios: List by lazy { + context?.let { + return@lazy when (val username = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("actor_username")) { + "" -> listOf( + Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)) + ) + + else -> listOf( + Radio(0, "actor_content", context.getString(R.string.radio_your_content_title), context.getString(R.string.radio_your_content_description), username), + Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description)), + Radio(0, "favorites", context.getString(R.string.favorites), context.getString(R.string.radio_favorites_description)), + Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description)) + ) + } + } + + listOf() + } + + private fun getRadioAt(position: Int): Radio { + return when (getItemViewType(position)) { + RowType.InstanceRadio.ordinal -> instanceRadios[position - 1] + else -> data[position - instanceRadios.size - 2] + } + } + + override fun getItemCount() = instanceRadios.size + data.size + 2 override fun getItemId(position: Int) = data[position].id.toLong() + override fun getItemViewType(position: Int): Int { + return when { + position == 0 || position == instanceRadios.size + 1 -> RowType.Header.ordinal + position <= instanceRadios.size -> RowType.InstanceRadio.ordinal + else -> RowType.UserRadio.ordinal + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder { - val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false) + return when (viewType) { + RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> { + val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false) + + ViewHolder(view, listener).also { + view.setOnClickListener(it) + } + } - return ViewHolder(view, listener).also { - view.setOnClickListener(it) + else -> ViewHolder(LayoutInflater.from(context).inflate(R.layout.row_radio_header, parent, false), null) } } override fun onBindViewHolder(holder: RadiosAdapter.ViewHolder, position: Int) { - val radio = data[position] + when (getItemViewType(position)) { + RowType.Header.ordinal -> { + context?.let { + when (position) { + 0 -> holder.label.text = context.getString(R.string.radio_instance_radios) + instanceRadios.size + 1 -> holder.label.text = context.getString(R.string.radio_user_radios) + } + } + } - holder.art.visibility = View.VISIBLE - holder.name.text = radio.name - holder.description.text = radio.description + RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> { + val radio = getRadioAt(position) - context?.let { context -> - val icon = when (radio.radio_type) { - "random" -> R.drawable.shuffle - "less-listened" -> R.drawable.sad - else -> null - } + holder.art.visibility = View.VISIBLE + holder.name.text = radio.name + holder.description.text = radio.description + + context?.let { context -> + val icon = when (radio.radio_type) { + "actor_content" -> R.drawable.library + "favorites" -> R.drawable.favorite + "random" -> R.drawable.shuffle + "less-listened" -> R.drawable.sad + else -> null + } - icon?.let { - holder.native = true + icon?.let { + holder.native = true - holder.art.setImageDrawable(context.getDrawable(icon)) - holder.art.alpha = 0.7f - holder.art.setColorFilter(context.getColor(R.color.controlForeground)) + holder.art.setImageDrawable(context.getDrawable(icon)) + holder.art.alpha = 0.7f + holder.art.setColorFilter(context.getColor(R.color.controlForeground)) + } + } } } } - inner class ViewHolder(view: View, private val listener: OnRadioClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener { + inner class ViewHolder(view: View, private val listener: OnRadioClickListener?) : RecyclerView.ViewHolder(view), View.OnClickListener { + val label = view.label val art = view.art val name = view.name val description = view.description @@ -66,7 +128,7 @@ class RadiosAdapter(val context: Context?, private val listener: OnRadioClickLis var native = false override fun onClick(view: View?) { - listener.onClick(this, data[layoutPosition]) + listener?.onClick(this, getRadioAt(layoutPosition)) } fun spin() { 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 7be22da..f9a8090 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 @@ -12,7 +12,6 @@ import android.os.Build import android.os.IBinder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent -import com.github.apognu.otter.Otter import com.github.apognu.otter.R import com.github.apognu.otter.utils.* import com.google.android.exoplayer2.C diff --git a/app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt b/app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt index 151800d..ac823a1 100644 --- a/app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt +++ b/app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt @@ -6,6 +6,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.utils.* import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResult import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.google.gson.Gson @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.withContext -data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null) +data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null, var related_object_id: String? = null) data class RadioSession(val id: Int) data class RadioTrackBody(val session: Int) data class RadioTrack(val position: Int, val track: RadioTrackID) @@ -29,6 +30,7 @@ class RadioPlayer(val context: Context) { private var currentRadio: Radio? = null private var session: Int? = null + private var cookie: String? = null private val favoritedRepository = FavoritedRepository(context) @@ -36,8 +38,11 @@ class RadioPlayer(val context: Context) { Cache.get(context, "radio_type")?.readLine()?.let { radio_type -> Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id -> Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session -> - currentRadio = Radio(radio_id, radio_type, "", "") - session = radio_session + Cache.get(context, "radio_cookie")?.readLine()?.let { radio_cookie -> + currentRadio = Radio(radio_id, radio_type, "", "") + session = radio_session + cookie = radio_cookie + } } } } @@ -59,6 +64,7 @@ class RadioPlayer(val context: Context) { Cache.delete(context, "radio_type") Cache.delete(context, "radio_id") Cache.delete(context, "radio_session") + Cache.delete(context, "radio_cookie") } fun isActive() = currentRadio != null && session != null @@ -66,24 +72,26 @@ class RadioPlayer(val context: Context) { private suspend fun createSession() { currentRadio?.let { radio -> try { - val request = RadioSessionBody(radio.radio_type).apply { + val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply { if (radio_type == "custom") { custom_radio = radio.id } } val body = Gson().toJson(request) - val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/")) + val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/")) .authorize() .header("Content-Type", "application/json") .body(body) - .awaitObjectResult(gsonDeserializerOf(RadioSession::class.java)) + .awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java)) session = result.get().id + cookie = response.header("set-cookie").joinToString(";") Cache.set(context, "radio_type", radio.radio_type.toByteArray()) Cache.set(context, "radio_id", radio.id.toString().toByteArray()) Cache.set(context, "radio_session", session.toString().toByteArray()) + Cache.set(context, "radio_cookie", cookie.toString().toByteArray()) prepareNextTrack(true) } catch (e: Exception) { @@ -101,6 +109,11 @@ class RadioPlayer(val context: Context) { val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/")) .authorize() .header("Content-Type", "application/json") + .apply { + cookie?.let { + header("cookie", it) + } + } .body(body) .awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java)) diff --git a/app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt index 2ed9dff..c7c1342 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt @@ -1,7 +1,6 @@ package com.github.apognu.otter.repositories import android.content.Context -import com.github.apognu.otter.R import com.github.apognu.otter.utils.FunkwhaleResponse import com.github.apognu.otter.utils.Radio import com.github.apognu.otter.utils.RadiosCache @@ -19,15 +18,7 @@ class RadiosRepository(override val context: Context?) : Repository): List { return data - .map { radio -> - radio.apply { radio_type = "custom" } - } + .map { radio -> radio.apply { radio_type = "custom" } } .toMutableList() - .apply { - context?.let { context -> - add(0, Radio(0, "random", context.getString(R.string.radio_random_title), context.getString(R.string.radio_random_description))) - add(1, Radio(0, "less-listened", context.getString(R.string.radio_less_listened_title), context.getString(R.string.radio_less_listened_description))) - } - } } } \ No newline at end of file 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 73b2846..052b603 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 @@ -3,6 +3,10 @@ package com.github.apognu.otter.utils import com.google.android.exoplayer2.offline.Download import com.preference.PowerPreference +data class User( + val full_username: String +) + sealed class CacheItem(val data: List) class ArtistsCache(data: List) : CacheItem(data) class AlbumsCache(data: List) : CacheItem(data) @@ -147,7 +151,8 @@ data class Radio( val id: Int, var radio_type: String, val name: String, - val description: String + val description: String, + var related_object_id: String? = null ) data class DownloadInfo( diff --git a/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt b/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt new file mode 100644 index 0000000..823a069 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt @@ -0,0 +1,34 @@ +package com.github.apognu.otter.utils + +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult +import com.github.kittinunf.fuel.gson.gsonDeserializerOf +import com.github.kittinunf.result.Result +import com.preference.PowerPreference + +object Userinfo { + suspend fun get(): User? { + try { + val hostname = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname") + val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/") + .authorize() + .awaitObjectResponseResult(gsonDeserializerOf(User::class.java)) + + return when (result) { + is Result.Success -> { + val user = result.get() + + PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply { + setString("actor_username", user.full_username) + } + + user + } + + else -> null + } + } catch (e: Exception) { + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/library.xml b/app/src/main/res/drawable/library.xml new file mode 100644 index 0000000..3de22fe --- /dev/null +++ b/app/src/main/res/drawable/library.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/row_radio_header.xml b/app/src/main/res/layout/row_radio_header.xml new file mode 100644 index 0000000..45892fe --- /dev/null +++ b/app/src/main/res/layout/row_radio_header.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a97f192..0838dd3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,6 +10,7 @@ Connexion Cela ne semble pas être un nom d\hôte valide Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS + Nous n\'avons pas pu récupérer les informations à propos de votre utilisateur Rechercher Téléchargements Paramètres @@ -88,6 +89,11 @@ Bitrate Instance Funkwhale Une erreur s\'est produite lors de la lecture de cette radio + Radios de l\'instance + Radios des utilisateurs + Votre contenu + Une sélection de votre propre bibliothèque. + Jouez vos morceaux favoris dans une boucle allègre infinie. Aléatoire Choix de pistes totalement aléatoires, vous découvrirez peut-être quelque chose ? Moins écoutées diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca2cea3..14399e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Logging in This could not be understood as a valid URL The Funkwhale hostname should be secure through HTTPS + We could not retrieve information about your user Search Downloads Settings @@ -89,6 +90,11 @@ Bitrate Funkwhale instance There was an error while trying to play this radio + Instance radios + User radios + Your content + Picks from your own libraries + Play your favorites tunes in a never-ending happiness loop. Random Totally random picks, maybe you\'ll discover new things? Less listened diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 92bbdf2..0dac914 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -93,4 +93,10 @@ @color/controlColor + +