migrate drafts to paging 3 (#2206)

* migrate drafts to paging 3

* migrate DraftHelper to coroutines
This commit is contained in:
Konrad Pozniak 2021-06-24 21:23:29 +02:00 committed by GitHub
parent 063dc49d41
commit f6dd131b95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 357 additions and 337 deletions

View file

@ -35,6 +35,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback import androidx.emoji.text.EmojiCompat.InitCallback
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
@ -61,7 +62,6 @@ import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
@ -84,6 +84,7 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
@ -614,29 +615,35 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.setTitle(R.string.action_logout) .setTitle(R.string.action_logout)
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) lifecycleScope.launch {
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
cacheUpdater.clearForUser(activeAccount.id) cacheUpdater.clearForUser(activeAccount.id)
conversationRepository.deleteCacheForAccount(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id)
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
removeShortcut(this, activeAccount) removeShortcut(this@MainActivity, activeAccount)
val newAccount = accountManager.logActiveAccountOut() val newAccount = accountManager.logActiveAccountOut()
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) { if (!NotificationHelper.areNotificationsEnabled(
NotificationHelper.disablePullNotifications(this) this@MainActivity,
accountManager
)
) {
NotificationHelper.disablePullNotifications(this@MainActivity)
} }
val intent = if (newAccount == null) { val intent = if (newAccount == null) {
LoginActivity.getIntent(this, false) LoginActivity.getIntent(this@MainActivity, false)
} else { } else {
Intent(this, MainActivity::class.java) Intent(this@MainActivity, MainActivity::class.java)
} }
startActivity(intent) startActivity(intent)
finishWithoutSlideOutAnimation() finishWithoutSlideOutAnimation()
} }
}
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
} }
private fun fetchUserInfo() { private fun fetchUserInfo() {
mastodonApi.accountVerifyCredentials() mastodonApi.accountVerifyCredentials()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY) .autoDispose(this, Lifecycle.Event.ON_DESTROY)
@ -648,9 +655,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Log.e(TAG, "Failed to fetch user info. " + throwable.message) Log.e(TAG, "Failed to fetch user info. " + throwable.message)
} }
) )
} }
private fun onFetchUserInfoSuccess(me: Account) { private fun onFetchUserInfoSuccess(me: Account) {
glide.asBitmap() glide.asBitmap()
.load(me.header) .load(me.header)
.into(header.accountHeaderBackground) .into(header.accountHeaderBackground)
@ -664,9 +671,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
updateProfiles() updateProfiles()
updateShortcut(this, accountManager.activeAccount!!) updateShortcut(this, accountManager.activeAccount!!)
} }
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
glide.asDrawable() glide.asDrawable()
@ -697,9 +704,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
} }
}) })
} }
private fun fetchAnnouncements() { private fun fetchAnnouncements() {
mastodonApi.listAnnouncements(false) mastodonApi.listAnnouncements(false)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY) .autoDispose(this, Lifecycle.Event.ON_DESTROY)
@ -712,13 +719,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Log.w(TAG, "Failed to fetch announcements.", it) Log.w(TAG, "Failed to fetch announcements.", it)
} }
) )
} }
private fun updateAnnouncementsBadge() { private fun updateAnnouncementsBadge() {
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
} }
private fun updateProfiles() { private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc -> val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
@ -743,18 +750,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.clear() header.clear()
header.profiles = profiles header.profiles = profiles
header.setActiveProfile(accountManager.activeAccount!!.id) header.setActiveProfile(accountManager.activeAccount!!.id)
} }
override fun getActionButton() = binding.composeButton override fun getActionButton() = binding.composeButton
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector
companion object { companion object {
private const val TAG = "MainActivity" // logging tag private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val STATUS_URL = "statusUrl" const val STATUS_URL = "statusUrl"
} }
} }
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {

View file

@ -21,6 +21,7 @@ import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.components.search.SearchType
@ -36,6 +37,7 @@ import com.keylesspalace.tusky.util.*
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.launch
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -214,14 +216,15 @@ class ComposeViewModel @Inject constructor(
} }
fun deleteDraft() { fun deleteDraft() {
viewModelScope.launch {
if (draftId != 0) { if (draftId != 0) {
draftHelper.deleteDraftAndAttachments(draftId) draftHelper.deleteDraftAndAttachments(draftId)
.subscribe() }
} }
} }
fun saveDraft(content: String, contentWarning: String) { fun saveDraft(content: String, contentWarning: String) {
viewModelScope.launch {
val mediaUris: MutableList<String> = mutableListOf() val mediaUris: MutableList<String> = mutableListOf()
val mediaDescriptions: MutableList<String?> = mutableListOf() val mediaDescriptions: MutableList<String?> = mutableListOf()
media.value?.forEach { item -> media.value?.forEach { item ->
@ -241,7 +244,8 @@ class ComposeViewModel @Inject constructor(
mediaDescriptions = mediaDescriptions, mediaDescriptions = mediaDescriptions,
poll = poll.value, poll = poll.value,
failedToSend = false failedToSend = false
).subscribe() )
}
} }
/** /**

View file

@ -28,13 +28,12 @@ import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.IOUtils import com.keylesspalace.tusky.util.IOUtils
import io.reactivex.rxjava3.core.Completable import kotlinx.coroutines.Dispatchers
import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.withContext
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class DraftHelper @Inject constructor( class DraftHelper @Inject constructor(
@ -44,7 +43,7 @@ class DraftHelper @Inject constructor(
private val draftDao = db.draftDao() private val draftDao = db.draftDao()
fun saveDraft( suspend fun saveDraft(
draftId: Int, draftId: Int,
accountId: Long, accountId: Long,
inReplyToId: String?, inReplyToId: String?,
@ -56,9 +55,7 @@ class DraftHelper @Inject constructor(
mediaDescriptions: List<String?>, mediaDescriptions: List<String?>,
poll: NewPoll?, poll: NewPoll?,
failedToSend: Boolean failedToSend: Boolean
): Completable { ) = withContext(Dispatchers.IO) {
return Single.fromCallable {
val externalFilesDir = context.getExternalFilesDir("Tusky") val externalFilesDir = context.getExternalFilesDir("Tusky")
if (externalFilesDir == null || !(externalFilesDir.exists())) { if (externalFilesDir == null || !(externalFilesDir.exists())) {
@ -103,7 +100,7 @@ class DraftHelper @Inject constructor(
) )
} }
DraftEntity( val draft = DraftEntity(
id = draftId, id = draftId,
accountId = accountId, accountId = accountId,
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
@ -116,42 +113,34 @@ class DraftHelper @Inject constructor(
failedToSend = failedToSend failedToSend = failedToSend
) )
}.flatMapCompletable { draft ->
draftDao.insertOrReplace(draft) draftDao.insertOrReplace(draft)
}.subscribeOn(Schedulers.io())
} }
fun deleteDraftAndAttachments(draftId: Int): Completable { suspend fun deleteDraftAndAttachments(draftId: Int) {
return draftDao.find(draftId) draftDao.find(draftId)?.let { draft ->
.flatMapCompletable { draft ->
draft?.let {
deleteDraftAndAttachments(it)
}
}
}
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
return deleteAttachments(draft)
.andThen(draftDao.delete(draft.id))
}
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
draftDao.loadDraftsSingle(accountId)
.flatMapObservable { Observable.fromIterable(it) }
.flatMapCompletable { draft ->
deleteDraftAndAttachments(draft) deleteDraftAndAttachments(draft)
}.subscribeOn(Schedulers.io()) }
.subscribe()
} }
fun deleteAttachments(draft: DraftEntity): Completable { suspend fun deleteDraftAndAttachments(draft: DraftEntity) {
return Completable.fromCallable { deleteAttachments(draft)
draftDao.delete(draft.id)
}
suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
draftDao.loadDrafts(accountId).forEach { draft ->
deleteDraftAndAttachments(draft)
}
}
suspend fun deleteAttachments(draft: DraftEntity) {
withContext(Dispatchers.IO) {
draft.attachments.forEach { attachment -> draft.attachments.forEach { attachment ->
if (context.contentResolver.delete(attachment.uri, null, null) == 0) { if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
} }
} }
}.subscribeOn(Schedulers.io()) }
} }
private fun Uri.isNotInFolder(folder: File): Boolean { private fun Uri.isNotInFolder(folder: File): Boolean {

View file

@ -22,6 +22,7 @@ import android.util.Log
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
@ -34,9 +35,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.util.show
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
@ -51,7 +53,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout> private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityDraftsBinding.inflate(layoutInflater) binding = ActivityDraftsBinding.inflate(layoutInflater)
@ -74,16 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
viewModel.drafts.observe(this) { draftList -> lifecycleScope.launch {
if (draftList.isEmpty()) { viewModel.drafts.collectLatest { draftData ->
binding.draftsRecyclerView.hide() adapter.submitData(draftData)
binding.draftsErrorMessageView.show()
} else {
binding.draftsRecyclerView.show()
binding.draftsErrorMessageView.hide()
adapter.submitList(draftList)
} }
} }
adapter.addLoadStateListener {
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
}
} }
override fun onOpenDraft(draft: DraftEntity) { override fun onOpenDraft(draft: DraftEntity) {

View file

@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.paging.PagedListAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -35,7 +35,7 @@ interface DraftActionListener {
class DraftsAdapter( class DraftsAdapter(
private val listener: DraftActionListener private val listener: DraftActionListener
) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>( ) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
object : DiffUtil.ItemCallback<DraftEntity>() { object : DiffUtil.ItemCallback<DraftEntity>() {
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
@ -87,6 +87,5 @@ class DraftsAdapter(
holder.binding.draftPoll.hide() holder.binding.draftPoll.hide()
} }
} }
} }
} }

View file

@ -16,13 +16,17 @@
package com.keylesspalace.tusky.components.drafts package com.keylesspalace.tusky.components.drafts
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.paging.toLiveData import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class DraftsViewModel @Inject constructor( class DraftsViewModel @Inject constructor(
@ -32,32 +36,39 @@ class DraftsViewModel @Inject constructor(
val draftHelper: DraftHelper val draftHelper: DraftHelper
) : ViewModel() { ) : ViewModel() {
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20) val drafts = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) }
).flow
.cachedIn(viewModelScope)
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf() private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
fun deleteDraft(draft: DraftEntity) { fun deleteDraft(draft: DraftEntity) {
// this does not immediately delete media files to avoid unnecessary file operations // this does not immediately delete media files to avoid unnecessary file operations
// in case the user decides to restore the draft // in case the user decides to restore the draft
viewModelScope.launch {
database.draftDao().delete(draft.id) database.draftDao().delete(draft.id)
.subscribe()
deletedDrafts.add(draft) deletedDrafts.add(draft)
} }
}
fun restoreDraft(draft: DraftEntity) { fun restoreDraft(draft: DraftEntity) {
viewModelScope.launch {
database.draftDao().insertOrReplace(draft) database.draftDao().insertOrReplace(draft)
.subscribe()
deletedDrafts.remove(draft) deletedDrafts.remove(draft)
} }
}
fun getToot(tootId: String): Single<Status> { fun getToot(tootId: String): Single<Status> {
return api.status(tootId) return api.status(tootId)
} }
override fun onCleared() { override fun onCleared() {
viewModelScope.launch {
deletedDrafts.forEach { deletedDrafts.forEach {
draftHelper.deleteAttachments(it).subscribe() draftHelper.deleteAttachments(it)
}
} }
} }
} }

View file

@ -15,30 +15,28 @@
package com.keylesspalace.tusky.db package com.keylesspalace.tusky.db
import androidx.paging.DataSource import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
@Dao @Dao
interface DraftDao { interface DraftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(draft: DraftEntity): Completable suspend fun insertOrReplace(draft: DraftEntity)
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
fun loadDrafts(accountId: Long): DataSource.Factory<Int, DraftEntity> fun draftsPagingSource(accountId: Long): PagingSource<Int, DraftEntity>
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId")
fun loadDraftsSingle(accountId: Long): Single<List<DraftEntity>> suspend fun loadDrafts(accountId: Long): List<DraftEntity>
@Query("DELETE FROM DraftEntity WHERE id = :id") @Query("DELETE FROM DraftEntity WHERE id = :id")
fun delete(id: Int): Completable suspend fun delete(id: Int)
@Query("SELECT * FROM DraftEntity WHERE id = :id") @Query("SELECT * FROM DraftEntity WHERE id = :id")
fun find(id: Int): Single<DraftEntity?> suspend fun find(id: Int): DraftEntity?
} }

View file

@ -27,6 +27,10 @@ import com.keylesspalace.tusky.entity.NewStatus
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -49,6 +53,9 @@ class SendTootService : Service(), Injectable {
@Inject @Inject
lateinit var draftHelper: DraftHelper lateinit var draftHelper: DraftHelper
private val supervisorJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>() private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>() private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
@ -148,7 +155,6 @@ class SendTootService : Service(), Injectable {
newStatus newStatus
) )
sendCalls[tootId] = sendCall sendCalls[tootId] = sendCall
val callback = object : Callback<Status> { val callback = object : Callback<Status> {
@ -160,8 +166,9 @@ class SendTootService : Service(), Injectable {
if (response.isSuccessful) { if (response.isSuccessful) {
// If the status was loaded from a draft, delete the draft and associated media files. // If the status was loaded from a draft, delete the draft and associated media files.
if (tootToSend.draftId != 0) { if (tootToSend.draftId != 0) {
serviceScope.launch {
draftHelper.deleteDraftAndAttachments(tootToSend.draftId) draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
.subscribe() }
} }
if (scheduled) { if (scheduled) {
@ -244,7 +251,7 @@ class SendTootService : Service(), Injectable {
} }
private fun saveTootToDrafts(toot: TootToSend) { private fun saveTootToDrafts(toot: TootToSend) {
serviceScope.launch {
draftHelper.saveDraft( draftHelper.saveDraft(
draftId = toot.draftId, draftId = toot.draftId,
accountId = toot.accountId, accountId = toot.accountId,
@ -257,7 +264,8 @@ class SendTootService : Service(), Injectable {
mediaDescriptions = toot.mediaDescriptions, mediaDescriptions = toot.mediaDescriptions,
poll = toot.poll, poll = toot.poll,
failedToSend = true failedToSend = true
).subscribe() )
}
} }
private fun cancelSendingIntent(tootId: Int): PendingIntent { private fun cancelSendingIntent(tootId: Int): PendingIntent {
@ -269,6 +277,10 @@ class SendTootService : Service(), Injectable {
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
override fun onDestroy() {
super.onDestroy()
supervisorJob.cancel()
}
companion object { companion object {