Fix crash after resuming app

This commit is contained in:
kyori19 2023-07-16 20:14:22 +09:00
parent 4f705ae074
commit 9968b561c4
No known key found for this signature in database
GPG Key ID: F7BDE7DD42BF366A
15 changed files with 110 additions and 163 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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()
}
} }
} }

View File

@ -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

View File

@ -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()

View File

@ -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
} }

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -1,7 +0,0 @@
package com.keylesspalace.tusky.di
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

View File

@ -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 {

View File

@ -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 -> {}
}
})
} }
} }

View File

@ -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>

View File

@ -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"