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.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.LoginDialog
@ -30,6 +31,16 @@ class LoginActivity : AppCompatActivity() {
override fun 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 {
var hostname = hostname.text.toString().trim()
val username = username.text.toString()
@ -55,58 +66,106 @@ class LoginActivity : AppCompatActivity() {
hostname_field.error = ""
val body = mapOf(
"username" to username,
"password" to password
).toList()
val dialog = LoginDialog().apply {
show(supportFragmentManager, "LoginDialog")
when (anonymous.isChecked) {
false -> authedLogin(hostname, username, password)
true -> anonymousLogin(hostname)
}
}
}
GlobalScope.launch(Main) {
try {
val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body)
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
private fun authedLogin(hostname: String, username: String, password: String) {
val body = mapOf(
"username" to username,
"password" to password
).toList()
when (result) {
is Result.Success -> {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
setString("hostname", hostname)
setString("username", username)
setString("password", password)
setString("access_token", result.get().token)
}
val dialog = LoginDialog().apply {
show(supportFragmentManager, "LoginDialog")
}
dialog.dismiss()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
GlobalScope.launch(Main) {
try {
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
username_field.error = null
val error = Gson().fromJson(String(response.data), FwCredentials::class.java)
if (error != null && error.non_field_errors.isNotEmpty()) {
username_field.error = error.non_field_errors[0]
} else {
hostname_field.error = result.error.localizedMessage
}
hostname_field.error = null
username_field.error = null
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 androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Settings
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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 {
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.Repository
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.untilNetwork
import com.google.gson.Gson
import kotlinx.android.synthetic.main.fragment_artists.*

View File

@ -57,11 +57,11 @@ class QueueManager(val context: Context) {
}
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 {
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.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -26,13 +25,16 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
}
fun addFavorite(id: Int) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
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) {
Fuel
.post(mustNormalizeUrl("/api/v1/favorites/tracks/"))
.header("Authorization", "Bearer $token")
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
@ -40,13 +42,16 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
}
fun deleteFavorite(id: Int) {
val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token")
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) {
Fuel
.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/"))
.header("Authorization", "Bearer $token")
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.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.result.Result
import com.google.gson.Gson
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@ -37,6 +36,8 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
.build()
.toString()
log(offsetUrl)
get(offsetUrl).fold(
{ response ->
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> {
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
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResponseResult(GenericDeserializer<R>(type))
val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
if (response.statusCode == 401) {
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> {
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
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResult(GenericDeserializer(type))
request.awaitObjectResult(GenericDeserializer(type))
} else {
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> {
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
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
if (response.statusCode == 401) {
return retryGet(url)
@ -52,12 +53,13 @@ object HTTP {
suspend inline fun <reified T : Any> retryGet(url: String): Result<T, FuelError> {
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
.get(mustNormalizeUrl(url))
.header("Authorization", "Bearer $token")
.awaitObjectResult(gsonDeserializerOf(T::class.java))
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
} else {
Result.Failure(FuelError.wrap(RefreshError))
}

View File

@ -48,4 +48,10 @@ fun toDurationString(seconds: Long): String {
if (minutes > 0) ret.append(" ${minutes}m")
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.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
android:id="@+id/username_field"
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_hostname">Nom d\'hôte</string>
<string name="login_anonymous">Authentification anonyme</string>
<string name="login_username">Nom d\'utilisateur</string>
<string name="login_password">Mot de passe</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_hostname">Host name</string>
<string name="login_anonymous">Anonymous authentication</string>
<string name="login_username">Username</string>
<string name="login_password">Password</string>
<string name="login_submit">Log in</string>