Merge branch 'remove_callback' into 'master'

Use coroutines instead of callbacks

Closes #270

See merge request pixeldroid/PixelDroid!279
This commit is contained in:
Matthieu 2020-12-30 19:10:48 +01:00
commit 32997f5e6c
19 changed files with 762 additions and 761 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

@ -12,6 +12,7 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
@ -34,7 +35,9 @@ import kotlinx.android.synthetic.main.image_album_creation.view.*
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
private const val TAG = "Post Creation Activity"
private const val MORE_PICTURES_REQUEST_CODE = 0xffff
@ -213,33 +216,30 @@ class PostCreationActivity : BaseActivity() {
private fun post() {
val description = new_post_description_input_field.text.toString()
enableButton(false)
pixelfedAPI.postStatus(
authorization = "Bearer $accessToken",
statusText = description,
media_ids = muListOfIds.toList()
).enqueue(object: Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
lifecycleScope.launchWhenCreated {
try {
pixelfedAPI.postStatus(
authorization = "Bearer $accessToken",
statusText = description,
media_ids = muListOfIds.toList()
)
Toast.makeText(applicationContext,getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(this@PostCreationActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(applicationContext,getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
enableButton(true)
} catch (exception: HttpException) {
Toast.makeText(applicationContext,getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, t.message + call.request())
Log.e(TAG, exception.response().toString() + exception.message().toString())
enableButton(true)
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if (response.code() == 200) {
Toast.makeText(applicationContext,getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(this@PostCreationActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
} else {
Toast.makeText(applicationContext,getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, call.request().toString() + response.raw().toString())
enableButton(true)
}
}
})
}
}
private fun enableButton(enable: Boolean = true){

View File

@ -12,10 +12,13 @@ import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.text.toSpanned
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Account.Companion.getAccountFromId
import com.h.pixeldroid.utils.api.objects.Account.Companion.openAccountFromId
import com.h.pixeldroid.utils.api.objects.Mention
import kotlinx.coroutines.coroutineScope
import java.net.URI
import java.net.URISyntaxException
import java.text.ParseException
@ -51,7 +54,8 @@ fun parseHTMLText(
mentions: List<Mention>?,
api : PixelfedAPI,
context: Context,
credential: String
credential: String,
lifecycleScope: LifecycleCoroutineScope
) : Spanned {
//Convert text to spannable
val content = fromHtml(text)
@ -103,7 +107,9 @@ fun parseHTMLText(
override fun onClick(widget: View) {
Log.e("MENTION", "CLICKED")
//Retrieve the account for the given profile
getAccountFromId(accountId, api, context, credential)
lifecycleScope.launchWhenCreated {
openAccountFromId(accountId, api, context, credential)
}
}
}
}

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

@ -3,6 +3,7 @@ package com.h.pixeldroid.posts
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.objects.Report
import com.h.pixeldroid.utils.api.objects.Status
@ -10,7 +11,9 @@ import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_report.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class ReportActivity : BaseActivity() {
@ -37,33 +40,34 @@ class ReportActivity : BaseActivity() {
val accessToken = user?.accessToken.orEmpty()
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), textInputLayout.editText?.text.toString())
.enqueue(object : Callback<Report> {
override fun onResponse(
call: Call<Report>,
response: Response<Report>
) {
if (response.body() == null || !response.isSuccessful) {
textInputLayout.error = getString(R.string.report_error)
reportButton.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = true
reportProgressBar.visibility = View.GONE
} else {
reportProgressBar.visibility = View.GONE
reportButton.isEnabled = false
reportButton.text = getString(R.string.reported)
reportButton.visibility = View.VISIBLE
}
}
override fun onFailure(call: Call<Report>, t: Throwable) {
Log.e("REPORT:", t.toString())
}
})
lifecycleScope.launchWhenCreated {
try {
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), textInputLayout.editText?.text.toString())
reportStatus(true)
} catch (exception: IOException) {
reportStatus(false)
} catch (exception: HttpException) {
reportStatus(false)
}
}
}
}
private fun reportStatus(success: Boolean){
if(success){
reportProgressBar.visibility = View.GONE
reportButton.isEnabled = false
reportButton.text = getString(R.string.reported)
reportButton.visibility = View.VISIBLE
} else {
textInputLayout.error = getString(R.string.report_error)
reportButton.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = true
reportProgressBar.visibility = View.GONE
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()

View File

@ -23,12 +23,12 @@ import com.bumptech.glide.RequestBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Context
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.db.AppDatabase
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
@ -206,12 +206,24 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}.attach()
}
private fun setDescription(rootView: View, api: PixelfedAPI, credential: String) {
private fun setDescription(
rootView: View,
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
rootView.findViewById<TextView>(R.id.description).apply {
if (status?.content.isNullOrBlank()) {
visibility = View.GONE
} else {
text = parseHTMLText(status?.content.orEmpty(), status?.mentions, api, rootView.context, credential)
text = parseHTMLText(
status?.content.orEmpty(),
status?.mentions,
api,
rootView.context,
credential,
lifecycleScope
)
movementMethod = LinkMovementMethod.getInstance()
}
}
@ -222,32 +234,32 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val credential = "Bearer ${user.accessToken}"
//Set the special HTML text
setDescription(holder.view, api, credential)
setDescription(holder.view, api, credential, lifecycleScope)
//Activate onclickListeners
activateLiker(
holder, api, credential,
status?.favourited ?: false
status?.favourited ?: false,
lifecycleScope
)
activateReblogger(
holder, api, credential,
status?.reblogged ?: false
status?.reblogged ?: false,
lifecycleScope
)
activateCommenter(holder, api, credential)
activateCommenter(holder, api, credential, lifecycleScope)
showComments(holder, api, credential)
//Activate double tap liking
activateDoubleTapLiker(holder, api, credential)
showComments(holder, api, credential, lifecycleScope)
activateMoreButton(holder, api, db, lifecycleScope)
}
private fun activateReblogger(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isReblogged: Boolean
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isReblogged: Boolean,
lifecycleScope: LifecycleCoroutineScope
) {
holder.reblogger.apply {
//Set initial button state
@ -255,12 +267,14 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
//Activate the button
setEventListener { _, buttonState ->
if (buttonState) {
// Button is active
undoReblogPost(holder, api, credential)
} else {
// Button is inactive
reblogPost(holder, api, credential)
lifecycleScope.launchWhenCreated {
if (buttonState) {
// Button is active
undoReblogPost(holder, api, credential)
} else {
// Button is inactive
reblogPost(holder, api, credential)
}
}
//show animation or not?
true
@ -268,63 +282,50 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
}
private fun reblogPost(
private suspend fun reblogPost(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String
) {
//Call the api function
status?.id?.let {
api.reblogStatus(credential, it).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("REBLOG ERROR", t.toString())
holder.reblogger.isChecked = false
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
try {
val resp = api.reblogStatus(credential, it)
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.reblogger.isChecked = false
}
}
})
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
holder.reblogger.isChecked = false
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.reblogger.isChecked = false
}
}
}
private fun undoReblogPost(
private suspend fun undoReblogPost(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
//Call the api function
status?.id?.let {
api.undoReblogStatus(credential, it).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("REBLOG ERROR", t.toString())
holder.reblogger.isChecked = true
}
try {
val resp = api.undoReblogStatus(credential, it)
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.reblogger.isChecked = true
}
}
})
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
holder.reblogger.isChecked = true
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.reblogger.isChecked = true
}
}
}
@ -444,45 +445,12 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
}
private fun activateDoubleTapLiker(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String
) {
holder.apply {
var clicked = false
postPic.setOnClickListener {
//Check that the post isn't hidden
if(sensitiveW.visibility == View.GONE) {
//Check for double click
if(clicked) {
if (holder.liker.isChecked) {
// Button is active, unlike
holder.liker.isChecked = false
unLikePostCall(holder, api, credential)
} else {
// Button is inactive, like
holder.liker.playAnimation()
holder.liker.isChecked = true
likePostCall(holder, api, credential)
}
} else {
clicked = true
//Reset clicked to false after 500ms
postPic.handler.postDelayed(fun() { clicked = false }, 500)
}
}
}
}
}
private fun activateLiker(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isLiked: Boolean
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isLiked: Boolean,
lifecycleScope: LifecycleCoroutineScope
) {
holder.liker.apply {
@ -491,84 +459,104 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
//Activate the liker
setEventListener { _, buttonState ->
if (buttonState) {
// Button is active, unlike
unLikePostCall(holder, api, credential)
} else {
// Button is inactive, like
likePostCall(holder, api, credential)
lifecycleScope.launchWhenCreated {
if (buttonState) {
// Button is active, unlike
unLikePostCall(holder, api, credential)
} else {
// Button is inactive, like
likePostCall(holder, api, credential)
}
}
//show animation or not?
true
}
}
}
private fun likePostCall(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
//Call the api function
status?.id?.let {
api.likePost(credential, it).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("LIKE ERROR", t.toString())
holder.liker.isChecked = false
}
//Activate double tap liking
holder.apply {
var clicked = false
postPic.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(sensitiveW.visibility == View.GONE) {
//Check for double click
if(clicked) {
if (holder.liker.isChecked) {
// Button is active, unlike
holder.liker.isChecked = false
unLikePostCall(holder, api, credential)
} else {
// Button is inactive, like
holder.liker.playAnimation()
holder.liker.isChecked = true
likePostCall(holder, api, credential)
}
} else {
clicked = true
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.liker.isChecked = false
//Reset clicked to false after 500ms
postPic.handler.postDelayed(fun() { clicked = false }, 500)
}
}
}
})
}
}
}
private fun unLikePostCall(
private suspend fun likePostCall(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
//Call the api function
status?.id?.let {
api.unlikePost(credential, it).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("UNLIKE ERROR", t.toString())
holder.liker.isChecked = true
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
try {
val resp = api.likePost(credential, it)
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.liker.isChecked = true
}
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
} catch (exception: IOException) {
Log.e("LIKE ERROR", exception.toString())
holder.liker.isChecked = false
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.liker.isChecked = false
}
}
}
}
private suspend fun unLikePostCall(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
//Call the api function
status?.id?.let {
})
try {
val resp = api.unlikePost(credential, it)
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
} catch (exception: IOException) {
Log.e("UNLIKE ERROR", exception.toString())
holder.liker.isChecked = true
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.liker.isChecked = true
}
}
}
private fun showComments(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
//Show all comments of a post
if (status?.replies_count == 0) {
@ -580,8 +568,10 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
setOnClickListener {
visibility = View.GONE
//Retrieve the comments
retrieveComments(holder, api, credential)
lifecycleScope.launchWhenCreated {
//Retrieve the comments
retrieveComments(holder, api, credential)
}
}
}
}
@ -590,7 +580,8 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private fun activateCommenter(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
//Toggle comment button
toggleCommentInput(holder)
@ -606,9 +597,10 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
Toast.LENGTH_SHORT
).show()
} else {
//Post the comment
postComment(holder, api, credential)
lifecycleScope.launchWhenCreated {
postComment(holder, api, credential)
}
}
}
}
@ -648,43 +640,34 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
view.commentText.text = commentContent
}
private fun retrieveComments(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
private suspend fun retrieveComments(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
status?.id?.let {
api.statusComments(it, credential).enqueue(object :
Callback<Context> {
override fun onFailure(call: Call<Context>, t: Throwable) {
Log.e("COMMENT FETCH ERROR", t.toString())
try {
val statuses = api.statusComments(it, credential).descendants
holder.commentCont.removeAllViews()
//Create the new views for each comment
for (status in statuses) {
addComment(holder.view.context, holder.commentCont, status.account!!.username!!,
status.content!!
)
}
holder.commentCont.visibility = View.VISIBLE
override fun onResponse(
call: Call<Context>,
response: Response<Context>
) {
if(response.code() == 200) {
val statuses = response.body()!!.descendants
holder.commentCont.removeAllViews()
//Create the new views for each comment
for (status in statuses) {
addComment(holder.view.context, holder.commentCont, status.account!!.username!!,
status.content!!
)
}
holder.commentCont.visibility = View.VISIBLE
} else {
Log.e("COMMENT ERROR", "${response.code()} with body ${response.errorBody()}")
}
}
})
} catch (exception: IOException) {
Log.e("COMMENT FETCH ERROR", exception.toString())
} catch (exception: HttpException) {
Log.e("COMMENT ERROR", "${exception.code()} with body ${exception.response()?.errorBody()}")
}
}
}
private fun postComment(
private suspend fun postComment(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
@ -692,39 +675,34 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val textIn = holder.comment.text
val nonNullText = textIn.toString()
status?.id?.let {
api.postStatus(credential, nonNullText, it).enqueue(object :
Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("COMMENT ERROR", t.toString())
Toast.makeText(
holder.view.context, holder.view.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
}
try {
val response = api.postStatus(credential, nonNullText, it)
holder.commentIn.visibility = View.GONE
override fun onResponse(call: Call<Status>, response: Response<Status>) {
//Check that the received response code is valid
if (response.code() == 200) {
val resp = response.body()!!
holder.commentIn.visibility = View.GONE
//Add the comment to the comment section
addComment(
holder.view.context, holder.commentCont, response.account!!.username!!,
response.content!!
)
//Add the comment to the comment section
addComment(
holder.view.context, holder.commentCont, resp.account!!.username!!,
resp.content!!
)
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT
).show()
Log.e("COMMENT SUCCESS", "posted: $textIn")
} else {
Log.e("ERROR_CODE", response.code().toString())
}
}
})
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Log.e("COMMENT ERROR", exception.toString())
Toast.makeText(
holder.view.context, holder.view.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
holder.view.context, holder.view.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
Log.e("ERROR_CODE", exception.code().toString())
}
}
}

View File

@ -10,7 +10,9 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
@ -53,7 +55,10 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
// get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(this, ViewModelFactory(db, db.notificationDao(), NotificationsRemoteMediator(apiHolder, db)))
viewModel = ViewModelProvider(
this,
ViewModelFactory(db, db.notificationDao(), NotificationsRemoteMediator(apiHolder, db))
)
.get(FeedViewModel::class.java) as FeedViewModel<Notification>
launch()
@ -62,149 +67,201 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
return view
}
}
/**
* View Holder for a [Notification] RecyclerView list item.
*/
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val notificationType: TextView = view.notification_type
private val notificationTime: TextView = view.notification_time
private val postDescription: TextView = view.notification_post_description
private val avatar: ImageView = view.notification_avatar
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
/**
* View Holder for a [Notification] RecyclerView list item.
*/
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val notificationType: TextView = view.notification_type
private val notificationTime: TextView = view.notification_time
private val postDescription: TextView = view.notification_post_description
private val avatar: ImageView = view.notification_avatar
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
private var notification: Notification? = null
private var notification: Notification? = null
init {
itemView.setOnClickListener {
init {
itemView.setOnClickListener {
notification?.openActivity()
}
}
private fun Notification.openActivity() {
val intent: Intent =
when (type){
Notification.NotificationType.mention, Notification.NotificationType.favourite,
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
openPostFromNotification()
private fun Notification.openActivity() {
val intent: Intent =
when (type) {
Notification.NotificationType.mention, Notification.NotificationType.favourite,
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
openPostFromNotification()
}
Notification.NotificationType.follow -> {
Intent(itemView.context, ProfileActivity::class.java).apply {
putExtra(Account.ACCOUNT_TAG, account)
}
}
null -> return //TODO show an error here?
}
itemView.context.startActivity(intent)
}
private fun Notification.openPostFromNotification(): Intent =
Intent(itemView.context, PostActivity::class.java).apply {
putExtra(Status.POST_TAG, status)
}
Notification.NotificationType.follow -> {
Intent(itemView.context, ProfileActivity::class.java).apply {
putExtra(Account.ACCOUNT_TAG, account)
private fun setNotificationType(
type: Notification.NotificationType,
username: String,
textView: TextView
) {
val context = textView.context
val (format: String, drawable: Drawable?) = when (type) {
Notification.NotificationType.follow -> {
getStringAndDrawable(
context,
R.string.followed_notification,
R.drawable.ic_follow
)
}
Notification.NotificationType.mention -> {
getStringAndDrawable(
context,
R.string.mention_notification,
R.drawable.mention_at_24dp
)
}
Notification.NotificationType.reblog -> {
getStringAndDrawable(
context,
R.string.shared_notification,
R.drawable.ic_reblog_blue
)
}
Notification.NotificationType.favourite -> {
getStringAndDrawable(
context,
R.string.liked_notification,
R.drawable.ic_like_full
)
}
Notification.NotificationType.poll -> {
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
}
}
null -> return //TODO show an error here?
}
itemView.context.startActivity(intent)
}
private fun Notification.openPostFromNotification(): Intent =
Intent(itemView.context, PostActivity::class.java).apply {
putExtra(Status.POST_TAG, status)
}
private fun setNotificationType(type: Notification.NotificationType,
username: String,
textView: TextView
){
val context = textView.context
val (format: String, drawable: Drawable?) = when(type) {
Notification.NotificationType.follow -> {
getStringAndDrawable(context, R.string.followed_notification, R.drawable.ic_follow)
}
Notification.NotificationType.mention -> {
getStringAndDrawable(context, R.string.mention_notification, R.drawable.mention_at_24dp)
}
Notification.NotificationType.reblog -> {
getStringAndDrawable(context, R.string.shared_notification, R.drawable.ic_reblog_blue)
}
Notification.NotificationType.favourite -> {
getStringAndDrawable(context, R.string.liked_notification, R.drawable.ic_like_full)
}
Notification.NotificationType.poll -> {
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
}
}
textView.text = format.format(username)
textView.setCompoundDrawablesWithIntrinsicBounds(
drawable,null,null,null
)
}
private fun getStringAndDrawable(context: Context, stringToFormat: Int, drawable: Int): Pair<String, Drawable?>
= Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
fun bind(notification: Notification?, api: PixelfedAPI, accessToken: String) {
this.notification = notification
Glide.with(itemView).load(notification?.account?.avatar_static).circleCrop().into(avatar)
val previewUrl = notification?.status?.media_attachments?.getOrNull(0)?.preview_url
if(!previewUrl.isNullOrBlank()){
Glide.with(itemView).load(previewUrl)
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
} else{
photoThumbnail.visibility = View.GONE
}
notification?.type?.let { notification.account?.username?.let { username -> setNotificationType(it, username, notificationType) } }
notification?.created_at?.let { setTextViewFromISO8601(it, notificationTime, false, itemView.context) }
//Convert HTML to clickable text
postDescription.text =
parseHTMLText(
notification?.status?.content ?: "",
notification?.status?.mentions,
api,
itemView.context,
"Bearer $accessToken"
textView.text = format.format(username)
textView.setCompoundDrawablesWithIntrinsicBounds(
drawable, null, null, null
)
}
companion object {
fun create(parent: ViewGroup): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_notifications, parent, false)
return NotificationViewHolder(view)
}
}
}
private fun getStringAndDrawable(
context: Context,
stringToFormat: Int,
drawable: Int
): Pair<String, Drawable?> =
Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
class NotificationsAdapter(private val apiHolder: PixelfedAPIHolder, private val db: AppDatabase) : PagingDataAdapter<Notification, RecyclerView.ViewHolder>(
UIMODEL_COMPARATOR
) {
fun bind(
notification: Notification?,
api: PixelfedAPI,
accessToken: String,
lifecycleScope: LifecycleCoroutineScope
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return NotificationViewHolder.create(parent)
}
this.notification = notification
override fun getItemViewType(position: Int): Int {
return R.layout.fragment_notifications
}
Glide.with(itemView).load(notification?.account?.avatar_static).circleCrop()
.into(avatar)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
(holder as NotificationViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db.userDao().getActiveUser()!!.accessToken)
val previewUrl = notification?.status?.media_attachments?.getOrNull(0)?.preview_url
if (!previewUrl.isNullOrBlank()) {
Glide.with(itemView).load(previewUrl)
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
} else {
photoThumbnail.visibility = View.GONE
}
notification?.type?.let {
notification.account?.username?.let { username ->
setNotificationType(
it,
username,
notificationType
)
}
}
notification?.created_at?.let {
setTextViewFromISO8601(
it,
notificationTime,
false,
itemView.context
)
}
//Convert HTML to clickable text
postDescription.text =
parseHTMLText(
notification?.status?.content ?: "",
notification?.status?.mentions,
api,
itemView.context,
"Bearer $accessToken",
lifecycleScope
)
}
companion object {
fun create(parent: ViewGroup): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_notifications, parent, false)
return NotificationViewHolder(view)
}
}
}
companion object {
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Notification>() {
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
inner class NotificationsAdapter(
private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase
) : PagingDataAdapter<Notification, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Notification>() {
override fun areItemsTheSame(
oldItem: Notification,
newItem: Notification
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean =
override fun areContentsTheSame(
oldItem: Notification,
newItem: Notification
): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return NotificationViewHolder.create(parent)
}
override fun getItemViewType(position: Int): Int {
return R.layout.fragment_notifications
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
(holder as NotificationViewHolder).bind(
it,
apiHolder.setDomainToCurrentUser(db),
db.userDao().getActiveUser()!!.accessToken,
lifecycleScope
)
}
}
}
}

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
@ -109,23 +107,17 @@ class ProfileActivity : BaseActivity() {
}
private fun getAndSetAccount(id: String){
pixelfedAPI.getAccount("Bearer $accessToken", id)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
val account = response.body()!!
setContent(account)
} else {
showError()
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("ProfileActivity:", t.toString())
showError()
}
})
lifecycleScope.launchWhenCreated {
val account = try{
pixelfedAPI.getAccount("Bearer $accessToken", id)
} catch (exception: IOException) {
Log.e("ProfileActivity:", exception.toString())
return@launchWhenCreated showError()
} catch (exception: HttpException) {
return@launchWhenCreated showError()
}
setContent(account)
}
}
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
@ -153,7 +145,8 @@ class ProfileActivity : BaseActivity() {
val description = findViewById<TextView>(R.id.descriptionTextView)
description.text = parseHTMLText(
account.note ?: "", emptyList(), pixelfedAPI,
applicationContext, "Bearer $accessToken"
applicationContext, "Bearer $accessToken",
lifecycleScope
)
val accountName = findViewById<TextView>(R.id.accountNameTextView)
@ -244,40 +237,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 +274,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 +300,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

@ -12,6 +12,7 @@ import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -32,7 +33,9 @@ import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
* This fragment lets you search and use Pixelfed's Discover feature
@ -107,25 +110,17 @@ class SearchDiscoverFragment : BaseFragment() {
private fun getDiscover() {
api.discover("Bearer $accessToken")
.enqueue(object : Callback<DiscoverPosts> {
override fun onFailure(call: Call<DiscoverPosts>, t: Throwable) {
showError()
Log.e("SearchDiscoverFragment:", t.toString())
}
override fun onResponse(call: Call<DiscoverPosts>, response: Response<DiscoverPosts>) {
if(response.code() == 200) {
val discoverPosts = response.body()!!
adapter.addPosts(discoverPosts.posts)
showError(show = false)
}
else {
showError()
}
}
})
lifecycleScope.launchWhenCreated {
try {
val discoverPosts = api.discover("Bearer $accessToken")
adapter.addPosts(discoverPosts.posts)
showError(show = false)
} catch (exception: IOException) {
showError()
} catch (exception: HttpException) {
showError()
}
}
}
/**

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,66 +54,61 @@ 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(
suspend fun likePost(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Path("id") statusId: String
) : Call<Status>
) : Status
@POST("/api/v1/statuses/{id}/unfavourite")
fun unlikePost(
suspend fun unlikePost(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Path("id") statusId: String
) : Call<Status>
@GET("/api/v1/statuses/{id}/favourited_by")
fun postLikedBy(
@Path("id") statusId: String
) : Call<List<Account>>
) : Status
//Used in our case to post a comment or a status
@FormUrlEncoded
@POST("/api/v1/statuses")
fun postStatus(
suspend fun postStatus(
//The authorization header needs to be of the form "Bearer <token>"
@Header("Authorization") authorization: String,
@Field("status") statusText : String,
@ -127,7 +123,7 @@ interface PixelfedAPI {
@Field("visibility") visibility : String = "public",
@Field("scheduled_at") scheduled_at : String? = null,
@Field("language") language : String? = null
) : Call<Status>
) : Status
@DELETE("/api/v1/statuses/{id}")
suspend fun deleteStatus(
@ -137,24 +133,24 @@ interface PixelfedAPI {
@FormUrlEncoded
@POST("/api/v1/statuses/{id}/reblog")
fun reblogStatus(
suspend fun reblogStatus(
@Header("Authorization") authorization: String,
@Path("id") statusId: String,
@Field("visibility") visibility: String? = null
) : Call<Status>
) : Status
@POST("/api/v1/statuses/{id}/unreblog")
fun undoReblogStatus(
suspend fun undoReblogStatus(
@Path("id") statusId: String,
@Header("Authorization") authorization: String
) : Call<Status>
) : Status
//Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context")
fun statusComments(
suspend fun statusComments(
@Path("id") statusId: String,
@Header("Authorization") authorization: String? = null
) : Call<Context>
) : Context
@GET("/api/v1/timelines/public")
suspend fun timelinePublic(
@ -205,10 +201,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 +214,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(
@ -243,10 +240,10 @@ interface PixelfedAPI {
) : Response<List<Account>>
@GET("/api/v1/accounts/{id}")
fun getAccount(
suspend fun getAccount(
@Header("Authorization") authorization: String,
@Path("id") accountId : String
): Call<Account>
): Account
@GET("/api/v1/statuses/{id}")
suspend fun getStatus(
@ -264,19 +261,19 @@ interface PixelfedAPI {
// get discover
@GET("/api/v2/discover/posts")
fun discover(
suspend fun discover(
@Header("Authorization") authorization: String
) : Call<DiscoverPosts>
) : DiscoverPosts
@FormUrlEncoded
@POST("/api/v1/reports")
@JvmSuppressWildcards
fun report(
suspend fun report(
@Header("Authorization") authorization: String,
@Field("account_id") account_id: String,
@Field("status_ids") status_ids: List<Status>,
@Field("comment") comment: String,
@Field("forward") forward: Boolean = true
) : Call<Report>
) : Report
}

View File

@ -6,9 +6,14 @@ import android.util.Log
import androidx.core.content.ContextCompat.startActivity
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
import java.io.Serializable
/*
@ -52,28 +57,19 @@ data class Account(
/**
* @brief Opens an activity of the profile with the given id
*/
fun getAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
Log.e("ACCOUNT_ID", id)
api.getAccount(credential, id).enqueue( object : Callback<Account> {
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("GET ACCOUNT ERROR", t.toString())
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
val account = try {
api.getAccount(credential, id)
} catch (exception: IOException) {
Log.e("GET ACCOUNT ERROR", exception.toString())
return
} catch (exception: HttpException) {
Log.e("ERROR CODE", exception.code().toString())
return
}
//Open the account page in a separate activity
account.openProfile(context)
override fun onResponse(
call: Call<Account>,
response: Response<Account>
) {
if(response.code() == 200) {
val account = response.body()!!
//Open the account page in a separate activity
account.openProfile(context)
} else {
Log.e("ERROR CODE", response.code().toString())
}
}
})
}
}

View File

@ -77,11 +77,6 @@ open class Status(
fun getProfilePicUrl() : String? = account?.avatar
fun getPostPreviewURL() : String? = media_attachments?.firstOrNull()?.preview_url
/**
* @brief returns the parsed version of the HTML description
*/
private fun getDescription(api: PixelfedAPI, context: Context, credential: String) : Spanned =
parseHTMLText(content ?: "", mentions, api, context, credential)
fun getNLikes(context: Context) : CharSequence {
return context.getString(R.string.likes).format(favourites_count.toString())

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>
@ -44,7 +45,7 @@
<string name="request_format_error">Upload error: bad request format</string>
<string name="upload_post_failed">Post upload failed</string>
<string name="upload_post_success">Post uploaded successfully</string>
<string name="upload_post_error">Post upload failed</string>
<string name="upload_post_error">Post upload error</string>
<string name="description">Description…</string>
<string name="post">post</string>
<string name="add_photo">Add a photo</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)