Allow for anonymous connection if server supports it. Should provide basic support for #14.

This commit is contained in:
Antoine POPINEAU 2019-11-25 23:14:16 +01:00
parent 3101fa5302
commit aad0ec439c
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
11 changed files with 160 additions and 75 deletions

View File

@ -3,6 +3,7 @@ package com.github.apognu.otter.activities
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.LoginDialog import com.github.apognu.otter.fragments.LoginDialog
@ -30,6 +31,16 @@ class LoginActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
anonymous?.setOnCheckedChangeListener { _, isChecked ->
val state = when (isChecked) {
true -> View.GONE
false -> View.VISIBLE
}
username_field.visibility = state
password_field.visibility = state
}
login?.setOnClickListener { login?.setOnClickListener {
var hostname = hostname.text.toString().trim() var hostname = hostname.text.toString().trim()
val username = username.text.toString() val username = username.text.toString()
@ -55,58 +66,106 @@ class LoginActivity : AppCompatActivity() {
hostname_field.error = "" hostname_field.error = ""
val body = mapOf( when (anonymous.isChecked) {
"username" to username, false -> authedLogin(hostname, username, password)
"password" to password true -> anonymousLogin(hostname)
).toList()
val dialog = LoginDialog().apply {
show(supportFragmentManager, "LoginDialog")
} }
}
}
GlobalScope.launch(Main) { private fun authedLogin(hostname: String, username: String, password: String) {
try { val body = mapOf(
val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body) "username" to username,
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java)) "password" to password
).toList()
when (result) { val dialog = LoginDialog().apply {
is Result.Success -> { show(supportFragmentManager, "LoginDialog")
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply { }
setString("hostname", hostname)
setString("username", username)
setString("password", password)
setString("access_token", result.get().token)
}
dialog.dismiss() GlobalScope.launch(Main) {
startActivity(Intent(this@LoginActivity, MainActivity::class.java)) try {
finish() val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body)
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
when (result) {
is Result.Success -> {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
setString("hostname", hostname)
setBoolean("anonymous", false)
setString("username", username)
setString("password", password)
setString("access_token", result.get().token)
} }
is Result.Failure -> { dialog.dismiss()
dialog.dismiss() startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
}
val error = Gson().fromJson(String(response.data), FwCredentials::class.java) is Result.Failure -> {
dialog.dismiss()
hostname_field.error = null val error = Gson().fromJson(String(response.data), FwCredentials::class.java)
username_field.error = null
if (error != null && error.non_field_errors.isNotEmpty()) { hostname_field.error = null
username_field.error = error.non_field_errors[0] username_field.error = null
} else {
hostname_field.error = result.error.localizedMessage if (error != null && error.non_field_errors.isNotEmpty()) {
} username_field.error = error.non_field_errors[0]
} else {
hostname_field.error = result.error.localizedMessage
} }
} }
} catch (e: Exception) {
dialog.dismiss()
val message =
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message
hostname_field.error = message
} }
} catch (e: Exception) {
dialog.dismiss()
val message =
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message
hostname_field.error = message
}
}
}
private fun anonymousLogin(hostname: String) {
val dialog = LoginDialog().apply {
show(supportFragmentManager, "LoginDialog")
}
GlobalScope.launch(Main) {
try {
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
when (result) {
is Result.Success -> {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
setString("hostname", hostname)
setBoolean("anonymous", true)
}
dialog.dismiss()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
}
is Result.Failure -> {
dialog.dismiss()
hostname_field.error = result.error.localizedMessage
}
}
} catch (e: Exception) {
dialog.dismiss()
val message =
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message
hostname_field.error = message
} }
} }
} }

View File

