Use coroutines

This commit is contained in:
Matthieu 2020-12-29 19:34:48 +01:00
parent 178ae0b392
commit 3a91b02e55
11 changed files with 272 additions and 292 deletions

View File

@ -9,6 +9,7 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.*
@ -17,24 +18,22 @@ import com.h.pixeldroid.utils.db.storeInstance
import com.h.pixeldroid.utils.hasInternet
import com.h.pixeldroid.utils.normalizeDomain
import com.h.pixeldroid.utils.openUrl
import io.reactivex.Single
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
Overview of the flow of the login process: (boxes are requests done in parallel,
since they do not depend on each other)
_________________________________
|[PixelfedAPI.registerApplication]|
|[PixelfedAPI.registerApplicationAsync]|
|[PixelfedAPI.wellKnownNodeInfo] |
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅ +----> [PixelfedAPI.nodeInfoSchema]
+----> [promptOAuth]
@ -130,81 +129,72 @@ class LoginActivity : BaseActivity() {
loadingAnimation(true)
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
Single.zip(
pixelfedAPI.registerApplication(
appName,"$oauthScheme://$PACKAGE_ID", SCOPE
),
pixelfedAPI.wellKnownNodeInfo(),
{ application, nodeInfoJRD ->
// we get here when both results have come in:
Pair(application, nodeInfoJRD)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Application, NodeInfoJRD>> {
override fun onSuccess(pair: Pair<Application, NodeInfoJRD>) {
val (credentials, nodeInfoJRD) = pair
val clientId = credentials.client_id ?: return failedRegistration()
preferences.edit()
.putString("domain", normalizedDomain)
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
//c.f. https://nodeinfo.diaspora.software/protocol.html for more info
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
}?.href ?: return failedRegistration(getString(R.string.instance_error))
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
lifecycleScope.launch {
try {
val credentialsDeferred = async {
pixelfedAPI.registerApplication(
appName, "$oauthScheme://$PACKAGE_ID", SCOPE
)
}
override fun onError(e: Throwable) {
//Error in any of the two requests will get to this
Log.e("registerAppToServer", e.message.toString())
failedRegistration()
}
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
override fun onSubscribe(d: Disposable) {}
})
val credentials = credentialsDeferred.await()
val clientId = credentials.client_id ?: return@launch failedRegistration()
preferences.edit()
.putString("domain", normalizedDomain)
.putString("clientID", clientId)
.putString("clientSecret", credentials.client_secret)
.apply()
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
}?.href ?: return@launch failedRegistration(getString(R.string.instance_error))
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
} catch (exception: IOException) {
return@launch failedRegistration()
} catch (exception: HttpException) {
return@launch failedRegistration()
}
}
}
private fun nodeInfoSchema(
private suspend fun nodeInfoSchema(
normalizedDomain: String,
clientId: String,
nodeInfoSchemaUrl: String
) {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl).enqueue(object : Callback<NodeInfo> {
override fun onResponse(call: Call<NodeInfo>, response: Response<NodeInfo>) {
if (response.body() == null || !response.isSuccessful) {
return failedRegistration(getString(R.string.instance_error))
}
val nodeInfo = response.body() as NodeInfo
val nodeInfo = try {
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
} catch (exception: IOException) {
return failedRegistration(getString(R.string.instance_error))
} catch (exception: HttpException) {
return failedRegistration(getString(R.string.instance_error))
}
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
val builder = AlertDialog.Builder(this@LoginActivity)
builder.apply {
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
}
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}
// Create the AlertDialog
builder.show()
return
} else {
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
val builder = AlertDialog.Builder(this@LoginActivity)
builder.apply {
setMessage(R.string.instance_not_pixelfed_warning)
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
promptOAuth(normalizedDomain, clientId)
}
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
loadingAnimation(false)
wipeSharedSettings()
}
}
override fun onFailure(call: Call<NodeInfo>, t: Throwable) {
failedRegistration(getString(R.string.instance_error))
}
})
// Create the AlertDialog
builder.show()
} else {
promptOAuth(normalizedDomain, clientId)
}
}
@ -233,43 +223,41 @@ class LoginActivity : BaseActivity() {
//Successful authorization
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
//TODO check why we can't do onErrorReturn { null } which would make more sense ¯\_(ツ)_/¯
//Also, maybe find a nicer way to do this, this feels hacky (although it can work fine)
val nullInstance = Instance(null, null, null, null, null, null, null, null)
val nullToken = Token(null, null, null, null, null)
Single.zip(
pixelfedAPI.instance().onErrorReturn { nullInstance },
pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).onErrorReturn { nullToken },
{ instance, token ->
// we get here when all results have come in:
Pair(instance, token)
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Pair<Instance, Token>> {
override fun onSuccess(triple: Pair<Instance, Token>) {
val (instance, token) = triple
if(token == nullToken || token.access_token == null){
return failedRegistration(getString(R.string.token_error))
} else if(instance == nullInstance || instance.uri == null){
return failedRegistration(getString(R.string.instance_error))
}
storeInstance(db, instance)
storeUser(token.access_token, token.refresh_token, clientId, clientSecret, instance.uri)
wipeSharedSettings()
lifecycleScope.launch {
try {
val instanceDeferred = async {
pixelfedAPI.instance()
}
val token = pixelfedAPI.obtainToken(
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
"authorization_code"
)
if (token.access_token == null) {
return@launch failedRegistration(getString(R.string.token_error))
}
override fun onError(e: Throwable) {
Log.e("saveUserAndInstance", e.message.toString())
failedRegistration(getString(R.string.token_error))
val instance = instanceDeferred.await()
if (instance.uri == null) {
return@launch failedRegistration(getString(R.string.instance_error))
}
override fun onSubscribe(d: Disposable) {}
})
storeInstance(db, instance)
storeUser(
token.access_token,
token.refresh_token,
clientId,
clientSecret,
instance.uri
)
wipeSharedSettings()
} catch (exception: IOException) {
return@launch failedRegistration(getString(R.string.token_error))
} catch (exception: HttpException) {
return@launch failedRegistration(getString(R.string.token_error))
}
}
}
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
@ -294,32 +282,29 @@ class LoginActivity : BaseActivity() {
}
}
private fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.body() != null && response.isSuccessful) {
db.userDao().deActivateActiveUsers()
val user = response.body() as Account
addUser(
db,
user,
instance,
activeUser = true,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
apiHolder.setDomainToCurrentUser(db)
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
}
})
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
try {
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
db.userDao().deActivateActiveUsers()
addUser(
db,
user,
instance,
activeUser = true,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
apiHolder.setDomainToCurrentUser(db)
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
} catch (exception: IOException) {
return failedRegistration(getString(R.string.verify_credentials))
} catch (exception: HttpException) {
return failedRegistration(getString(R.string.verify_credentials))
}
}
}

