Fix crash after resuming app
This commit is contained in:
parent
4f705ae074
commit
9968b561c4
|
@ -301,14 +301,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
fetchAnnouncements()
|
fetchAnnouncements()
|
||||||
|
|
||||||
streamingManager.setup(lifecycleScope.coroutineContext.job) { active ->
|
|
||||||
if (active) {
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
} else {
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise the tab adapter and set to viewpager. Fragments appear to be leaked if the
|
// Initialise the tab adapter and set to viewpager. Fragments appear to be leaked if the
|
||||||
// adapter changes over the life of the viewPager (the adapter, not its contents), so set
|
// adapter changes over the life of the viewPager (the adapter, not its contents), so set
|
||||||
// the initial list of tabs to empty, and set the full list later in setupTabs(). See
|
// the initial list of tabs to empty, and set the full list later in setupTabs(). See
|
||||||
|
@ -732,15 +724,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState))
|
super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tintCheckIcon(item: MenuItem) {
|
|
||||||
if (item.isChecked) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
item.icon?.setColorFilter(ContextCompat.getColor(this, R.color.tusky_green_light), PorterDuff.Mode.SRC_IN)
|
|
||||||
} else {
|
|
||||||
setDrawableTint(this, item.icon!!, android.R.attr.textColorTertiary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupTabs(selectNotificationTab: Boolean) {
|
private fun setupTabs(selectNotificationTab: Boolean) {
|
||||||
val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") {
|
val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") {
|
||||||
val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize)
|
val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize)
|
||||||
|
@ -794,12 +777,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
if (data.id == LIST) {
|
if (data.id == LIST) {
|
||||||
menuBuilder.findItem(R.id.tabEditList).isVisible = true
|
menuBuilder.findItem(R.id.tabEditList).isVisible = true
|
||||||
}
|
}
|
||||||
if (data.id in arrayOf(HOME, LOCAL, FEDERATED, LIST)) {
|
|
||||||
menuBuilder.findItem(R.id.tabToggleStreaming).apply {
|
|
||||||
isVisible = true
|
|
||||||
isChecked = data.enableStreaming
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.id == NOTIFICATIONS) {
|
if (data.id == NOTIFICATIONS) {
|
||||||
menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true
|
menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true
|
||||||
}
|
}
|
||||||
|
@ -813,7 +790,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
setDrawableTint(this, item.icon!!, android.R.attr.textColorPrimary)
|
setDrawableTint(this, item.icon!!, android.R.attr.textColorPrimary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tintCheckIcon(menuBuilder.findItem(R.id.tabToggleStreaming))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
popup.setOnMenuItemClickListener { item ->
|
||||||
|
@ -837,20 +813,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
data.arguments.getOrNull(1).orEmpty()
|
data.arguments.getOrNull(1).orEmpty()
|
||||||
).show(supportFragmentManager, null)
|
).show(supportFragmentManager, null)
|
||||||
}
|
}
|
||||||
R.id.tabToggleStreaming -> {
|
|
||||||
if (fragment is TimelineFragment) {
|
|
||||||
val to = !item.isChecked
|
|
||||||
fragment.setStreamingEnabled(to)
|
|
||||||
item.isChecked = to
|
|
||||||
tintCheckIcon(item)
|
|
||||||
|
|
||||||
tabs[position] = data.copy(enableStreaming = to)
|
|
||||||
accountManager.activeAccount?.let {
|
|
||||||
it.tabPreferences = tabs
|
|
||||||
accountManager.saveAccount(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.tabToggleNotificationsFilter -> {
|
R.id.tabToggleNotificationsFilter -> {
|
||||||
if (fragment is NotificationsFragment) {
|
if (fragment is NotificationsFragment) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
@ -915,6 +877,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProfiles()
|
updateProfiles()
|
||||||
|
|
||||||
|
streamingManager.setup(
|
||||||
|
this,
|
||||||
|
tabs.mapNotNull { it.subscription }.toSet(),
|
||||||
|
) { active ->
|
||||||
|
if (active) {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
} else {
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshComposeButtonState(adapter: MainPagerAdapter, tabPosition: Int) {
|
private fun refreshComposeButtonState(adapter: MainPagerAdapter, tabPosition: Int) {
|
||||||
|
|
|
@ -24,6 +24,8 @@ import com.keylesspalace.tusky.components.notifications.NotificationsFragment
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||||
import com.keylesspalace.tusky.components.trending.TrendingFragment
|
import com.keylesspalace.tusky.components.trending.TrendingFragment
|
||||||
|
import net.accelf.yuito.streaming.StreamType
|
||||||
|
import net.accelf.yuito.streaming.Subscription
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
|
||||||
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
||||||
|
@ -48,6 +50,20 @@ data class TabData(
|
||||||
val title: (Context) -> String = { context -> context.getString(text) },
|
val title: (Context) -> String = { context -> context.getString(text) },
|
||||||
val enableStreaming: Boolean = false,
|
val enableStreaming: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val subscription by lazy {
|
||||||
|
if (enableStreaming) {
|
||||||
|
when (id) {
|
||||||
|
HOME -> Subscription(StreamType.USER)
|
||||||
|
LOCAL -> Subscription(StreamType.LOCAL)
|
||||||
|
FEDERATED -> Subscription(StreamType.PUBLIC)
|
||||||
|
LIST -> Subscription(StreamType.LIST, arguments[0].toInt())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
|
@ -212,6 +212,14 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
currentTabsAdapter.notifyItemChanged(tabPosition)
|
currentTabsAdapter.notifyItemChanged(tabPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStreamingChanged(tab: TabData, tabPosition: Int, enabled: Boolean) {
|
||||||
|
val newTab = tab.copy(enableStreaming = enabled)
|
||||||
|
currentTabs[tabPosition] = newTab
|
||||||
|
saveTabs()
|
||||||
|
|
||||||
|
currentTabsAdapter.notifyItemChanged(tabPosition)
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleFab(expand: Boolean) {
|
private fun toggleFab(expand: Boolean) {
|
||||||
val transition = MaterialContainerTransform().apply {
|
val transition = MaterialContainerTransform().apply {
|
||||||
startView = if (expand) binding.actionButton else binding.sheet
|
startView = if (expand) binding.actionButton else binding.sheet
|
||||||
|
|
|
@ -18,12 +18,16 @@ package com.keylesspalace.tusky.adapter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.keylesspalace.tusky.FEDERATED
|
||||||
import com.keylesspalace.tusky.HASHTAG
|
import com.keylesspalace.tusky.HASHTAG
|
||||||
|
import com.keylesspalace.tusky.HOME
|
||||||
import com.keylesspalace.tusky.LIST
|
import com.keylesspalace.tusky.LIST
|
||||||
|
import com.keylesspalace.tusky.LOCAL
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding
|
import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding
|
||||||
|
@ -40,6 +44,7 @@ interface ItemInteractionListener {
|
||||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
||||||
fun onActionChipClicked(tab: TabData, tabPosition: Int)
|
fun onActionChipClicked(tab: TabData, tabPosition: Int)
|
||||||
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
|
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
|
||||||
|
fun onStreamingChanged(tab: TabData, tabPosition: Int, enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabAdapter(
|
class TabAdapter(
|
||||||
|
@ -146,6 +151,18 @@ class TabAdapter(
|
||||||
} else {
|
} else {
|
||||||
binding.chipGroup.hide()
|
binding.chipGroup.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.id in arrayOf(HOME, LOCAL, FEDERATED, LIST)) {
|
||||||
|
binding.switchStreaming.show()
|
||||||
|
|
||||||
|
binding.switchStreaming.isChecked = tab.enableStreaming
|
||||||
|
binding.switchStreaming.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
listener.onStreamingChanged(tab, holder.bindingAdapterPosition, isChecked)
|
||||||
|
binding.switchStreaming.setOnCheckedChangeListener(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.switchStreaming.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,4 +25,4 @@ data class DomainMuteEvent(val instance: String) : Event
|
||||||
data class AnnouncementReadEvent(val announcementId: String) : Event
|
data class AnnouncementReadEvent(val announcementId: String) : Event
|
||||||
data class PinEvent(val statusId: String, val pinned: Boolean) : Event
|
data class PinEvent(val statusId: String, val pinned: Boolean) : Event
|
||||||
data class QuickReplyEvent(val status: Status) : Event
|
data class QuickReplyEvent(val status: Status) : Event
|
||||||
data class StreamUpdateEvent(val status: Status, val subscription: Subscription) : Event
|
data class StreamUpdateEvent(val status: Status, val subscription: Subscription, val streamId: Int) : Event
|
||||||
|
|
|
@ -82,7 +82,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.accelf.yuito.streaming.StreamingManager
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -103,9 +102,6 @@ class TimelineFragment :
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var streamingManager: StreamingManager
|
|
||||||
|
|
||||||
private val viewModel: TimelineViewModel by unsafeLazy {
|
private val viewModel: TimelineViewModel by unsafeLazy {
|
||||||
if (kind == TimelineViewModel.Kind.HOME) {
|
if (kind == TimelineViewModel.Kind.HOME) {
|
||||||
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||||
|
@ -180,10 +176,8 @@ class TimelineFragment :
|
||||||
kind,
|
kind,
|
||||||
id,
|
id,
|
||||||
tags,
|
tags,
|
||||||
|
arguments.getBoolean(ARG_ENABLE_STREAMING),
|
||||||
)
|
)
|
||||||
if (arguments.getBoolean(ARG_ENABLE_STREAMING)) {
|
|
||||||
setStreamingEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||||
|
|
||||||
|
@ -428,23 +422,6 @@ class TimelineFragment :
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
viewModel.isFirstOfStreaming = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStreamingEnabled(to: Boolean) {
|
|
||||||
viewModel.isStreamingEnabled = to
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
streamingManager.subscribe(viewModel.subscription)
|
|
||||||
viewModel.isFirstOfStreaming = true
|
|
||||||
} else {
|
|
||||||
streamingManager.unsubscribe(viewModel.subscription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
binding.statusView.hide()
|
binding.statusView.hide()
|
||||||
|
|
||||||
|
|
|
@ -283,15 +283,15 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
// handled by CacheUpdater
|
// handled by CacheUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleStreamUpdateEvent(status: Status) {
|
override fun handleStreamUpdateEvent(status: Status, streamId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val timelineDao = db.timelineDao()
|
val timelineDao = db.timelineDao()
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
if (isFirstOfStreaming) {
|
if (streamId != currentStreamId) {
|
||||||
timelineDao.insertStatus(Placeholder(status.id, loading = false).toEntity(activeAccount.id))
|
timelineDao.insertStatus(Placeholder(status.id, loading = false).toEntity(activeAccount.id))
|
||||||
isFirstOfStreaming = false
|
currentStreamId = streamId
|
||||||
return@withTransaction
|
return@withTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -243,13 +243,13 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleStreamUpdateEvent(status: Status) {
|
override fun handleStreamUpdateEvent(status: Status, streamId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
if (isFirstOfStreaming) {
|
if (streamId != currentStreamId) {
|
||||||
statusData.add(0, StatusViewData.Placeholder(status.id, isLoading = false))
|
statusData.add(0, StatusViewData.Placeholder(status.id, isLoading = false))
|
||||||
isFirstOfStreaming = false
|
currentStreamId = streamId
|
||||||
} else {
|
} else {
|
||||||
statusData.add(
|
statusData.add(
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -73,6 +73,7 @@ abstract class TimelineViewModel(
|
||||||
private set
|
private set
|
||||||
var tags: List<String> = emptyList()
|
var tags: List<String> = emptyList()
|
||||||
private set
|
private set
|
||||||
|
private var isStreamingEnabled = false
|
||||||
|
|
||||||
protected var alwaysShowSensitiveMedia = false
|
protected var alwaysShowSensitiveMedia = false
|
||||||
private var alwaysOpenSpoilers = false
|
private var alwaysOpenSpoilers = false
|
||||||
|
@ -97,7 +98,7 @@ abstract class TimelineViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFirstOfStreaming = false
|
var currentStreamId: Int = 0
|
||||||
val subscription by lazy {
|
val subscription by lazy {
|
||||||
when (kind) {
|
when (kind) {
|
||||||
Kind.HOME -> Subscription(StreamType.USER)
|
Kind.HOME -> Subscription(StreamType.USER)
|
||||||
|
@ -115,16 +116,17 @@ abstract class TimelineViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var isStreamingEnabled = false
|
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
id: String?,
|
id: String?,
|
||||||
tags: List<String>,
|
tags: List<String>,
|
||||||
|
isStreamingEnabled: Boolean,
|
||||||
) {
|
) {
|
||||||
this.kind = kind
|
this.kind = kind
|
||||||
this.id = id
|
this.id = id
|
||||||
this.tags = tags
|
this.tags = tags
|
||||||
|
this.isStreamingEnabled = isStreamingEnabled
|
||||||
filterModel.kind = kind.toFilterKind()
|
filterModel.kind = kind.toFilterKind()
|
||||||
|
|
||||||
if (kind == Kind.HOME) {
|
if (kind == Kind.HOME) {
|
||||||
|
@ -219,7 +221,7 @@ abstract class TimelineViewModel(
|
||||||
|
|
||||||
abstract fun handlePinEvent(pinEvent: PinEvent)
|
abstract fun handlePinEvent(pinEvent: PinEvent)
|
||||||
|
|
||||||
abstract fun handleStreamUpdateEvent(status: Status)
|
abstract fun handleStreamUpdateEvent(status: Status, streamId: Int)
|
||||||
|
|
||||||
abstract fun fullReload()
|
abstract fun fullReload()
|
||||||
|
|
||||||
|
@ -286,7 +288,7 @@ abstract class TimelineViewModel(
|
||||||
is PinEvent -> handlePinEvent(event)
|
is PinEvent -> handlePinEvent(event)
|
||||||
is StreamUpdateEvent -> {
|
is StreamUpdateEvent -> {
|
||||||
if (isStreamingEnabled && event.subscription == subscription) {
|
if (isStreamingEnabled && event.subscription == subscription) {
|
||||||
handleStreamUpdateEvent(event.status)
|
handleStreamUpdateEvent(event.status, event.streamId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MuteConversationEvent -> fullReload()
|
is MuteConversationEvent -> fullReload()
|
||||||
|
|
|
@ -56,15 +56,12 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesBaseActivity(): BaseActivity
|
abstract fun contributesBaseActivity(): BaseActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesMainActivity(): MainActivity
|
abstract fun contributesMainActivity(): MainActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesAccountActivity(): AccountActivity
|
abstract fun contributesAccountActivity(): AccountActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesListsActivity(): ListsActivity
|
abstract fun contributesListsActivity(): ListsActivity
|
||||||
|
|
||||||
|
@ -74,19 +71,15 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesEditProfileActivity(): EditProfileActivity
|
abstract fun contributesEditProfileActivity(): EditProfileActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesAccountListActivity(): AccountListActivity
|
abstract fun contributesAccountListActivity(): AccountListActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
abstract fun contributesViewThreadActivity(): ViewThreadActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesStatusListActivity(): StatusListActivity
|
abstract fun contributesStatusListActivity(): StatusListActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesSearchActivity(): SearchActivity
|
abstract fun contributesSearchActivity(): SearchActivity
|
||||||
|
|
||||||
|
@ -99,7 +92,6 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
||||||
|
|
||||||
|
@ -118,11 +110,9 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
|
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesReportActivity(): ReportActivity
|
abstract fun contributesReportActivity(): ReportActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||||
|
|
||||||
|
@ -138,7 +128,6 @@ abstract class ActivitiesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSplashActivity(): SplashActivity
|
abstract fun contributesSplashActivity(): SplashActivity
|
||||||
|
|
||||||
@ActivityScope
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesTrendingActivity(): TrendingActivity
|
abstract fun contributesTrendingActivity(): TrendingActivity
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package com.keylesspalace.tusky.di
|
|
||||||
|
|
||||||
import javax.inject.Scope
|
|
||||||
|
|
||||||
@Scope
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
annotation class ActivityScope
|
|
|
@ -10,79 +10,36 @@ import com.keylesspalace.tusky.entity.Filter
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.accelf.yuito.streaming.SubscribeRequest.RequestType.SUBSCRIBE
|
import net.accelf.yuito.streaming.SubscribeRequest.RequestType.SUBSCRIBE
|
||||||
import net.accelf.yuito.streaming.SubscribeRequest.RequestType.UNSUBSCRIBE
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
class MastodonStream(
|
class MastodonStream(
|
||||||
parent: Job,
|
coroutineScope: CoroutineScope,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val gson: Gson,
|
private val gson: Gson,
|
||||||
private val eventHub: EventHub,
|
private val eventHub: EventHub,
|
||||||
private val onStatusChange: (Boolean) -> Unit,
|
) : WebSocketListener(), CoroutineScope by coroutineScope {
|
||||||
) : WebSocketListener(), CoroutineScope {
|
|
||||||
|
|
||||||
private var webSocket: WebSocket? = null
|
private var webSocket: WebSocket? = null
|
||||||
private val subscribing = mutableSetOf<Subscription>()
|
|
||||||
|
|
||||||
private val job = SupervisorJob(parent).apply {
|
fun openSocket(subscriptions: Set<Subscription>) {
|
||||||
invokeOnCompletion {
|
|
||||||
webSocket?.let {
|
|
||||||
closeSocket()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override val coroutineContext: CoroutineContext
|
|
||||||
get() = job
|
|
||||||
|
|
||||||
fun subscribe(subscription: Subscription) {
|
|
||||||
if (!subscribing.add(subscription)) {
|
|
||||||
// already subscribed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webSocket == null) {
|
|
||||||
openSocket()
|
|
||||||
}
|
|
||||||
|
|
||||||
send(SubscribeRequest.fromSubscription(SUBSCRIBE, subscription))
|
|
||||||
Log.d(TAG, "Subscribed $subscription")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unsubscribe(subscription: Subscription) {
|
|
||||||
if (!subscribing.remove(subscription)) {
|
|
||||||
// already unsubscribed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscribing.isEmpty()) {
|
|
||||||
closeSocket()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
send(SubscribeRequest.fromSubscription(UNSUBSCRIBE, subscription))
|
|
||||||
Log.d(TAG, "Unsubscribed $subscription")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openSocket() {
|
|
||||||
val request = Request.Builder().url(STREAMING_URL).build()
|
val request = Request.Builder().url(STREAMING_URL).build()
|
||||||
webSocket = okHttpClient.newWebSocket(request, this)
|
webSocket = okHttpClient.newWebSocket(request, this)
|
||||||
onStatusChange(true)
|
subscriptions.forEach {
|
||||||
|
send(SubscribeRequest.fromSubscription(SUBSCRIBE, it))
|
||||||
|
Log.d(TAG, "Subscribed $it")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun closeSocket() {
|
fun closeSocket() {
|
||||||
webSocket!!.close(1000, null)
|
webSocket!!.close(1000, null)
|
||||||
webSocket = null
|
webSocket = null
|
||||||
onStatusChange(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(subscribeRequest: SubscribeRequest) {
|
private fun send(subscribeRequest: SubscribeRequest) {
|
||||||
|
@ -100,7 +57,7 @@ class MastodonStream(
|
||||||
StreamEvent.EventType.UPDATE -> {
|
StreamEvent.EventType.UPDATE -> {
|
||||||
val status = gson.fromJson(payload, Status::class.java)
|
val status = gson.fromJson(payload, Status::class.java)
|
||||||
launch {
|
launch {
|
||||||
eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream)))
|
eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream), this@MastodonStream.hashCode()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StreamEvent.EventType.DELETE -> launch {
|
StreamEvent.EventType.DELETE -> launch {
|
||||||
|
|
|
@ -1,30 +1,39 @@
|
||||||
package net.accelf.yuito.streaming
|
package net.accelf.yuito.streaming
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.di.ActivityScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ActivityScope
|
@Singleton
|
||||||
class StreamingManager @Inject constructor(
|
class StreamingManager @Inject constructor(
|
||||||
private val eventHub: EventHub,
|
private val eventHub: EventHub,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val gson: Gson,
|
private val gson: Gson,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private lateinit var stream: MastodonStream
|
private var stream: MastodonStream? = null
|
||||||
|
|
||||||
fun setup(parent: Job, onStatusChange: (Boolean) -> Unit) {
|
fun setup(owner: LifecycleOwner, subscriptions: Set<Subscription>, onStatusChange: (Boolean) -> Unit) {
|
||||||
stream = MastodonStream(parent, okHttpClient, gson, eventHub, onStatusChange)
|
owner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||||
}
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
fun subscribe(subscription: Subscription) {
|
stream = MastodonStream(owner.lifecycleScope, okHttpClient, gson, eventHub)
|
||||||
stream.subscribe(subscription)
|
stream?.openSocket(subscriptions)
|
||||||
}
|
onStatusChange(true)
|
||||||
|
}
|
||||||
fun unsubscribe(subscription: Subscription) {
|
Lifecycle.Event.ON_PAUSE -> {
|
||||||
stream.unsubscribe(subscription)
|
stream?.closeSocket()
|
||||||
|
stream = null
|
||||||
|
onStatusChange(false)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
app:layout_constraintBottom_toTopOf="@id/switchStreaming">
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/actionChip"
|
android:id="@+id/actionChip"
|
||||||
|
@ -73,4 +73,15 @@
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switchStreaming"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:text="@string/action_tab_toggle_streaming"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -15,11 +15,6 @@
|
||||||
android:title="@string/action_tab_edit_list"
|
android:title="@string/action_tab_edit_list"
|
||||||
android:visible="false" />
|
android:visible="false" />
|
||||||
|
|
||||||
<item android:id="@+id/tabToggleStreaming"
|
|
||||||
android:icon="@drawable/ic_check_24dp"
|
|
||||||
android:title="@string/action_tab_toggle_streaming"
|
|
||||||
android:visible="false" />
|
|
||||||
|
|
||||||
<item android:id="@+id/tabToggleNotificationsFilter"
|
<item android:id="@+id/tabToggleNotificationsFilter"
|
||||||
android:icon="@drawable/ic_notifications_24dp"
|
android:icon="@drawable/ic_notifications_24dp"
|
||||||
android:title="@string/action_tab_toggle_notifications_filter"
|
android:title="@string/action_tab_toggle_notifications_filter"
|
||||||
|
|
Loading…
Reference in New Issue