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.
housekeeping/remove-warnings
Antoine POPINEAU 2020-06-21 13:36:42 +02:00
rodzic 18e981fba5
commit 490de25b05
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A78AC64694F84063
13 zmienionych plików z 201 dodań i 47 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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<Radio> 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<Radio>()
}
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 onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_radio, parent, false)
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
}
}
return ViewHolder(view, listener).also {
view.setOnClickListener(it)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RadiosAdapter.ViewHolder {
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)
}
}
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]
holder.art.visibility = View.VISIBLE
holder.name.text = radio.name
holder.description.text = radio.description
context?.let { context ->
val icon = when (radio.radio_type) {
"random" -> R.drawable.shuffle
"less-listened" -> R.drawable.sad
else -> null
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)
}
}
}
icon?.let {
holder.native = true
RowType.InstanceRadio.ordinal, RowType.UserRadio.ordinal -> {
val radio = getRadioAt(position)
holder.art.setImageDrawable(context.getDrawable(icon))
holder.art.alpha = 0.7f
holder.art.setColorFilter(context.getColor(R.color.controlForeground))
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
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() {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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<Radio, Radio
override fun onDataFetched(data: List<Radio>): List<Radio> {
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)))
}
}
}
}

Wyświetl plik

@ -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<D : Any>(val data: List<D>)
class ArtistsCache(data: List<Artist>) : CacheItem<Artist>(data)
class AlbumsCache(data: List<Album>) : CacheItem<Album>(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(

Wyświetl plik

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

Wyświetl plik

@ -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="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
</vector>

Wyświetl plik

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:padding="8dp">
<TextView
android:id="@+id/label"
style="@style/AppTheme.ListHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

Wyświetl plik

@ -10,6 +10,7 @@
<string name="login_logging_in">Connexion</string>
<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="login_error_userinfo">Nous n\'avons pas pu récupérer les informations à propos de votre utilisateur</string>
<string name="toolbar_search">Rechercher</string>
<string name="title_downloads">Téléchargements</string>
<string name="title_settings">Paramètres</string>
@ -88,6 +89,11 @@
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_instance">Instance Funkwhale</string>
<string name="radio_playback_error">Une erreur s\'est produite lors de la lecture de cette radio</string>
<string name="radio_instance_radios">Radios de l\'instance</string>
<string name="radio_user_radios">Radios des utilisateurs</string>
<string name="radio_your_content_title">Votre contenu</string>
<string name="radio_your_content_description">Une sélection de votre propre bibliothèque.</string>
<string name="radio_favorites_description">Jouez vos morceaux favoris dans une boucle allègre infinie.</string>
<string name="radio_random_title">Aléatoire</string>
<string name="radio_random_description">Choix de pistes totalement aléatoires, vous découvrirez peut-être quelque chose ?</string>
<string name="radio_less_listened_title">Moins écoutées</string>

Wyświetl plik

@ -11,6 +11,7 @@
<string name="login_logging_in">Logging in</string>
<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="login_error_userinfo">We could not retrieve information about your user</string>
<string name="toolbar_search">Search</string>
<string name="title_downloads">Downloads</string>
<string name="title_settings">Settings</string>
@ -89,6 +90,11 @@
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_instance">Funkwhale instance</string>
<string name="radio_playback_error">There was an error while trying to play this radio</string>
<string name="radio_instance_radios">Instance radios</string>
<string name="radio_user_radios">User radios</string>
<string name="radio_your_content_title">Your content</string>
<string name="radio_your_content_description">Picks from your own libraries</string>
<string name="radio_favorites_description"> Play your favorites tunes in a never-ending happiness loop.</string>
<string name="radio_random_title">Random</string>
<string name="radio_random_description">Totally random picks, maybe you\'ll discover new things?</string>
<string name="radio_less_listened_title">Less listened</string>

Wyświetl plik

@ -93,4 +93,10 @@
<item name="android:textColor">@color/controlColor</item>
</style>
<style name="AppTheme.ListHeader">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">@android:color/white</item>
</style>
</resources>