@ -5,13 +5,14 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Settings
class SplashActivity : AppCompatActivity() { class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE).apply { getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE).apply {
when (contains("access_token")) { when (Settings.hasAccessToken() || Settings.isAnonymous()) {
true -> Intent(this@SplashActivity, MainActivity::class.java).apply { true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION flags = Intent.FLAG_ACTIVITY_NO_ANIMATION

View File

@ -10,7 +10,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.repositories.HttpUpstream import com.github.apognu.otter.repositories.HttpUpstream
import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.repositories.Repository
import com.github.apognu.otter.utils.Cache import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.untilNetwork import com.github.apognu.otter.utils.untilNetwork
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.android.synthetic.main.fragment_artists.* import kotlinx.android.synthetic.main.fragment_artists.*

View File

@ -57,11 +57,11 @@ class QueueManager(val context: Context) {
} }
private fun factory(): CacheDataSourceFactory { private fun factory(): CacheDataSourceFactory {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
val http = DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.getString(R.string.app_name))).apply { val http = DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.getString(R.string.app_name))).apply {
defaultRequestProperties.apply { defaultRequestProperties.apply {
set("Authorization", "Bearer $token") if (!Settings.isAnonymous()) {
set("Authorization", "Bearer ${Settings.getAccessToken()}")
}
} }
} }

View File

@ -7,7 +7,6 @@ import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,13 +25,16 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
} }
fun addFavorite(id: Int) { fun addFavorite(id: Int) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
val body = mapOf("track" to id) val body = mapOf("track" to id)
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
Fuel request
.post(mustNormalizeUrl("/api/v1/favorites/tracks/"))
.header("Authorization", "Bearer $token")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Gson().toJson(body)) .body(Gson().toJson(body))
.awaitByteArrayResponseResult() .awaitByteArrayResponseResult()
@ -40,13 +42,16 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
} }
fun deleteFavorite(id: Int) { fun deleteFavorite(id: Int) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
val body = mapOf("track" to id) val body = mapOf("track" to id)
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
if (!Settings.isAnonymous()) {
request.header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
Fuel request
.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/"))
.header("Authorization", "Bearer $token")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Gson().toJson(body)) .body(Gson().toJson(body))
.awaitByteArrayResponseResult() .awaitByteArrayResponseResult()

View File

@ -9,7 +9,6 @@ import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.github.kittinunf.result.Result import com.github.kittinunf.result.Result
import com.google.gson.Gson import com.google.gson.Gson
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -37,6 +36,8 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
.build() .build()
.toString() .toString()
log(offsetUrl)
get(offsetUrl).fold( get(offsetUrl).fold(
{ response -> { response ->
val data = response.getData() val data = response.getData()
@ -65,12 +66,13 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
} }
suspend fun get(url: String): Result<R, FuelError> { suspend fun get(url: String): Result<R, FuelError> {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token") val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.isAnonymous()}")
}
}
val (_, response, result) = Fuel val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResponseResult(GenericDeserializer<R>(type))
if (response.statusCode == 401) { if (response.statusCode == 401) {
return retryGet(url) return retryGet(url)
@ -81,12 +83,13 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
private suspend fun retryGet(url: String): Result<R, FuelError> { private suspend fun retryGet(url: String): Result<R, FuelError> {
return if (HTTP.refresh()) { return if (HTTP.refresh()) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token") val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.isAnonymous()}")
}
}
Fuel request.awaitObjectResult(GenericDeserializer(type))
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResult(GenericDeserializer(type))
} else { } else {
Result.Failure(FuelError.wrap(RefreshError)) Result.Failure(FuelError.wrap(RefreshError))
} }

View File

@ -36,12 +36,13 @@ object HTTP {
} }
suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> { suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token") val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
val (_, response, result) = Fuel val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
if (response.statusCode == 401) { if (response.statusCode == 401) {
return retryGet(url) return retryGet(url)
@ -52,12 +53,13 @@ object HTTP {
suspend inline fun <reified T : Any> retryGet(url: String): Result<T, FuelError> { suspend inline fun <reified T : Any> retryGet(url: String): Result<T, FuelError> {
return if (refresh()) { return if (refresh()) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token") val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
Fuel request.awaitObjectResult(gsonDeserializerOf(T::class.java))
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResult(gsonDeserializerOf(T::class.java))
} else { } else {
Result.Failure(FuelError.wrap(RefreshError)) Result.Failure(FuelError.wrap(RefreshError))
} }

View File

@ -48,4 +48,10 @@ fun toDurationString(seconds: Long): String {
if (minutes > 0) ret.append(" ${minutes}m") if (minutes > 0) ret.append(" ${minutes}m")
return ret.toString() return ret.toString()
}
object Settings {
fun hasAccessToken() = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).contains("access_token")
fun getAccessToken() = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token", "")
fun isAnonymous() = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getBoolean("anonymous", false)
} }

View File

@ -49,6 +49,14 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/anonymous"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_anonymous"
android:buttonTint="@android:color/white"
android:textColor="@android:color/white"/>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_field" android:id="@+id/username_field"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"

View File

@ -4,6 +4,7 @@
<string name="login_welcome">Veuillez saisir les détails de votre instance Funkwhale pour accéder à son contenu</string> <string name="login_welcome">Veuillez saisir les détails de votre instance Funkwhale pour accéder à son contenu</string>
<string name="login_hostname">Nom d\'hôte</string> <string name="login_hostname">Nom d\'hôte</string>
<string name="login_anonymous">Authentification anonyme</string>
<string name="login_username">Nom d\'utilisateur</string> <string name="login_username">Nom d\'utilisateur</string>
<string name="login_password">Mot de passe</string> <string name="login_password">Mot de passe</string>
<string name="login_submit">Se connecter</string> <string name="login_submit">Se connecter</string>

View File

@ -4,6 +4,7 @@
<string name="login_welcome">Please enter the details of your Funkwhale instance to access its content</string> <string name="login_welcome">Please enter the details of your Funkwhale instance to access its content</string>
<string name="login_hostname">Host name</string> <string name="login_hostname">Host name</string>
<string name="login_anonymous">Anonymous authentication</string>
<string name="login_username">Username</string> <string name="login_username">Username</string>
<string name="login_password">Password</string> <string name="login_password">Password</string>
<string name="login_submit">Log in</string> <string name="login_submit">Log in</string>