Browse Source

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 4 years ago
parent
commit
490de25b05
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
  1. 11
      app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt
  2. 4
      app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt
  3. 110
      app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt
  4. 1
      app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt
  5. 25
      app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt
  6. 11
      app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt
  7. 7
      app/src/main/java/com/github/apognu/otter/utils/Models.kt
  8. 34
      app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt
  9. 9
      app/src/main/res/drawable/library.xml
  10. 14
      app/src/main/res/layout/row_radio_header.xml
  11. 6
      app/src/main/res/values-fr/strings.xml
  12. 6
      app/src/main/res/values/strings.xml
  13. 6
      app/src/main/res/values/styles.xml

11
app/src/main/java/com/github/apognu/otter/activities/LoginActivity.kt

@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity @@ -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() { @@ -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 -> {

4
app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt

@ -88,6 +88,10 @@ class MainActivity : AppCompatActivity() { @@ -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)
}

110
app/src/main/java/com/github/apognu/otter/adapters/RadiosAdapter.kt

@ -7,11 +7,11 @@ import android.view.ViewGroup @@ -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 @@ -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 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 @@ -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() {

1
app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt

@ -12,7 +12,6 @@ import android.os.Build @@ -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

25
app/src/main/java/com/github/apognu/otter/playback/RadioPlayer.kt

@ -6,6 +6,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository @@ -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 @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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))

11
app/src/main/java/com/github/apognu/otter/repositories/RadiosRepository.kt

@ -1,7 +1,6 @@ @@ -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 @@ -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)))
}
}
}
}

7
app/src/main/java/com/github/apognu/otter/utils/Models.kt

@ -3,6 +3,10 @@ package com.github.apognu.otter.utils @@ -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( @@ -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(

34
app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt

@ -0,0 +1,34 @@ @@ -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
}
}
}

9
app/src/main/res/drawable/library.xml

@ -0,0 +1,9 @@ @@ -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>

14
app/src/main/res/layout/row_radio_header.xml

@ -0,0 +1,14 @@ @@ -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>

6
app/src/main/res/values-fr/strings.xml

@ -10,6 +10,7 @@ @@ -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 @@ @@ -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>

6
app/src/main/res/values/strings.xml

@ -11,6 +11,7 @@ @@ -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 @@ @@ -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>

6
app/src/main/res/values/styles.xml

@ -93,4 +93,10 @@ @@ -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>

Loading…
Cancel
Save