Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SES-1936] Fix memory leaks #1493

Merged
merged 8 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public AttachmentServer(Context context, Attachment attachment)
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (hexEncodedSeed == null) {
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
}

val appContext = applicationContext

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove and inline?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually can't be inlined. As the syntax val appContext = applicationContext is equivalent to val appContext = this.getApplicationContext(), if you move applicationContext into the lambda, you move this.getApplicationContext() there hence moving this into the lambda, where this is where the memory leak comes from

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like loadFileContents is used once, synchronously in the next line, so Activity Context was fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at MnemonicCodec.encode, it creates a new Language and calls Language.loadWordSet, where Language instance is stored in static cache, in whichloadFileContents is a field of.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of those dodgy "Putting everything you can think of into cache" scenarios

val loadFileContents: (String) -> String = { fileName ->
MnemonicUtilities.loadFileContents(this, fileName)
MnemonicUtilities.loadFileContents(appContext, fileName)
}
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
}
Expand Down Expand Up @@ -831,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe

override fun onDestroy() {
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
cancelVoiceMessage()
tearDownRecipientObserver()
super.onDestroy()
binding = null
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
animateDotView()
pulse()
animateLockViewUp()
updateTimer()
startTimer(scope)
}

fun hide() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,10 @@ public Reader(Cursor cursor) {
this.cursor = cursor;
}

public int getCount() {
return cursor == null ? 0 : cursor.getCount();
}

public ThreadRecord getNext() {
if (cursor == null || !cursor.moveToNext())
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,22 +9,22 @@ 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,
width: Int,
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() {}
}
Expand Down
75 changes: 26 additions & 49 deletions app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"))
Comment on lines -213 to -219
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This piece of code is useless: it doesn't update any data but asking RecyclerView to refresh itself completely on the same data. The only two data source for the home adapter is List<ThreadRecord> and Set<TypingID>, which are totally immutable: trying to reload the immutable data will always result in the same UI representation.

The "blocked contact" will actually be updated through the change to the thread itself. I have tested and it still works without this code here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it looked like there's no other usage of "blockedContactsChanged"


// subscribe to outdated config updates, this should be removed after long enough time for device migration
lifecycleScope.launch {
Expand All @@ -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
Expand Down Expand 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())
}
Comment on lines -429 to -431
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved this change detection into HomeViewModel so we have a uniform data coming from the VM instead.

}

private fun updateEmptyState() {
val threadCount = (binding.recyclerView.adapter)!!.itemCount
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
Expand All @@ -441,7 +418,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (event.recipient.isLocalNumber) {
updateProfileButton()
} else {
homeViewModel.tryUpdateChannel()
homeViewModel.tryReload()
}
}

Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -688,7 +665,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
homeViewModel.tryUpdateChannel()
homeViewModel.tryReload()
}
button(R.string.no)
}
Expand Down
Loading