View File

@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide
@ -42,7 +43,9 @@ import kotlinx.android.synthetic.main.activity_main.*
import org.ligi.tracedroid.sending.TraceDroidEmailSender
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class MainActivity : BaseActivity() {
@ -188,23 +191,18 @@ class MainActivity : BaseActivity() {
val clientId = user?.clientId.orEmpty()
val clientSecret = user?.clientSecret.orEmpty()
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
api.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(
call: Call<Account>,
response: Response<Account>
) {
if (response.body() != null && response.isSuccessful) {
val account = response.body() as Account
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
fillDrawerAccountInfo(account.id!!)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("DRAWER ACCOUNT:", t.toString())
}
})
lifecycleScope.launchWhenCreated {
try {
val account = api.verifyCredentials("Bearer $accessToken")
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
fillDrawerAccountInfo(account.id!!)
} catch (exception: IOException) {
Log.e("ACCOUNT UPDATE:", exception.toString())
} catch (exception: HttpException) {
Log.e("ACCOUNT UPDATE:", exception.toString())
}
}
}
}

View File

@ -33,7 +33,7 @@ class PostFragment : BaseFragment() {
val user = db.userDao().getActiveUser()!!
val api = apiHolder.api ?: apiHolder.setDomain(user)
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val holder = StatusViewHolder(root)

View File

@ -89,8 +89,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
val user = db.userDao().getActiveUser()!!
(holder as StatusViewHolder).bind(it, apiHolder.setDomain(user), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
}
}
}

