Fix crash after resuming app

This commit is contained in:
kyori19 2023-07-16 20:14:22 +09:00
parent 4f705ae074
commit d365caf301
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()
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
// 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
@ -732,15 +724,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
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) {
val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") {
val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize)
@ -794,12 +777,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
if (data.id == LIST) {
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) {
menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true
}
@ -813,7 +790,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setDrawableTint(this, item.icon!!, android.R.attr.textColorPrimary)
}
}
tintCheckIcon(menuBuilder.findItem(R.id.tabToggleStreaming))
}
popup.setOnMenuItemClickListener { item ->
@ -837,20 +813,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
data.arguments.getOrNull(1).orEmpty()
).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 -> {
if (fragment is NotificationsFragment) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
@ -915,6 +877,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
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) {

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.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.components.trending.TrendingFragment
import net.accelf.yuito.streaming.StreamType
import net.accelf.yuito.streaming.Subscription
import java.util.Objects
/** 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 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 {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@ -212,6 +212,14 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
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) {
val transition = MaterialContainerTransform().apply {
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.MotionEvent
import android.view.ViewGroup
import android.widget.CompoundButton.OnCheckedChangeListener
import androidx.core.view.size
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.android.material.chip.Chip
import com.keylesspalace.tusky.FEDERATED
import com.keylesspalace.tusky.HASHTAG
import com.keylesspalace.tusky.HOME
import com.keylesspalace.tusky.LIST
import com.keylesspalace.tusky.LOCAL
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding
@ -40,6 +44,7 @@ interface ItemInteractionListener {
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
fun onActionChipClicked(tab: TabData, tabPosition: Int)
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
fun onStreamingChanged(tab: TabData, tabPosition: Int, enabled: Boolean)
}
class TabAdapter(
@ -146,6 +151,18 @@ class TabAdapter(
} else {
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 PinEvent(val statusId: String, val pinned: Boolean) : 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 kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import net.accelf.yuito.streaming.StreamingManager
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -103,9 +102,6 @@ class TimelineFragment :
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var streamingManager: StreamingManager
private val viewModel: TimelineViewModel by unsafeLazy {
if (kind == TimelineViewModel.Kind.HOME) {
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
@ -180,10 +176,8 @@ class TimelineFragment :
kind,
id,
tags,
arguments.getBoolean(ARG_ENABLE_STREAMING),
)
if (arguments.getBoolean(ARG_ENABLE_STREAMING)) {
setStreamingEnabled(true)
}
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
@ -428,23 +422,6 @@ class TimelineFragment :
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() {
binding.statusView.hide()

View File

@ -283,15 +283,15 @@ class CachedTimelineViewModel @Inject constructor(
// handled by CacheUpdater
}
override fun handleStreamUpdateEvent(status: Status) {
override fun handleStreamUpdateEvent(status: Status, streamId: Int) {
viewModelScope.launch {
val timelineDao = db.timelineDao()
val activeAccount = accountManager.activeAccount!!
db.withTransaction {
if (isFirstOfStreaming) {
if (streamId != currentStreamId) {
timelineDao.insertStatus(Placeholder(status.id, loading = false).toEntity(activeAccount.id))
isFirstOfStreaming = false
currentStreamId = streamId
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 {
val activeAccount = accountManager.activeAccount!!
if (isFirstOfStreaming) {
if (streamId != currentStreamId) {
statusData.add(0, StatusViewData.Placeholder(status.id, isLoading = false))
isFirstOfStreaming = false
currentStreamId = streamId
} else {
statusData.add(
0,

View File

@ -73,6 +73,7 @@ abstract class TimelineViewModel(
private set
var tags: List<String> = emptyList()
private set
private var isStreamingEnabled = false
protected var alwaysShowSensitiveMedia = false
private var alwaysOpenSpoilers = false
@ -97,7 +98,7 @@ abstract class TimelineViewModel(
}
}
var isFirstOfStreaming = false
var currentStreamId: Int = 0
val subscription by lazy {
when (kind) {
Kind.HOME -> Subscription(StreamType.USER)
@ -115,16 +116,17 @@ abstract class TimelineViewModel(
}
}
}
var isStreamingEnabled = false
fun init(
kind: Kind,
id: String?,
tags: List<String>,
isStreamingEnabled: Boolean,
) {
this.kind = kind
this.id = id
this.tags = tags
this.isStreamingEnabled = isStreamingEnabled
filterModel.kind = kind.toFilterKind()
if (kind == Kind.HOME) {
@ -219,7 +221,7 @@ abstract class TimelineViewModel(
abstract fun handlePinEvent(pinEvent: PinEvent)
abstract fun handleStreamUpdateEvent(status: Status)
abstract fun handleStreamUpdateEvent(status: Status, streamId: Int)
abstract fun fullReload()
@ -286,7 +288,7 @@ abstract class TimelineViewModel(
is PinEvent -> handlePinEvent(event)
is StreamUpdateEvent -> {
if (isStreamingEnabled && event.subscription == subscription) {
handleStreamUpdateEvent(event.status)
handleStreamUpdateEvent(event.status, event.streamId)
}
}
is MuteConversationEvent -> fullReload()

View File

@ -56,15 +56,12 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesBaseActivity(): BaseActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesMainActivity(): MainActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesAccountActivity(): AccountActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesListsActivity(): ListsActivity
@ -74,19 +71,15 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesEditProfileActivity(): EditProfileActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesAccountListActivity(): AccountListActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesViewThreadActivity(): ViewThreadActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesStatusListActivity(): StatusListActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesSearchActivity(): SearchActivity
@ -99,7 +92,6 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesPreferencesActivity(): PreferencesActivity
@ -118,11 +110,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesReportActivity(): ReportActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesInstanceListActivity(): InstanceListActivity
@ -138,7 +128,6 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesSplashActivity(): SplashActivity
@ActivityScope
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
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.network.MastodonApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.accelf.yuito.streaming.SubscribeRequest.RequestType.SUBSCRIBE
import net.accelf.yuito.streaming.SubscribeRequest.RequestType.UNSUBSCRIBE
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import kotlin.coroutines.CoroutineContext
class MastodonStream(
parent: Job,
coroutineScope: CoroutineScope,
private val okHttpClient: OkHttpClient,
private val gson: Gson,
private val eventHub: EventHub,
private val onStatusChange: (Boolean) -> Unit,
) : WebSocketListener(), CoroutineScope {
) : WebSocketListener(), CoroutineScope by coroutineScope {
private var webSocket: WebSocket? = null
private val subscribing = mutableSetOf<Subscription>()
private val job = SupervisorJob(parent).apply {
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() {
fun openSocket(subscriptions: Set<Subscription>) {
val request = Request.Builder().url(STREAMING_URL).build()
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 = null
onStatusChange(false)
}
private fun send(subscribeRequest: SubscribeRequest) {
@ -100,7 +57,7 @@ class MastodonStream(
StreamEvent.EventType.UPDATE -> {
val status = gson.fromJson(payload, Status::class.java)
launch {
eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream)))
eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream), this.hashCode()))
}
}
StreamEvent.EventType.DELETE -> launch {

View File

@ -1,30 +1,39 @@
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.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.di.ActivityScope
import kotlinx.coroutines.Job
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Singleton
@ActivityScope
@Singleton
class StreamingManager @Inject constructor(
private val eventHub: EventHub,
private val okHttpClient: OkHttpClient,
private val gson: Gson,
) {
private lateinit var stream: MastodonStream
private var stream: MastodonStream? = null
fun setup(parent: Job, onStatusChange: (Boolean) -> Unit) {
stream = MastodonStream(parent, okHttpClient, gson, eventHub, onStatusChange)
}
fun subscribe(subscription: Subscription) {
stream.subscribe(subscription)
}
fun unsubscribe(subscription: Subscription) {
stream.unsubscribe(subscription)
fun setup(owner: LifecycleOwner, subscriptions: Set<Subscription>, onStatusChange: (Boolean) -> Unit) {
owner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
stream = MastodonStream(owner.lifecycleScope, okHttpClient, gson, eventHub)
stream?.openSocket(subscriptions)
onStatusChange(true)
}
Lifecycle.Event.ON_PAUSE -> {
stream?.closeSocket()
stream = null
onStatusChange(false)
}
else -> {}
}
})
}
}

View File

@ -59,7 +59,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintBottom_toTopOf="@id/switchStreaming">
<com.google.android.material.chip.Chip
android:id="@+id/actionChip"
@ -73,4 +73,15 @@
</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>

View File

@ -15,11 +15,6 @@
android:title="@string/action_tab_edit_list"
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"
android:icon="@drawable/ic_notifications_24dp"
android:title="@string/action_tab_toggle_notifications_filter"