migrate drafts to paging 3 (#2206)
* migrate drafts to paging 3 * migrate DraftHelper to coroutines
This commit is contained in:
parent
063dc49d41
commit
f6dd131b95
8 changed files with 357 additions and 337 deletions
|
@ -35,6 +35,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
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.db.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
|
@ -84,6 +84,7 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
||||
|
@ -218,18 +219,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
NotificationHelper.disablePullNotifications(this)
|
||||
}
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event: Event? ->
|
||||
when (event) {
|
||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||
is MainTabsChangedEvent -> setupTabs(false)
|
||||
is AnnouncementReadEvent -> {
|
||||
unreadAnnouncementsCount--
|
||||
updateAnnouncementsBadge()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event: Event? ->
|
||||
when (event) {
|
||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||
is MainTabsChangedEvent -> setupTabs(false)
|
||||
is AnnouncementReadEvent -> {
|
||||
unreadAnnouncementsCount--
|
||||
updateAnnouncementsBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Schedulers.io().scheduleDirect {
|
||||
// Flush old media that was cached for sharing
|
||||
|
@ -341,13 +342,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
if (animateAvatars) {
|
||||
glide.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
} else {
|
||||
glide.asBitmap()
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,114 +368,114 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
binding.mainDrawer.apply {
|
||||
tintStatusBar = true
|
||||
addItems(
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_edit_profile
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
onClick = {
|
||||
val intent = Intent(context, EditProfileActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_favourites
|
||||
isSelectable = false
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newFavouritesIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_bookmarks
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newBookmarksIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_follow_requests
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||
onClick = {
|
||||
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListsActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_drafts
|
||||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivity.newIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_toot
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
identifier = DRAWER_ITEM_ANNOUNCEMENTS
|
||||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_account_preferences
|
||||
iconRes = R.drawable.ic_account_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_preferences
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.about_title_activity
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
val intent = Intent(context, AboutActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_logout
|
||||
iconRes = R.drawable.ic_logout
|
||||
onClick = ::logout
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_edit_profile
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
onClick = {
|
||||
val intent = Intent(context, EditProfileActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_favourites
|
||||
isSelectable = false
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newFavouritesIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_bookmarks
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newBookmarksIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_follow_requests
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||
onClick = {
|
||||
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListsActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_drafts
|
||||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivity.newIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_toot
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
identifier = DRAWER_ITEM_ANNOUNCEMENTS
|
||||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_account_preferences
|
||||
iconRes = R.drawable.ic_account_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_preferences
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.about_title_activity
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
val intent = Intent(context, AboutActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_logout
|
||||
iconRes = R.drawable.ic_logout
|
||||
onClick = ::logout
|
||||
}
|
||||
)
|
||||
|
||||
if (addSearchButton) {
|
||||
binding.mainDrawer.addItemsAtPosition(4,
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||
}
|
||||
})
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setSavedInstance(savedInstanceState)
|
||||
|
@ -482,11 +483,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
if (BuildConfig.DEBUG) {
|
||||
binding.mainDrawer.addItems(
|
||||
secondaryDrawerItem {
|
||||
nameText = "debug"
|
||||
isEnabled = false
|
||||
textColor = ColorStateList.valueOf(Color.GREEN)
|
||||
}
|
||||
secondaryDrawerItem {
|
||||
nameText = "debug"
|
||||
isEnabled = false
|
||||
textColor = ColorStateList.valueOf(Color.GREEN)
|
||||
}
|
||||
)
|
||||
}
|
||||
EmojiCompat.get().registerInitCallback(emojiInitCallback)
|
||||
|
@ -519,7 +520,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
activeTabLayout.removeAllTabs()
|
||||
for (i in tabs.indices) {
|
||||
val tab = activeTabLayout.newTab()
|
||||
.setIcon(tabs[i].icon)
|
||||
.setIcon(tabs[i].icon)
|
||||
if (tabs[i].id == LIST) {
|
||||
tab.contentDescription = tabs[i].arguments[1]
|
||||
} else {
|
||||
|
@ -611,168 +612,174 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private fun logout() {
|
||||
accountManager.activeAccount?.let { activeAccount ->
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.action_logout)
|
||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
|
||||
.setTitle(R.string.action_logout)
|
||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch {
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||
cacheUpdater.clearForUser(activeAccount.id)
|
||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
||||
removeShortcut(this, activeAccount)
|
||||
removeShortcut(this@MainActivity, activeAccount)
|
||||
val newAccount = accountManager.logActiveAccountOut()
|
||||
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
NotificationHelper.disablePullNotifications(this)
|
||||
if (!NotificationHelper.areNotificationsEnabled(
|
||||
this@MainActivity,
|
||||
accountManager
|
||||
)
|
||||
) {
|
||||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||
}
|
||||
val intent = if (newAccount == null) {
|
||||
LoginActivity.getIntent(this, false)
|
||||
LoginActivity.getIntent(this@MainActivity, false)
|
||||
} else {
|
||||
Intent(this, MainActivity::class.java)
|
||||
Intent(this@MainActivity, MainActivity::class.java)
|
||||
}
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchUserInfo() {
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchUserInfoSuccess(me: Account) {
|
||||
glide.asBitmap()
|
||||
.load(me.header)
|
||||
.into(header.accountHeaderBackground)
|
||||
|
||||
loadDrawerAvatar(me.avatar, false)
|
||||
|
||||
accountManager.updateActiveAccount(me)
|
||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
||||
|
||||
accountLocked = me.locked
|
||||
|
||||
updateProfiles()
|
||||
updateShortcut(this, accountManager.activeAccount!!)
|
||||
}
|
||||
|
||||
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
|
||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||
|
||||
glide.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchUserInfo() {
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
private fun onFetchUserInfoSuccess(me: Account) {
|
||||
glide.asBitmap()
|
||||
.load(me.header)
|
||||
.into(header.accountHeaderBackground)
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
loadDrawerAvatar(me.avatar, false)
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
accountManager.updateActiveAccount(me)
|
||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
||||
|
||||
private fun fetchAnnouncements() {
|
||||
mastodonApi.listAnnouncements(false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ announcements ->
|
||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||
updateAnnouncementsBadge()
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
accountLocked = me.locked
|
||||
|
||||
private fun updateAnnouncementsBadge() {
|
||||
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
|
||||
}
|
||||
updateProfiles()
|
||||
updateShortcut(this, accountManager.activeAccount!!)
|
||||
}
|
||||
|
||||
private fun updateProfiles() {
|
||||
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
||||
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
|
||||
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
|
||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = acc.isActive
|
||||
nameText = emojifiedName
|
||||
iconUrl = acc.profilePictureUrl
|
||||
isNameShown = true
|
||||
identifier = acc.id
|
||||
descriptionText = acc.fullName
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
for (profile in header.profiles.orEmpty()) {
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
profiles.add(profile)
|
||||
break
|
||||
glide.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
}
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun fetchAnnouncements() {
|
||||
mastodonApi.listAnnouncements(false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ announcements ->
|
||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||
updateAnnouncementsBadge()
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateAnnouncementsBadge() {
|
||||
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
|
||||
}
|
||||
|
||||
private fun updateProfiles() {
|
||||
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
||||
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
|
||||
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = acc.isActive
|
||||
nameText = emojifiedName
|
||||
iconUrl = acc.profilePictureUrl
|
||||
isNameShown = true
|
||||
identifier = acc.id
|
||||
descriptionText = acc.fullName
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
for (profile in header.profiles.orEmpty()) {
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
profiles.add(profile)
|
||||
break
|
||||
}
|
||||
}
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
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 {
|
||||
private const val TAG = "MainActivity" // logging tag
|
||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||
const val STATUS_URL = "statusUrl"
|
||||
}
|
||||
companion object {
|
||||
private const val TAG = "MainActivity" // logging tag
|
||||
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
|
||||
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
|
||||
const val STATUS_URL = "statusUrl"
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
return PrimaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private inline fun secondaryDrawerItem(block: SecondaryDrawerItem.() -> Unit): SecondaryDrawerItem {
|
||||
return SecondaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private var AbstractDrawerItem<*, *>.onClick: () -> Unit
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.net.toUri
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
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.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -214,22 +216,23 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
if (draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(draftId)
|
||||
.subscribe()
|
||||
viewModelScope.launch {
|
||||
if (draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(draftId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(content: String, contentWarning: String) {
|
||||
viewModelScope.launch {
|
||||
val mediaUris: MutableList<String> = mutableListOf()
|
||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||
media.value?.forEach { item ->
|
||||
mediaUris.add(item.uri.toString())
|
||||
mediaDescriptions.add(item.description)
|
||||
}
|
||||
|
||||
val mediaUris: MutableList<String> = mutableListOf()
|
||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||
media.value?.forEach { item ->
|
||||
mediaUris.add(item.uri.toString())
|
||||
mediaDescriptions.add(item.description)
|
||||
}
|
||||
|
||||
draftHelper.saveDraft(
|
||||
draftHelper.saveDraft(
|
||||
draftId = draftId,
|
||||
accountId = accountManager.activeAccount?.id!!,
|
||||
inReplyToId = inReplyToId,
|
||||
|
@ -241,7 +244,8 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaDescriptions = mediaDescriptions,
|
||||
poll = poll.value,
|
||||
failedToSend = false
|
||||
).subscribe()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,13 +28,12 @@ import com.keylesspalace.tusky.db.DraftEntity
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftHelper @Inject constructor(
|
||||
|
@ -44,7 +43,7 @@ class DraftHelper @Inject constructor(
|
|||
|
||||
private val draftDao = db.draftDao()
|
||||
|
||||
fun saveDraft(
|
||||
suspend fun saveDraft(
|
||||
draftId: Int,
|
||||
accountId: Long,
|
||||
inReplyToId: String?,
|
||||
|
@ -56,9 +55,7 @@ class DraftHelper @Inject constructor(
|
|||
mediaDescriptions: List<String?>,
|
||||
poll: NewPoll?,
|
||||
failedToSend: Boolean
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||
|
||||
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
||||
|
@ -103,7 +100,7 @@ class DraftHelper @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
DraftEntity(
|
||||
val draft = DraftEntity(
|
||||
id = draftId,
|
||||
accountId = accountId,
|
||||
inReplyToId = inReplyToId,
|
||||
|
@ -116,42 +113,34 @@ class DraftHelper @Inject constructor(
|
|||
failedToSend = failedToSend
|
||||
)
|
||||
|
||||
}.flatMapCompletable { draft ->
|
||||
draftDao.insertOrReplace(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draftId: Int): Completable {
|
||||
return draftDao.find(draftId)
|
||||
.flatMapCompletable { draft ->
|
||||
draft?.let {
|
||||
deleteDraftAndAttachments(it)
|
||||
}
|
||||
}
|
||||
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
||||
draftDao.find(draftId)?.let { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
|
||||
return deleteAttachments(draft)
|
||||
.andThen(draftDao.delete(draft.id))
|
||||
suspend fun deleteDraftAndAttachments(draft: DraftEntity) {
|
||||
deleteAttachments(draft)
|
||||
draftDao.delete(draft.id)
|
||||
}
|
||||
|
||||
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDraftsSingle(accountId)
|
||||
.flatMapObservable { Observable.fromIterable(it) }
|
||||
.flatMapCompletable { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDrafts(accountId).forEach { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAttachments(draft: DraftEntity): Completable {
|
||||
return Completable.fromCallable {
|
||||
suspend fun deleteAttachments(draft: DraftEntity) {
|
||||
withContext(Dispatchers.IO) {
|
||||
draft.attachments.forEach { attachment ->
|
||||
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.isNotInFolder(folder: File): Boolean {
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.util.Log
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.db.DraftEntity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -51,7 +53,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
||||
|
@ -74,16 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||
|
||||
viewModel.drafts.observe(this) { draftList ->
|
||||
if (draftList.isEmpty()) {
|
||||
binding.draftsRecyclerView.hide()
|
||||
binding.draftsErrorMessageView.show()
|
||||
} else {
|
||||
binding.draftsRecyclerView.show()
|
||||
binding.draftsErrorMessageView.hide()
|
||||
adapter.submitList(draftList)
|
||||
lifecycleScope.launch {
|
||||
viewModel.drafts.collectLatest { draftData ->
|
||||
adapter.submitData(draftData)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.addLoadStateListener {
|
||||
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenDraft(draft: DraftEntity) {
|
||||
|
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -35,7 +35,7 @@ interface DraftActionListener {
|
|||
|
||||
class DraftsAdapter(
|
||||
private val listener: DraftActionListener
|
||||
) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
|
@ -87,6 +87,5 @@ class DraftsAdapter(
|
|||
holder.binding.draftPoll.hide()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,17 @@
|
|||
package com.keylesspalace.tusky.components.drafts
|
||||
|
||||
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.AppDatabase
|
||||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftsViewModel @Inject constructor(
|
||||
|
@ -32,22 +36,28 @@ class DraftsViewModel @Inject constructor(
|
|||
val draftHelper: DraftHelper
|
||||
) : 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()
|
||||
|
||||
fun deleteDraft(draft: DraftEntity) {
|
||||
// this does not immediately delete media files to avoid unnecessary file operations
|
||||
// in case the user decides to restore the draft
|
||||
database.draftDao().delete(draft.id)
|
||||
.subscribe()
|
||||
deletedDrafts.add(draft)
|
||||
viewModelScope.launch {
|
||||
database.draftDao().delete(draft.id)
|
||||
deletedDrafts.add(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreDraft(draft: DraftEntity) {
|
||||
database.draftDao().insertOrReplace(draft)
|
||||
.subscribe()
|
||||
deletedDrafts.remove(draft)
|
||||
viewModelScope.launch {
|
||||
database.draftDao().insertOrReplace(draft)
|
||||
deletedDrafts.remove(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun getToot(tootId: String): Single<Status> {
|
||||
|
@ -55,9 +65,10 @@ class DraftsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCleared() {
|
||||
deletedDrafts.forEach {
|
||||
draftHelper.deleteAttachments(it).subscribe()
|
||||
viewModelScope.launch {
|
||||
deletedDrafts.forEach {
|
||||
draftHelper.deleteAttachments(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,30 +15,28 @@
|
|||
|
||||
package com.keylesspalace.tusky.db
|
||||
|
||||
import androidx.paging.DataSource
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
||||
@Dao
|
||||
interface DraftDao {
|
||||
|
||||
@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")
|
||||
fun loadDrafts(accountId: Long): DataSource.Factory<Int, DraftEntity>
|
||||
fun draftsPagingSource(accountId: Long): PagingSource<Int, DraftEntity>
|
||||
|
||||
@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")
|
||||
fun delete(id: Int): Completable
|
||||
suspend fun delete(id: Int)
|
||||
|
||||
@Query("SELECT * FROM DraftEntity WHERE id = :id")
|
||||
fun find(id: Int): Single<DraftEntity?>
|
||||
suspend fun find(id: Int): DraftEntity?
|
||||
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ import com.keylesspalace.tusky.entity.NewStatus
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
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 retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
@ -49,6 +53,9 @@ class SendTootService : Service(), Injectable {
|
|||
@Inject
|
||||
lateinit var draftHelper: DraftHelper
|
||||
|
||||
private val supervisorJob = SupervisorJob()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
||||
|
||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||
|
||||
|
@ -148,7 +155,6 @@ class SendTootService : Service(), Injectable {
|
|||
newStatus
|
||||
)
|
||||
|
||||
|
||||
sendCalls[tootId] = sendCall
|
||||
|
||||
val callback = object : Callback<Status> {
|
||||
|
@ -160,8 +166,9 @@ class SendTootService : Service(), Injectable {
|
|||
if (response.isSuccessful) {
|
||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||
if (tootToSend.draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
|
||||
.subscribe()
|
||||
serviceScope.launch {
|
||||
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduled) {
|
||||
|
@ -244,8 +251,8 @@ class SendTootService : Service(), Injectable {
|
|||
}
|
||||
|
||||
private fun saveTootToDrafts(toot: TootToSend) {
|
||||
|
||||
draftHelper.saveDraft(
|
||||
serviceScope.launch {
|
||||
draftHelper.saveDraft(
|
||||
draftId = toot.draftId,
|
||||
accountId = toot.accountId,
|
||||
inReplyToId = toot.inReplyToId,
|
||||
|
@ -257,7 +264,8 @@ class SendTootService : Service(), Injectable {
|
|||
mediaDescriptions = toot.mediaDescriptions,
|
||||
poll = toot.poll,
|
||||
failedToSend = true
|
||||
).subscribe()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
||||
|
@ -269,6 +277,10 @@ class SendTootService : Service(), Injectable {
|
|||
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
supervisorJob.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
|
|
Loading…
Reference in a new issue