View File

@ -79,8 +79,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
val user = db.userDao().getActiveUser()!!
(holder as StatusViewHolder).bind(it, apiHolder.setDomain(user), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
}
}
}

View File

@ -9,6 +9,7 @@ import android.widget.*
import androidx.annotation.StringRes
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -22,9 +23,12 @@ import com.h.pixeldroid.posts.parseHTMLText
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.openUrl
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class ProfileActivity : BaseActivity() {
private lateinit var pixelfedAPI : PixelfedAPI
@ -75,25 +79,19 @@ class ProfileActivity : BaseActivity() {
setViews(account)
setPosts(account)
} else {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
val myAccount = response.body()!!
setViews(myAccount)
// Populate profile page with user's posts
setPosts(myAccount)
} else {
showError()
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("ProfileActivity:", t.toString())
showError()
}
})
lifecycleScope.launchWhenResumed {
val myAccount: Account = try {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
} catch (exception: IOException) {
Log.e("ProfileActivity:", exception.toString())
return@launchWhenResumed showError()
} catch (exception: HttpException) {
return@launchWhenResumed showError()
}
setViews(myAccount)
// Populate profile page with user's posts
setPosts(myAccount)
}
}
//if we aren't viewing our own account, activate follow button
@ -244,40 +242,35 @@ class ProfileActivity : BaseActivity() {
*/
private fun activateFollow(account: Account) {
// Get relationship between the two users (credential and this) and set followButton accordingly
pixelfedAPI.checkRelationships("Bearer $accessToken", listOf(account.id.orEmpty()))
.enqueue(object : Callback<List<Relationship>> {
lifecycleScope.launch {
try {
val relationship = pixelfedAPI.checkRelationships(
"Bearer $accessToken", listOf(account.id.orEmpty())
).firstOrNull()
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_status_failed),
Toast.LENGTH_SHORT
).show()
}
if(relationship != null){
val followButton = findViewById<Button>(R.id.followButton)
override fun onResponse(
call: Call<List<Relationship>>,
response: Response<List<Relationship>>
) {
if (response.code() == 200) {
if (response.body()!!.isNotEmpty()) {
val followButton = findViewById<Button>(R.id.followButton)
if (response.body()!![0].following) {
setOnClickUnfollow(account)
} else {
setOnClickFollow(account)
}
followButton.visibility = View.VISIBLE
}
if (relationship.following) {
setOnClickUnfollow(account)
} else {
Toast.makeText(
applicationContext, getString(R.string.follow_button_failed),
Toast.LENGTH_SHORT
).show()
setOnClickFollow(account)
}
followButton.visibility = View.VISIBLE
}
})
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_status_failed),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.follow_button_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
private fun setOnClickFollow(account: Account) {
@ -286,31 +279,23 @@ class ProfileActivity : BaseActivity() {
followButton.setText(R.string.follow)
followButton.setOnClickListener {
pixelfedAPI.follow(account.id.orEmpty(), "Bearer $accessToken")
.enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("FOLLOW ERROR", t.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
}
override fun onResponse(
call: Call<Relationship>,
response: Response<Relationship>
) {
if (response.code() == 200) {
setOnClickUnfollow(account)
} else if (response.code() == 403) {
Toast.makeText(
applicationContext, getString(R.string.action_not_allowed),
Toast.LENGTH_SHORT
).show()
}
}
})
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.follow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickUnfollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
}
}
}
}
@ -320,31 +305,24 @@ class ProfileActivity : BaseActivity() {
followButton.setText(R.string.unfollow)
followButton.setOnClickListener {
pixelfedAPI.unfollow(account.id.orEmpty(), "Bearer $accessToken")
.enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
Log.e("UNFOLLOW ERROR", t.toString())
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
}
override fun onResponse(
call: Call<Relationship>,
response: Response<Relationship>
) {
if (response.code() == 200) {
setOnClickFollow(account)
} else if (response.code() == 401) {
Toast.makeText(
applicationContext, getString(R.string.access_token_invalid),
Toast.LENGTH_SHORT
).show()
}
}
})
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.unfollow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickFollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.h.pixeldroid.utils.api
import com.h.pixeldroid.utils.api.objects.*
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.Deferred
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.Response
@ -35,17 +36,17 @@ interface PixelfedAPI {
@FormUrlEncoded
@POST("/api/v1/apps")
fun registerApplication(
suspend fun registerApplication(
@Field("client_name") client_name: String,
@Field("redirect_uris") redirect_uris: String,
@Field("scopes") scopes: String? = null,
@Field("website") website: String? = null
): Single<Application>
): Application
@FormUrlEncoded
@POST("/oauth/token")
fun obtainToken(
suspend fun obtainToken(
@Field("client_id") client_id: String,
@Field("client_secret") client_secret: String,
@Field("redirect_uri") redirect_uri: String? = null,
@ -53,41 +54,41 @@ interface PixelfedAPI {
@Field("code") code: String? = null,
@Field("grant_type") grant_type: String? = null,
@Field("refresh_token") refresh_token: String? = null
): Single<Token>
): Token
// get instance configuration
@GET("/api/v1/instance")
fun instance() : Single<Instance>
suspend fun instance() : Instance
/**
* Instance info from the Nodeinfo .well_known (https://nodeinfo.diaspora.software/protocol.html) endpoint
*/
@GET("/.well-known/nodeinfo")
fun wellKnownNodeInfo() : Single<NodeInfoJRD>
suspend fun wellKnownNodeInfo() : NodeInfoJRD
/**
* Instance info from [NodeInfo] (https://nodeinfo.diaspora.software/schema.html) endpoint
*/
@GET
fun nodeInfoSchema(
suspend fun nodeInfoSchema(
@Url nodeInfo_schema_url: String
) : Call<NodeInfo>
) : NodeInfo
@FormUrlEncoded
@POST("/api/v1/accounts/{id}/follow")
fun follow(
suspend fun follow(
//The authorization header needs to be of the form "Bearer <token>"
@Path("id") statusId: String,
@Header("Authorization") authorization: String,
@Field("reblogs") reblogs : Boolean = true
) : Call<Relationship>
) : Relationship
@POST("/api/v1/accounts/{id}/unfollow")
fun unfollow(
suspend fun unfollow(
//The authorization header needs to be of the form "Bearer <token>"
@Path("id") statusId: String,
@Header("Authorization") authorization: String
) : Call<Relationship>
) : Relationship
@POST("api/v1/statuses/{id}/favourite")
fun likePost(
@ -205,10 +206,11 @@ interface PixelfedAPI {
): List<Notification>
@GET("/api/v1/accounts/verify_credentials")
fun verifyCredentials(
suspend fun verifyCredentials(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String
): Call<Account>
): Account
@GET("/api/v1/accounts/{id}/statuses")
fun accountPosts(
@ -217,10 +219,10 @@ interface PixelfedAPI {
): Call<List<Status>>
@GET("/api/v1/accounts/relationships")
fun checkRelationships(
suspend fun checkRelationships(
@Header("Authorization") authorization : String,
@Query("id[]") account_ids : List<String>
) : Call<List<Relationship>>
) : List<Relationship>
@GET("/api/v1/accounts/{id}/followers")
suspend fun followers(

View File

@ -1,9 +1,6 @@
package com.h.pixeldroid.utils.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
@Dao
@ -11,6 +8,9 @@ interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: UserDatabaseEntity)
@Query("UPDATE users SET accessToken = :accessToken WHERE user_id = :id and instance_uri = :instance_uri")
fun updateAccessToken(accessToken: String, id: String, instance_uri: String)
@Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity>

View File

@ -2,9 +2,11 @@ package com.h.pixeldroid.utils.di
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.runBlocking
import okhttp3.*
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
@ -18,11 +20,11 @@ class APIModule{
@Provides
@Singleton
fun providesAPIHolder(db: AppDatabase): PixelfedAPIHolder {
return PixelfedAPIHolder(db.userDao().getActiveUser())
return PixelfedAPIHolder(db)
}
}
class TokenAuthenticator(val user: UserDatabaseEntity) : Authenticator {
class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase) : Authenticator {
private val pixelfedAPI = PixelfedAPI.createFromUrl(user.instance_uri)
@ -32,37 +34,48 @@ class TokenAuthenticator(val user: UserDatabaseEntity) : Authenticator {
return null // Give up, we've already failed to authenticate.
}
// Refresh the access_token using a synchronous api request
val newAccessToken: String = try {
pixelfedAPI.obtainToken(
scope = "", grant_type = "refresh_token",
refresh_token = user.refreshToken, client_id = user.clientId, client_secret = user.clientSecret
).blockingGet().access_token
val newAccessToken: String? = try {
runBlocking {
pixelfedAPI.obtainToken(
scope = "",
grant_type = "refresh_token",
refresh_token = user.refreshToken,
client_id = user.clientId,
client_secret = user.clientSecret
).access_token
}
}catch (e: Exception){
null
}.orEmpty()
}
if (newAccessToken != null) {
db.userDao().updateAccessToken(newAccessToken, user.user_id, user.instance_uri)
}
// Add new header to rejected request and retry it
return response.request.newBuilder()
.header("Authorization", "Bearer $newAccessToken")
.header("Authorization", "Bearer ${newAccessToken.orEmpty()}")
.build()
}
}
class PixelfedAPIHolder(user: UserDatabaseEntity?){
class PixelfedAPIHolder(db: AppDatabase?){
private val intermediate: Retrofit.Builder = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
var api: PixelfedAPI? = if (user != null) setDomain(user) else null
var api: PixelfedAPI? =
db?.userDao()?.getActiveUser()?.let {
setDomainToCurrentUser(db, it)
}
fun setDomainToCurrentUser(db: AppDatabase): PixelfedAPI {
return setDomain(db.userDao().getActiveUser()!!)
}
fun setDomain(user: UserDatabaseEntity): PixelfedAPI {
fun setDomainToCurrentUser(
db: AppDatabase,
user: UserDatabaseEntity = db.userDao().getActiveUser()!!
): PixelfedAPI {
val newAPI = intermediate
.baseUrl(user.instance_uri)
.client(OkHttpClient().newBuilder().authenticator(TokenAuthenticator(user)).build())
.build().create(PixelfedAPI::class.java)
.baseUrl(user.instance_uri)
.client(OkHttpClient().newBuilder().authenticator(TokenAuthenticator(user, db)).build())
.build().create(PixelfedAPI::class.java)
api = newAPI
return newAPI
}

View File

@ -7,6 +7,7 @@
<string name="registration_failed">"Could not register the application with this server"</string>
<string name="browser_launch_failed">"Could not launch a browser, do you have one?"</string>
<string name="auth_failed">"Could not authenticate"</string>
<string name="verify_credentials">"Could not get user information"</string>
<string name="token_error">"Error getting token"</string>
<string name="instance_error">"Could not get instance information"</string>
<string name="instance_not_pixelfed_warning">"This doesn't seem to be a Pixelfed instance, so the app could break in unexpected ways."</string>

View File

@ -112,10 +112,11 @@ class APIUnitTest {
.withHeader("Content-Type", "application/json")
.withBody(""" {"id":3197,"name":"Pixeldroid","website":null,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":3197,"client_secret":"hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m","vapid_key":null}"""
)))
val call: Single<Application> = PixelfedAPI.createFromUrl("http://localhost:8089")
.registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow")
val application: Application = runBlocking {
PixelfedAPI.createFromUrl("http://localhost:8089")
.registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow")
}
val application: Application = call.toFuture().get()
assertEquals("3197", application.client_id)
assertEquals("hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m", application.client_secret)
assertEquals("Pixeldroid", application.name)
@ -141,10 +142,14 @@ class APIUnitTest {
val OAUTH_SCHEME = "oauth2redirect"
val SCOPE = "read write follow"
val PACKAGE_ID = "com.h.pixeldroid"
val call: Single<Token> = PixelfedAPI.createFromUrl("http://localhost:8089")
.obtainToken("123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc",
"authorization_code")
val token: Token = call.toFuture().get()
val token: Token = runBlocking {
PixelfedAPI.createFromUrl("http://localhost:8089")
.obtainToken(
"123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc",
"authorization_code"
)
}
assertEquals("ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0", token.access_token)
assertEquals("Bearer", token.token_type)
assertEquals("read write follow push", token.scope)