Merge pull request #1493 from simophin/ses-1936-oom

[SES-1936] Fix memory leaks
pull/1502/head
Andrew 1 month ago committed by GitHub
commit b757691334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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…
Cancel
Save