diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 47ac51f..dd6db60 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,6 +51,8 @@ android {
versionCode = androidGitVersion.code()
versionName = androidGitVersion.name()
+
+ manifestPlaceholders["appAuthRedirectScheme"] = "urn"
}
signingConfigs {
@@ -158,4 +160,6 @@ dependencies {
implementation("com.google.code.gson:gson:2.8.7")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.4.0")
+
+ implementation("net.openid:appauth:0.9.1")
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 10bfe9a..5e495a2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,82 +1,81 @@
+ package="audio.funkwhale.ffa">
-
-
+
+
-
+
-
+
-
+
-
-
-
+
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
diff --git a/app/src/main/java/audio/funkwhale/ffa/FFA.kt b/app/src/main/java/audio/funkwhale/ffa/FFA.kt
index 93eebb0..2a02fbb 100644
--- a/app/src/main/java/audio/funkwhale/ffa/FFA.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/FFA.kt
@@ -70,7 +70,7 @@ class FFA : Application() {
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
- FFA.Companion.instance = this
+ instance = this
when (PowerPreference.getDefaultFile().getString("night_mode")) {
"on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt
index d5ef6d0..b2f4196 100644
--- a/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt
@@ -4,7 +4,6 @@ import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
-import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnLayout
@@ -13,12 +12,13 @@ import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.AppContext
+import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Userinfo
+import audio.funkwhale.ffa.utils.log
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.google.gson.Gson
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
@@ -29,62 +29,79 @@ class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
-
limitContainerWidth()
}
- override fun onResume() {
- super.onResume()
-
- binding.anonymous.setOnCheckedChangeListener { _, isChecked ->
- val state = when (isChecked) {
- true -> View.GONE
- false -> View.VISIBLE
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ data?.let {
+ when (requestCode) {
+ 0 -> {
+ OAuth.exchange(this, data,
+ {
+ PowerPreference
+ .getFileByName(AppContext.PREFS_CREDENTIALS)
+ .setBoolean("anonymous", false)
+
+ lifecycleScope.launch(Main) {
+ Userinfo.get(this@LoginActivity)?.let {
+ startActivity(Intent(this@LoginActivity, MainActivity::class.java))
+
+ return@launch finish()
+ }
+ throw Exception(getString(R.string.login_error_userinfo))
+ }
+ },
+ { "error".log() }
+ )
+ }
}
-
- binding.usernameField.visibility = state
- binding.passwordField.visibility = state
}
+ }
- binding.login?.setOnClickListener {
- var hostname = binding.hostname.text.toString().trim()
- val username = binding.username.text.toString()
- val password = binding.password.text.toString()
+ override fun onResume() {
+ super.onResume()
+ with(binding) {
+ login.setOnClickListener {
+ var hostname = hostname.text.toString().trim()
- try {
- if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
+ try {
+ if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
- Uri.parse(hostname).apply {
- if (!binding.cleartext.isChecked && scheme == "http") {
- throw Exception(getString(R.string.login_error_hostname_https))
- }
+ Uri.parse(hostname).apply {
+ if (!cleartext.isChecked && scheme == "http") {
+ throw Exception(getString(R.string.login_error_hostname_https))
+ }
- if (scheme == null) {
- hostname = when (binding.cleartext.isChecked) {
- true -> "http://$hostname"
- false -> "https://$hostname"
+ if (scheme == null) {
+ hostname = when (cleartext.isChecked) {
+ true -> "http://$hostname"
+ false -> "https://$hostname"
+ }
}
}
- }
- binding.hostnameField.error = ""
+ hostnameField.error = ""
- when (binding.anonymous.isChecked) {
- false -> authedLogin(hostname, username, password)
- true -> anonymousLogin(hostname)
- }
- } catch (e: Exception) {
- val message =
- if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
- else e.message
+ when (anonymous.isChecked) {
+ false -> authedLogin(hostname)
+ true -> anonymousLogin(hostname)
+ }
+ } catch (e: Exception) {
+ val message =
+ if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
+ else e.message
- binding.hostnameField.error = message
+ hostnameField.error = message
+ }
}
}
}
@@ -95,65 +112,13 @@ class LoginActivity : AppCompatActivity() {
limitContainerWidth()
}
- private fun authedLogin(hostname: String, username: String, password: String) {
- val body = mapOf(
- "username" to username,
- "password" to password
- ).toList()
+ private fun authedLogin(hostname: String) {
+ PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
- val dialog = LoginDialog().apply {
- show(supportFragmentManager, "LoginDialog")
- }
-
- lifecycleScope.launch(Main) {
- try {
- val (_, response, result) = Fuel.post("$hostname/api/v1/token/", body)
- .awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
+ OAuth.init(hostname)
- 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)
- }
-
- Userinfo.get()?.let {
- dialog.dismiss()
- startActivity(Intent(this@LoginActivity, MainActivity::class.java))
-
- return@launch finish()
- }
-
- throw Exception(getString(R.string.login_error_userinfo))
- }
-
- is Result.Failure -> {
- dialog.dismiss()
-
- val error = Gson().fromJson(String(response.data), FwCredentials::class.java)
-
- binding.hostnameField.error = null
- binding.usernameField.error = null
-
- if (error != null && error.non_field_errors?.isNotEmpty() == true) {
- binding.usernameField.error = error.non_field_errors[0]
- } else {
- binding.hostnameField.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
-
- binding.hostnameField.error = message
- }
+ OAuth.register {
+ OAuth.authorize(this)
}
}
diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt
index bfb2cf9..c1f2e70 100644
--- a/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt
@@ -103,7 +103,7 @@ class MainActivity : AppCompatActivity() {
CommandBus.send(Command.RefreshService)
lifecycleScope.launch(IO) {
- Userinfo.get()
+ Userinfo.get(this@MainActivity)
}
with(binding) {
@@ -630,7 +630,7 @@ class MainActivity : AppCompatActivity() {
try {
Fuel
.post(mustNormalizeUrl("/api/v1/history/listenings/"))
- .authorize()
+ .authorize(this@MainActivity)
.header("Content-Type", "application/json")
.body(Gson().toJson(mapOf("track" to track.id)))
.awaitStringResponse()
diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt
index 21f4081..9f864f2 100644
--- a/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt
@@ -78,7 +78,7 @@ class SettingsFragment :
activity?.let { activity ->
(activity.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.also { clip ->
Cache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
- clip.setPrimaryClip(ClipData.newPlainText("Otter logs", it))
+ clip.setPrimaryClip(ClipData.newPlainText("Funkwhale logs", it))
Toast.makeText(
activity,
@@ -95,7 +95,7 @@ class SettingsFragment :
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.logout_title))
.setMessage(context.getString(R.string.logout_content))
- .setPositiveButton(android.R.string.yes) { _, _ ->
+ .setPositiveButton(android.R.string.ok) { _, _ ->
CommandBus.send(Command.ClearQueue)
FFA.get().deleteAllData()
@@ -103,7 +103,7 @@ class SettingsFragment :
activity?.setResult(MainActivity.ResultCode.LOGOUT.code)
activity?.finish()
}
- .setNegativeButton(android.R.string.no, null)
+ .setNegativeButton(android.R.string.cancel, null)
.show()
}
}
diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt
index aa62427..26e147d 100644
--- a/app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt
@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.utils.AppContext
+import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Settings
class SplashActivity : AppCompatActivity() {
@@ -13,22 +14,21 @@ class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE).apply {
- when (Settings.hasAccessToken() || Settings.isAnonymous()) {
- true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
- flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
-
- startActivity(this)
- }
-
- false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
- FFA.get().deleteAllData()
-
- flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
-
- startActivity(this)
+ getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE)
+ .apply {
+ when (OAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
+ true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
+ startActivity(this)
+ }
+
+ false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
+ FFA.get().deleteAllData()
+ flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
+ startActivity(this)
+ }
}
}
- }
}
+
}
diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/OAuth2Datasource.kt b/app/src/main/java/audio/funkwhale/ffa/playback/OAuth2Datasource.kt
new file mode 100644
index 0000000..e189045
--- /dev/null
+++ b/app/src/main/java/audio/funkwhale/ffa/playback/OAuth2Datasource.kt
@@ -0,0 +1,48 @@
+package audio.funkwhale.ffa.playback
+
+import android.content.Context
+import android.net.Uri
+import audio.funkwhale.ffa.utils.OAuth
+import com.google.android.exoplayer2.upstream.DataSource
+import com.google.android.exoplayer2.upstream.DataSpec
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
+import com.google.android.exoplayer2.upstream.HttpDataSource
+import com.google.android.exoplayer2.upstream.TransferListener
+
+class OAuthDatasource(
+ private val context: Context,
+ private val http: HttpDataSource
+) : DataSource {
+
+ override fun addTransferListener(transferListener: TransferListener?) {
+ http.addTransferListener(transferListener)
+ }
+
+ override fun open(dataSpec: DataSpec?): Long {
+ OAuth.tryRefreshAccessToken(context)
+ return http.open(dataSpec)
+ }
+
+ override fun read(buffer: ByteArray?, offset: Int, readLength: Int): Int {
+ return http.read(buffer, offset, readLength)
+ }
+
+ override fun getUri(): Uri? {
+ return http.uri
+ }
+
+ override fun close() {
+ http.close()
+ }
+
+}
+
+class OAuth2DatasourceFactory(
+ private val context: Context,
+ private val http: DefaultHttpDataSourceFactory
+) : DataSource.Factory {
+
+ override fun createDataSource(): DataSource {
+ return OAuthDatasource(context, http.createDataSource())
+ }
+}
diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt b/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt
index 16f0dc9..baac829 100644
--- a/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt
@@ -4,7 +4,16 @@ import android.content.Context
import android.net.Uri
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
-import audio.funkwhale.ffa.utils.*
+import audio.funkwhale.ffa.utils.Cache
+import audio.funkwhale.ffa.utils.Command
+import audio.funkwhale.ffa.utils.CommandBus
+import audio.funkwhale.ffa.utils.Event
+import audio.funkwhale.ffa.utils.EventBus
+import audio.funkwhale.ffa.utils.OAuth
+import audio.funkwhale.ffa.utils.QueueCache
+import audio.funkwhale.ffa.utils.Settings
+import audio.funkwhale.ffa.utils.Track
+import audio.funkwhale.ffa.utils.mustNormalizeUrl
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
@@ -21,16 +30,21 @@ class QueueManager(val context: Context) {
var current = -1
companion object {
+
fun factory(context: Context): CacheDataSourceFactory {
- val http = DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.getString(R.string.app_name))).apply {
- defaultRequestProperties.apply {
- if (!Settings.isAnonymous()) {
- set("Authorization", "Bearer ${Settings.getAccessToken()}")
+ val http = DefaultHttpDataSourceFactory(
+ Util.getUserAgent(context, context.getString(R.string.app_name))
+ )
+ .apply {
+ defaultRequestProperties.apply {
+ if (!Settings.isAnonymous()) {
+ set("Authorization", "Bearer ${OAuth.state().accessToken}")
+ }
}
}
- }
- val playbackCache = CacheDataSourceFactory(FFA.get().exoCache, http)
+ val playbackCache =
+ CacheDataSourceFactory(FFA.get().exoCache, OAuth2DatasourceFactory(context, http))
return CacheDataSourceFactory(
FFA.get().exoDownloadCache,
@@ -53,7 +67,8 @@ class QueueManager(val context: Context) {
datasources.addMediaSources(metadata.map { track ->
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
- ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
+ ProgressiveMediaSource.Factory(factory).setTag(track.title)
+ .createMediaSource(Uri.parse(url))
})
}
}
diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt b/app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt
index 31745c1..a53eddb 100644
--- a/app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt
@@ -4,7 +4,16 @@ import android.content.Context
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.Repository
-import audio.funkwhale.ffa.utils.*
+import audio.funkwhale.ffa.utils.Cache
+import audio.funkwhale.ffa.utils.Command
+import audio.funkwhale.ffa.utils.CommandBus
+import audio.funkwhale.ffa.utils.Event
+import audio.funkwhale.ffa.utils.EventBus
+import audio.funkwhale.ffa.utils.Radio
+import audio.funkwhale.ffa.utils.Track
+import audio.funkwhale.ffa.utils.authorize
+import audio.funkwhale.ffa.utils.mustNormalizeUrl
+import audio.funkwhale.ffa.utils.toast
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
@@ -80,7 +89,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
val body = Gson().toJson(request)
val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
- .authorize()
+ .authorize(context)
.header("Content-Type", "application/json")
.body(body)
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
@@ -107,7 +116,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
try {
val body = Gson().toJson(RadioTrackBody(session))
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
- .authorize()
+ .authorize(context)
.header("Content-Type", "application/json")
.apply {
cookie?.let {
@@ -118,7 +127,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
val trackResponse = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
- .authorize()
+ .authorize(context)
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
diff --git a/app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt b/app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt
index 923f03b..8ff809d 100644
--- a/app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt
@@ -4,12 +4,13 @@ import android.content.Context
import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.AlbumsCache
import audio.funkwhale.ffa.utils.AlbumsResponse
-import audio.funkwhale.ffa.utils.OtterResponse
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import java.io.BufferedReader
-class AlbumsRepository(override val context: Context?, artistId: Int? = null) : Repository() {
+class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
+ Repository() {
+
override val cacheId: String by lazy {
if (artistId == null) "albums"
else "albums-artist-$artistId"
@@ -20,7 +21,8 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
- HttpUpstream>(
+ HttpUpstream(
+ context!!,
HttpUpstream.Behavior.Progressive,
url,
object : TypeToken() {}.type
@@ -28,5 +30,6 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
}
override fun cache(data: List) = AlbumsCache(data)
- override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
+ override fun uncache(reader: BufferedReader) =
+ gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
}
diff --git a/app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt b/app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt
index 69e503c..ef8b8f8 100644
--- a/app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt
+++ b/app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt
@@ -9,10 +9,19 @@ import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import java.io.BufferedReader
-class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository