mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05: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) {
|
if (hexEncodedSeed == null) {
|
||||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appContext = applicationContext
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
val loadFileContents: (String) -> String = { fileName ->
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
MnemonicUtilities.loadFileContents(appContext, fileName)
|
||||||
}
|
}
|
||||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||||
}
|
}
|
||||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
|||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return cursor == null ? 0 : cursor.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
public ThreadRecord getNext() {
|
public ThreadRecord getNext() {
|
||||||
if (cursor == null || !cursor.moveToNext())
|
if (cursor == null || !cursor.moveToNext())
|
||||||
return null;
|
return null;
|
||||||
|
@ -24,7 +24,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
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.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.themeState
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -99,7 +100,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
private var broadcastReceiver: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||||
@ -205,18 +205,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
startObservingUpdates()
|
|
||||||
|
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||||
|
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||||
|
}
|
||||||
|
|
||||||
// Set up new conversation button
|
// Set up new conversation button
|
||||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||||
// Observe blocked contacts changed events
|
// 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
|
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||||
lifecycleScope.launch {
|
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 {
|
lifecycleScope.launchWhenStarted {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
// Double check that the long poller is up
|
// Double check that the long poller is up
|
||||||
@ -385,52 +402,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
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() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||||
|
|
||||||
homeViewModel.getObservable(this).removeObservers(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
val broadcastReceiver = this.broadcastReceiver
|
|
||||||
if (broadcastReceiver != null) {
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
EventBus.getDefault().unregister(this)
|
EventBus.getDefault().unregister(this)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// 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() {
|
private fun updateEmptyState() {
|
||||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||||
|
@ -26,16 +26,15 @@ class HomeAdapter(
|
|||||||
|
|
||||||
var header: View? = null
|
var header: View? = null
|
||||||
|
|
||||||
private var _data: List<ThreadRecord> = emptyList()
|
var data: List<ThreadRecord> = emptyList()
|
||||||
var data: List<ThreadRecord>
|
|
||||||
get() = _data.toList()
|
|
||||||
set(newData) {
|
set(newData) {
|
||||||
val previousData = _data.toList()
|
if (field !== newData) {
|
||||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||||
val diffResult = DiffUtil.calculateDiff(diff)
|
val diffResult = DiffUtil.calculateDiff(diff)
|
||||||
_data = newData
|
field = newData
|
||||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun hasHeaderView(): Boolean = header != null
|
fun hasHeaderView(): Boolean = header != null
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ class HomeAdapter(
|
|||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
if (hasHeaderView() && position == 0) return NO_ID
|
if (hasHeaderView() && position == 0) return NO_ID
|
||||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||||
return _data[offsetPosition].threadId
|
return data[offsetPosition].threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
|
@ -1,80 +1,67 @@
|
|||||||
package org.thoughtcrime.securesms.home
|
package org.thoughtcrime.securesms.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.cash.copper.flow.observeQuery
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.*
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.onEach
|
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.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import java.lang.ref.WeakReference
|
import org.thoughtcrime.securesms.util.observeChanges
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
class HomeViewModel @Inject constructor(
|
||||||
|
private val threadDb: ThreadDatabase,
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
@ApplicationContext appContext: Context,
|
||||||
private var lastContext: WeakReference<Context>? = null
|
) : ViewModel() {
|
||||||
private val updateJobs: MutableList<Job> = mutableListOf()
|
// SharedFlow that emits whenever the user asks us to reload the conversation
|
||||||
|
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
||||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
extraBufferCapacity = 1,
|
||||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
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 ->
|
threadDb.approvedConversationList.use { openCursor ->
|
||||||
val reader = threadDb.readerFor(openCursor)
|
val reader = threadDb.readerFor(openCursor)
|
||||||
val threads = mutableListOf<ThreadRecord>()
|
buildList(reader.length) {
|
||||||
while (true) {
|
while (true) {
|
||||||
threads += reader.next ?: break
|
add(reader.next ?: break)
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
_conversations.value = threads
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
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