mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Tidy up
This commit is contained in:
parent
c7c0519a20
commit
90f0caebbd
@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (hexEncodedSeed == null) {
|
||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
|
||||
val appContext = applicationContext
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
MnemonicUtilities.loadFileContents(appContext, fileName)
|
||||
}
|
||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||
}
|
||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return cursor == null ? 0 : cursor.getCount();
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
if (cursor == null || !cursor.moveToNext())
|
||||
return null;
|
||||
|
@ -24,7 +24,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -81,7 +83,6 @@ import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import org.thoughtcrime.securesms.util.themeState
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
@ -99,7 +100,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
private lateinit var glide: GlideRequests
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||
@ -205,18 +205,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Set up empty state view
|
||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
startObservingUpdates()
|
||||
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||
}
|
||||
|
||||
// Set up new conversation button
|
||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||
// Observe blocked contacts changed events
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
||||
|
||||
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||
lifecycleScope.launch {
|
||||
@ -227,6 +223,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to threads and update the UI
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
homeViewModel.threads
|
||||
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
|
||||
.collectLatest { threads ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||
val offsetTop = if(firstPos >= 0) {
|
||||
manager.findViewByPosition(firstPos)?.let { view ->
|
||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||
} ?: 0
|
||||
} else 0
|
||||
homeAdapter.data = threads
|
||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launchWhenStarted {
|
||||
launch(Dispatchers.IO) {
|
||||
// Double check that the long poller is up
|
||||
@ -385,52 +402,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
||||
}
|
||||
}
|
||||
|
||||
// If the theme hasn't changed then start observing updates again (if it does change then we
|
||||
// will recreate the activity resulting in it responding to changes multiple times)
|
||||
if (currentThemeState == textSecurePreferences.themeState() && !homeViewModel.getObservable(this).hasActiveObservers()) {
|
||||
startObservingUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||
|
||||
homeViewModel.getObservable(this).removeObservers(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val broadcastReceiver = this.broadcastReceiver
|
||||
if (broadcastReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
super.onDestroy()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun startObservingUpdates() {
|
||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||
val offsetTop = if(firstPos >= 0) {
|
||||
manager.findViewByPosition(firstPos)?.let { view ->
|
||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||
} ?: 0
|
||||
} else 0
|
||||
homeAdapter.data = newData
|
||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEmptyState() {
|
||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||
|
@ -26,16 +26,15 @@ class HomeAdapter(
|
||||
|
||||
var header: View? = null
|
||||
|
||||
private var _data: List<ThreadRecord> = emptyList()
|
||||
var data: List<ThreadRecord>
|
||||
get() = _data.toList()
|
||||
var data: List<ThreadRecord> = emptyList()
|
||||
set(newData) {
|
||||
val previousData = _data.toList()
|
||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
||||
if (field !== newData) {
|
||||
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||
val diffResult = DiffUtil.calculateDiff(diff)
|
||||
_data = newData
|
||||
field = newData
|
||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasHeaderView(): Boolean = header != null
|
||||
|
||||
@ -61,7 +60,7 @@ class HomeAdapter(
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (hasHeaderView() && position == 0) return NO_ID
|
||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||
return _data[offsetPosition].threadId
|
||||
return data[offsetPosition].threadId
|
||||
}
|
||||
|
||||
lateinit var glide: GlideRequests
|
||||
|
@ -1,80 +1,67 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import java.lang.ref.WeakReference
|
||||
import org.thoughtcrime.securesms.util.observeChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
private var lastContext: WeakReference<Context>? = null
|
||||
private val updateJobs: MutableList<Job> = mutableListOf()
|
||||
|
||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
||||
|
||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
for (job in updateJobs) {
|
||||
job.cancel()
|
||||
}
|
||||
updateJobs.clear()
|
||||
}
|
||||
|
||||
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
|
||||
// If the context has changed (eg. the activity gets recreated) then
|
||||
// we need to cancel the old executors and recreate them to prevent
|
||||
// the app from triggering extra updates when data changes
|
||||
if (context != lastContext?.get()) {
|
||||
lastContext = WeakReference(context)
|
||||
updateJobs.forEach { it.cancel() }
|
||||
updateJobs.clear()
|
||||
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
context.contentResolver
|
||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
||||
.onEach { listUpdateChannel.trySend(Unit) }
|
||||
.collect()
|
||||
}
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val threadDb: ThreadDatabase,
|
||||
@ApplicationContext appContext: Context,
|
||||
) : ViewModel() {
|
||||
// SharedFlow that emits whenever the user asks us to reload the conversation
|
||||
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
for (update in listUpdateChannel) {
|
||||
|
||||
/**
|
||||
* A [StateFlow] that emits the list of threads in the conversation list.
|
||||
*
|
||||
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||
* whenever the conversation list changes.
|
||||
*/
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
val threads: StateFlow<List<ThreadRecord>?> = merge(
|
||||
manualReloadTrigger,
|
||||
appContext.contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI))
|
||||
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
|
||||
.onStart { emit(Unit) }
|
||||
.mapLatest { _ ->
|
||||
withContext(Dispatchers.IO) {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<ThreadRecord>()
|
||||
buildList(reader.length) {
|
||||
while (true) {
|
||||
threads += reader.next ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_conversations.value = threads
|
||||
add(reader.next ?: break)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return conversations
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
fun tryUpdateChannel() = manualReloadTrigger.tryEmit(Unit)
|
||||
|
||||
companion object {
|
||||
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.CheckResult
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
/**
|
||||
* Observe changes to a content URI. This function will emit the URI whenever the content or
|
||||
* its descendants change, according to the parameter [notifyForDescendants].
|
||||
*/
|
||||
@CheckResult
|
||||
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
|
||||
return callbackFlow {
|
||||
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
trySend(uri)
|
||||
}
|
||||
}
|
||||
|
||||
registerContentObserver(uri, notifyForDescendants, observer)
|
||||
awaitClose {
|
||||
unregisterContentObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user