mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge pull request #1493 from simophin/ses-1936-oom
[SES-1936] Fix memory leaks
This commit is contained in:
commit
b757691334
@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.attachment = attachment;
|
||||
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
||||
this.port = socket.getLocalPort();
|
||||
|
@ -122,7 +122,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
|
||||
glide.clear(imageView)
|
||||
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.load(signalProfilePicture)
|
||||
|
@ -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)
|
||||
}
|
||||
@ -831,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun onDestroy() {
|
||||
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
||||
cancelVoiceMessage()
|
||||
tearDownRecipientObserver()
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
@ -1019,7 +1022,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun showVoiceMessageUI() {
|
||||
binding?.inputBarRecordingView?.show()
|
||||
binding?.inputBarRecordingView?.show(lifecycleScope)
|
||||
binding?.inputBar?.alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
animation.duration = 250L
|
||||
|
@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
|
||||
import android.animation.IntEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
@ -14,6 +12,11 @@ import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
@ -25,10 +28,10 @@ import java.util.Date
|
||||
class InputBarRecordingView : RelativeLayout {
|
||||
private lateinit var binding: ViewInputBarRecordingBinding
|
||||
private var startTimestamp = 0L
|
||||
private val snHandler = Handler(Looper.getMainLooper())
|
||||
private var dotViewAnimation: ValueAnimator? = null
|
||||
private var pulseAnimation: ValueAnimator? = null
|
||||
var delegate: InputBarRecordingViewDelegate? = null
|
||||
private var timerJob: Job? = null
|
||||
|
||||
val lockView: LinearLayout
|
||||
get() = binding.lockView
|
||||
@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
|
||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.inputBarMiddleContentContainer.disableClipping()
|
||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||
|
||||
}
|
||||
|
||||
fun show() {
|
||||
fun show(scope: CoroutineScope) {
|
||||
startTimestamp = Date().time
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
binding.inputBarCancelButton.alpha = 0.0f
|
||||
@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
|
||||
animateDotView()
|
||||
pulse()
|
||||
animateLockViewUp()
|
||||
updateTimer()
|
||||
startTimer(scope)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
|
||||
}
|
||||
animation.start()
|
||||
delegate?.handleVoiceMessageUIHidden()
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
private fun startTimer(scope: CoroutineScope) {
|
||||
timerJob?.cancel()
|
||||
timerJob = scope.launch {
|
||||
while (isActive) {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
private fun animateDotView() {
|
||||
@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun updateTimer() {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
snHandler.postDelayed({ updateTimer() }, 500)
|
||||
}
|
||||
|
||||
fun lock() {
|
||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
fadeOutAnimation.duration = 250L
|
||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return cursor == null ? 0 : cursor.getCount();
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
if (cursor == null || !cursor.moveToNext())
|
||||
return null;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
@ -8,7 +9,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||
|
||||
class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: PlaceholderAvatarPhoto,
|
||||
@ -16,14 +17,14 @@ class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawa
|
||||
height: Int,
|
||||
options: Options
|
||||
): LoadData<BitmapDrawable> {
|
||||
return LoadData(model, PlaceholderAvatarFetcher(model.context, model))
|
||||
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
|
||||
}
|
||||
|
||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||
|
||||
class Factory() : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
class Factory(private val appContext: Context) : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
return PlaceholderAvatarLoader()
|
||||
return PlaceholderAvatarLoader(appContext)
|
||||
}
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
@ -18,13 +15,14 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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 +79,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 +96,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 +201,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Set up empty state view
|
||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
startObservingUpdates()
|
||||
|
||||
// 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 +215,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 +394,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
|
||||
@ -441,7 +418,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
if (event.recipient.isLocalNumber) {
|
||||
updateProfileButton()
|
||||
} else {
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,7 +589,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setPinned(threadId, pinned)
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -688,7 +665,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
button(R.string.yes) {
|
||||
textSecurePreferences.setHasHiddenMessageRequests()
|
||||
setupMessageRequestsBanner()
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
button(R.string.no)
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
@ -26,14 +25,15 @@ class HomeAdapter(
|
||||
|
||||
var header: View? = null
|
||||
|
||||
private var _data: List<ThreadRecord> = emptyList()
|
||||
var data: List<ThreadRecord>
|
||||
get() = _data.toList()
|
||||
var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), emptySet())
|
||||
set(newData) {
|
||||
val previousData = _data.toList()
|
||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
||||
if (field === newData) {
|
||||
return
|
||||
}
|
||||
|
||||
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||
val diffResult = DiffUtil.calculateDiff(diff)
|
||||
_data = newData
|
||||
field = newData
|
||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||
}
|
||||
|
||||
@ -61,18 +61,10 @@ 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.threads[offsetPosition].threadId
|
||||
}
|
||||
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) {
|
||||
if (field == value) { return }
|
||||
|
||||
field = value
|
||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||
when (viewType) {
|
||||
@ -95,8 +87,8 @@ class HomeAdapter(
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is ConversationViewHolder) {
|
||||
val offset = if (hasHeaderView()) position - 1 else position
|
||||
val thread = data[offset]
|
||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
||||
val thread = data.threads[offset]
|
||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
||||
holder.view.bind(thread, isTyping, glide)
|
||||
}
|
||||
}
|
||||
@ -113,7 +105,7 @@ class HomeAdapter(
|
||||
if (hasHeaderView() && position == 0) HEADER
|
||||
else ITEM
|
||||
|
||||
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0
|
||||
override fun getItemCount(): Int = data.threads.size + if (hasHeaderView()) 1 else 0
|
||||
|
||||
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
|
@ -2,27 +2,26 @@ package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||
|
||||
class HomeDiffUtil(
|
||||
private val old: List<ThreadRecord>,
|
||||
private val new: List<ThreadRecord>,
|
||||
private val context: Context,
|
||||
private val configFactory: ConfigFactory
|
||||
private val old: HomeViewModel.Data,
|
||||
private val new: HomeViewModel.Data,
|
||||
private val context: Context,
|
||||
private val configFactory: ConfigFactory
|
||||
): DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
override fun getOldListSize(): Int = old.threads.size
|
||||
|
||||
override fun getNewListSize(): Int = new.size
|
||||
override fun getNewListSize(): Int = new.threads.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old[oldItemPosition].threadId == new[newItemPosition].threadId
|
||||
old.threads[oldItemPosition].threadId == new.threads[newItemPosition].threadId
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = old[oldItemPosition]
|
||||
val newItem = new[newItemPosition]
|
||||
val oldItem = old.threads[oldItemPosition]
|
||||
val newItem = new.threads[newItemPosition]
|
||||
|
||||
// return early to save getDisplayBody or expensive calls
|
||||
var isSameItem = true
|
||||
@ -47,7 +46,8 @@ class HomeDiffUtil(
|
||||
oldItem.isSent == newItem.isSent &&
|
||||
oldItem.isPending == newItem.isPending &&
|
||||
oldItem.lastSeen == newItem.lastSeen &&
|
||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true
|
||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
|
||||
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,71 +1,88 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
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.ApplicationContext
|
||||
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
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val contentResolver: ContentResolver,
|
||||
@ApplicationContextQualifier private val context: 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
|
||||
)
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
private var lastContext: WeakReference<Context>? = null
|
||||
private var updateJobs: MutableList<Job> = mutableListOf()
|
||||
/**
|
||||
* A [StateFlow] that emits the list of threads and the typing status of each thread.
|
||||
*
|
||||
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||
* whenever the conversation list changes.
|
||||
*/
|
||||
val threads: StateFlow<Data?> = combine(observeConversationList(), observeTypingStatus(), ::Data)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
||||
private fun observeTypingStatus(): Flow<Set<Long>> =
|
||||
ApplicationContext.getInstance(context).typingStatusRepository
|
||||
.typingThreads
|
||||
.asFlow()
|
||||
.onStart { emit(emptySet()) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
|
||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
||||
|
||||
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) {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<ThreadRecord>()
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge(
|
||||
manualReloadTrigger,
|
||||
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)
|
||||
buildList(reader.count) {
|
||||
while (true) {
|
||||
threads += reader.next ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_conversations.value = threads
|
||||
add(reader.next ?: break)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return conversations
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
fun tryReload() = manualReloadTrigger.tryEmit(Unit)
|
||||
|
||||
data class Data(
|
||||
val threads: List<ThreadRecord>,
|
||||
val typingThreadIDs: Set<Long>
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
@ -17,11 +16,17 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPathBinding
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
@ -184,6 +189,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var location: Location
|
||||
private var dotAnimationStartDelay: Long = 0
|
||||
private var dotAnimationRepeatInterval: Long = 0
|
||||
private var job: Job? = null
|
||||
|
||||
private val dotView by lazy {
|
||||
val result = PathDotView(context)
|
||||
@ -240,19 +246,38 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
||||
dotView.layoutParams = dotViewLayoutParams
|
||||
addView(dotView)
|
||||
Handler().postDelayed({
|
||||
performAnimation()
|
||||
}, dotAnimationStartDelay)
|
||||
}
|
||||
|
||||
private fun performAnimation() {
|
||||
expand()
|
||||
Handler().postDelayed({
|
||||
collapse()
|
||||
Handler().postDelayed({
|
||||
performAnimation()
|
||||
}, dotAnimationRepeatInterval)
|
||||
}, 1000)
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
startAnimation()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
stopAnimation()
|
||||
}
|
||||
|
||||
private fun startAnimation() {
|
||||
job?.cancel()
|
||||
job = GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
while (isActive) {
|
||||
delay(dotAnimationStartDelay)
|
||||
expand()
|
||||
delay(EXPAND_ANIM_DELAY_MILLS)
|
||||
collapse()
|
||||
delay(dotAnimationRepeatInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAnimation() {
|
||||
job?.cancel()
|
||||
job = null
|
||||
}
|
||||
|
||||
private fun expand() {
|
||||
@ -270,6 +295,10 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPAND_ANIM_DELAY_MILLS = 1000L
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -73,7 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory());
|
||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,7 @@ class IP2Country private constructor(private val context: Context) {
|
||||
|
||||
public fun configureIfNeeded(context: Context) {
|
||||
if (isInitialized) { return; }
|
||||
shared = IP2Country(context)
|
||||
shared = IP2Country(context.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
package org.session.libsession.avatars
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
class PlaceholderAvatarPhoto(val context: Context,
|
||||
val hashString: String,
|
||||
class PlaceholderAvatarPhoto(val hashString: String,
|
||||
val displayName: String): Key {
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(hashString.encodeToByteArray())
|
||||
messageDigest.update(displayName.encodeToByteArray())
|
||||
|
@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final @NonNull Address address;
|
||||
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private @Nullable String name;
|
||||
private @Nullable String customLabel;
|
||||
private boolean resolving;
|
||||
@ -132,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
@NonNull Optional<RecipientDetails> details,
|
||||
@NonNull ListenableFutureTask<RecipientDetails> future)
|
||||
{
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.color = null;
|
||||
this.resolving = true;
|
||||
@ -259,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
|
||||
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.contactUri = details.contactUri;
|
||||
this.name = details.name;
|
||||
|
Loading…
Reference in New Issue
Block a user