OAuth2 authentication is working. Details and edge cases still pending.

This commit is contained in:
Antoine POPINEAU 2020-08-29 13:58:01 +02:00
parent 0f7703be70
commit 93af0e416e
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
13 changed files with 70 additions and 110 deletions

View File

@ -21,6 +21,7 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
HttpUpstream<Album, OtterResponse<Album>>(
context,
HttpUpstream.Behavior.Progressive,
url,
object : TypeToken<AlbumsResponse>() {}.type

View File

@ -11,7 +11,7 @@ import java.io.BufferedReader
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
override val cacheId = "tracks-artist-$artistId"
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)

View File

@ -11,7 +11,7 @@ import java.io.BufferedReader
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
override val cacheId = "artists"
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
override fun cache(data: List<Artist>) = ArtistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)

View File

@ -16,7 +16,7 @@ import java.io.BufferedReader
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
override val cacheId = "favorites.v2"
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
@ -81,7 +81,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
override val cacheId = "favorited"
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(context, HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
override fun cache(data: List<Int>) = FavoritedCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.repositories
import android.content.Context
import android.net.Uri
import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.Fuel
@ -18,47 +19,49 @@ import java.io.Reader
import java.lang.reflect.Type
import kotlin.math.ceil
class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
class HttpUpstream<D : Any, R : OtterResponse<D>>(val context: Context?, val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
enum class Behavior {
Single, AtOnce, Progressive
}
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow {
if (behavior == Behavior.Single && size != 0) return@flow
context?.let {
if (behavior == Behavior.Single && size != 0) return@flow
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
val page = ceil(size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1
val url =
Uri.parse(url)
.buildUpon()
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
.appendQueryParameter("page", page.toString())
.appendQueryParameter("scope", Settings.getScope())
.build()
.toString()
val url =
Uri.parse(url)
.buildUpon()
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
.appendQueryParameter("page", page.toString())
.appendQueryParameter("scope", Settings.getScope())
.build()
.toString()
get(url).fold(
{ response ->
val data = response.getData()
get(context, url).fold(
{ response ->
val data = response.getData()
when (behavior) {
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
when (behavior) {
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
else -> {
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
else -> {
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
if (response.next != null) fetch(size + data.size).collect { emit(it) }
if (response.next != null) fetch(size + data.size).collect { emit(it) }
}
}
},
{ error ->
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
}
}
},
{ error ->
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
}
}
)
)
}
}.flowOn(IO)
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
@ -67,18 +70,16 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
}
}
suspend fun get(url: String): Result<R, FuelError> {
suspend fun get(context: Context, url: String): Result<R, FuelError> {
return try {
val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
authorize(context)
}
val (_, response, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
if (response.statusCode == 401) {
return retryGet(url)
return Result.Failure(FuelError.wrap(RefreshError))
}
result
@ -86,22 +87,4 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, privat
Result.error(FuelError.wrap(e))
}
}
private suspend fun retryGet(url: String): Result<R, FuelError> {
return try {
return if (HTTP.refresh()) {
val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
request.awaitObjectResult(GenericDeserializer(type))
} else {
Result.Failure(FuelError.wrap(RefreshError))
}
} catch (e: Exception) {
Result.error(FuelError.wrap(e))
}
}
}

View File

@ -14,7 +14,7 @@ import java.io.BufferedReader
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
override val cacheId = "tracks-playlist-$playlistId"
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(context, HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)

View File

@ -11,7 +11,7 @@ import java.io.BufferedReader
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
override val cacheId = "tracks-playlists"
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)

View File

@ -11,7 +11,7 @@ import java.io.BufferedReader
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
override val cacheId = "radios"
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(context, HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
override fun cache(data: List<Radio>) = RadiosCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)

View File

@ -13,7 +13,7 @@ import java.io.BufferedReader
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
override val cacheId: String? = null
override val upstream: Upstream<Track>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
@ -44,7 +44,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
override val cacheId: String? = null
override val upstream: Upstream<Artist>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
override fun cache(data: List<Artist>) = ArtistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
@ -53,7 +53,7 @@ class ArtistsSearchRepository(override val context: Context?, var query: String)
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
override val cacheId: String? = null
override val upstream: Upstream<Album>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
get() = HttpUpstream(context, HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
override fun cache(data: List<Album>) = AlbumsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)

View File

@ -13,7 +13,7 @@ import java.io.BufferedReader
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
override val cacheId = "tracks-album-$albumId"
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(context, HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)

View File

@ -34,36 +34,6 @@ object HTTP {
{ false }
)
}
suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> {
val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
if (response.statusCode == 401) {
return retryGet(url)
}
return result
}
suspend inline fun <reified T : Any> retryGet(url: String): Result<T, FuelError> {
return if (refresh()) {
val request = Fuel.get(mustNormalizeUrl(url)).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
} else {
Result.Failure(FuelError.wrap(RefreshError))
}
}
}
object Cache {

View File

@ -11,11 +11,11 @@ import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.openid.appauth.ClientSecretPost
import kotlin.coroutines.CoroutineContext
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
@ -70,10 +70,27 @@ fun Picasso.maybeLoad(url: String?): RequestCreator {
}
fun Request.authorize(context: Context): Request {
return this.apply {
if (!Settings.isAnonymous()) {
OAuth.state().performActionWithFreshTokens(OAuth.service(context)) { token, _, _ ->
header("Authorization", "Bearer $token")
return runBlocking {
this@authorize.apply {
if (!Settings.isAnonymous()) {
OAuth.state().let { state ->
val old = state.accessToken
val auth = ClientSecretPost(OAuth.state().clientSecret)
val done = CompletableDeferred<Boolean>()
state.performActionWithFreshTokens(OAuth.service(context), auth) { token, _, _ ->
if (token != old && token != null) {
state.save()
}
header("Authorization", "Bearer ${OAuth.state().accessToken}")
done.complete(true)
}
done.await()
return@runBlocking this
}
}
}
}

View File

@ -126,18 +126,7 @@ object OAuth {
ResponseTypeValues.CODE,
REDIRECT_URI
)
.setScopes(
/* "read:profile",
"read:libraries",
"write:libraries",
"read:favorites",
"write:favorites",
"read:playlists",
"write:playlists",
"read:radios",
"write:listenings" */
"read", "write"
)
.setScopes("read", "write")
.build()
}
}