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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,6 @@
package com.h.pixeldroid.utils.db.dao package com.h.pixeldroid.utils.db.dao
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
@Dao @Dao
@ -11,6 +8,9 @@ interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: UserDatabaseEntity) 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") @Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity> 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.api.PixelfedAPI
import com.h.pixeldroid.utils.db.AppDatabase import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.runBlocking
import okhttp3.* import okhttp3.*
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
@ -18,11 +20,11 @@ class APIModule{
@Provides @Provides
@Singleton @Singleton
fun providesAPIHolder(db: AppDatabase): PixelfedAPIHolder { 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) private val pixelfedAPI = PixelfedAPI.createFromUrl(user.instance_uri)
@ -32,36 +34,47 @@ class TokenAuthenticator(val user: UserDatabaseEntity) : Authenticator {
return null // Give up, we've already failed to authenticate. return null // Give up, we've already failed to authenticate.
} }
// Refresh the access_token using a synchronous api request // Refresh the access_token using a synchronous api request
val newAccessToken: String = try { val newAccessToken: String? = try {
runBlocking {
pixelfedAPI.obtainToken( pixelfedAPI.obtainToken(
scope = "", grant_type = "refresh_token", scope = "",
refresh_token = user.refreshToken, client_id = user.clientId, client_secret = user.clientSecret grant_type = "refresh_token",
).blockingGet().access_token refresh_token = user.refreshToken,
client_id = user.clientId,
client_secret = user.clientSecret
).access_token
}
}catch (e: Exception){ }catch (e: Exception){
null null
}.orEmpty() }
if (newAccessToken != null) {
db.userDao().updateAccessToken(newAccessToken, user.user_id, user.instance_uri)
}
// Add new header to rejected request and retry it // Add new header to rejected request and retry it
return response.request.newBuilder() return response.request.newBuilder()
.header("Authorization", "Bearer $newAccessToken") .header("Authorization", "Bearer ${newAccessToken.orEmpty()}")
.build() .build()
} }
} }
class PixelfedAPIHolder(user: UserDatabaseEntity?){ class PixelfedAPIHolder(db: AppDatabase?){
private val intermediate: Retrofit.Builder = Retrofit.Builder() private val intermediate: Retrofit.Builder = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
var api: PixelfedAPI? = if (user != null) setDomain(user) else null var api: PixelfedAPI? =
db?.userDao()?.getActiveUser()?.let {
fun setDomainToCurrentUser(db: AppDatabase): PixelfedAPI { setDomainToCurrentUser(db, it)
return setDomain(db.userDao().getActiveUser()!!)
} }
fun setDomain(user: UserDatabaseEntity): PixelfedAPI { fun setDomainToCurrentUser(
db: AppDatabase,
user: UserDatabaseEntity = db.userDao().getActiveUser()!!
): PixelfedAPI {
val newAPI = intermediate val newAPI = intermediate
.baseUrl(user.instance_uri) .baseUrl(user.instance_uri)
.client(OkHttpClient().newBuilder().authenticator(TokenAuthenticator(user)).build()) .client(OkHttpClient().newBuilder().authenticator(TokenAuthenticator(user, db)).build())
.build().create(PixelfedAPI::class.java) .build().create(PixelfedAPI::class.java)
api = newAPI api = newAPI
return newAPI return newAPI

View File

@ -7,6 +7,7 @@
<string name="registration_failed">"Could not register the application with this server"</string> <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="browser_launch_failed">"Could not launch a browser, do you have one?"</string>
<string name="auth_failed">"Could not authenticate"</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="token_error">"Error getting token"</string>
<string name="instance_error">"Could not get instance information"</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> <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") .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}""" .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") val application: Application = runBlocking {
PixelfedAPI.createFromUrl("http://localhost:8089")
.registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow") .registerApplication("Pixeldroid", "urn:ietf:wg:oauth:2.0:oob", "read write follow")
}
val application: Application = call.toFuture().get()
assertEquals("3197", application.client_id) assertEquals("3197", application.client_id)
assertEquals("hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m", application.client_secret) assertEquals("hhRwLupqUJPghKsZzpZtxNV67g5DBdPYCqW6XE3m", application.client_secret)
assertEquals("Pixeldroid", application.name) assertEquals("Pixeldroid", application.name)
@ -141,10 +142,14 @@ class APIUnitTest {
val OAUTH_SCHEME = "oauth2redirect" val OAUTH_SCHEME = "oauth2redirect"
val SCOPE = "read write follow" val SCOPE = "read write follow"
val PACKAGE_ID = "com.h.pixeldroid" val PACKAGE_ID = "com.h.pixeldroid"
val call: Single<Token> = PixelfedAPI.createFromUrl("http://localhost:8089")
.obtainToken("123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc", val token: Token = runBlocking {
"authorization_code") PixelfedAPI.createFromUrl("http://localhost:8089")
val token: Token = call.toFuture().get() .obtainToken(
"123", "ssqdfqsdfqds", "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, "abc",
"authorization_code"
)
}
assertEquals("ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0", token.access_token) assertEquals("ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0", token.access_token)
assertEquals("Bearer", token.token_type) assertEquals("Bearer", token.token_type)
assertEquals("read write follow push", token.scope) assertEquals("read write follow push", token.scope)