Merge pull request #1504 from oxen-io/release-1.18.4

Merge release-1.18.4 to master
This commit is contained in:
Andrew 2024-05-31 09:33:54 +09:30 committed by GitHub
commit b544961d28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 632 additions and 406 deletions

View File

@ -31,8 +31,8 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 372 def canonicalVersionCode = 373
def canonicalVersionName = "1.18.3" def canonicalVersionName = "1.18.4"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -41,6 +41,17 @@ def abiPostFix = ['armeabi-v7a' : 1,
'x86_64' : 4, 'x86_64' : 4,
'universal' : 5] 'universal' : 5]
// Function to get the current git commit hash so we can embed it along w/ the build version.
// Note: This is visible in the SettingsActivity, right at the bottom (R.id.versionTextView).
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git", "rev-parse", "--short", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}
android { android {
compileSdkVersion androidCompileSdkVersion compileSdkVersion androidCompileSdkVersion
namespace 'network.loki.messenger' namespace 'network.loki.messenger'
@ -94,6 +105,7 @@ android {
project.ext.set("archivesBaseName", "session") project.ext.set("archivesBaseName", "session")
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"$getGitHash\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "USER_AGENT", "\"OWA\"" buildConfigField "String", "USER_AGENT", "\"OWA\""

View File

@ -21,6 +21,7 @@ import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
@ -145,6 +146,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
} }
}; };
private MediaItemAdapter adapter;
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) { public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread()); return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
@ -217,13 +219,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
} }
@TargetApi(VERSION_CODES.JELLY_BEAN)
private void setFullscreenIfPossible() {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
}
}
@Override @Override
public void onModified(Recipient recipient) { public void onModified(Recipient recipient) {
Util.runOnMain(this::updateActionBar); Util.runOnMain(this::updateActionBar);
@ -285,9 +280,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaPager = findViewById(R.id.media_pager); mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1); mediaPager.setOffscreenPageLimit(1);
viewPagerListener = new ViewPagerListener();
mediaPager.addOnPageChangeListener(viewPagerListener);
albumRail = findViewById(R.id.media_preview_album_rail); albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false); albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
@ -378,7 +370,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (conversationRecipient != null) { if (conversationRecipient != null) {
getSupportLoaderManager().restartLoader(0, null, this); getSupportLoaderManager().restartLoader(0, null, this);
} else { } else {
mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize)); adapter = new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize);
mediaPager.setAdapter(adapter);
if (initialCaption != null) { if (initialCaption != null) {
detailsContainer.setVisibility(View.VISIBLE); detailsContainer.setVisibility(View.VISIBLE);
@ -506,13 +499,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
private @Nullable MediaItem getCurrentMediaItem() { private @Nullable MediaItem getCurrentMediaItem() {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return null;
if (adapter != null) {
return adapter.getMediaItemFor(mediaPager.getCurrentItem()); return adapter.getMediaItemFor(mediaPager.getCurrentItem());
} else {
return null;
}
} }
public static boolean isContentTypeSupported(final String contentType) { public static boolean isContentTypeSupported(final String contentType) {
@ -526,24 +514,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override @Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) { public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) { if (data == null) return;
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.removeOnPageChangeListener(viewPagerListener);
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter); mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, data.first, leftIsRecent); viewModel.setCursor(this, data.first, leftIsRecent);
if (restartItem >= 0 || data.second >= 0) { int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
int item = restartItem >= 0 ? restartItem : data.second;
viewPagerListener = new ViewPagerListener();
mediaPager.addOnPageChangeListener(viewPagerListener);
try {
mediaPager.setCurrentItem(item); mediaPager.setCurrentItem(item);
} catch (CursorIndexOutOfBoundsException e) {
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
}
if (item == 0) { if (item == 0) {
viewPagerListener.onPageSelected(0); viewPagerListener.onPageSelected(0);
} }
} else {
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
}
}
} }
@Override @Override
@ -560,27 +553,27 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage); if (currentPage != -1 && currentPage != position) onPageUnselected(currentPage);
currentPage = position; currentPage = position;
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return;
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this); if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
updateActionBar(); updateActionBar();
} }
}
public void onPageUnselected(int position) { public void onPageUnselected(int position) {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); if (adapter == null) return;
if (adapter != null) { try {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this); if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this);
} catch (CursorIndexOutOfBoundsException e) {
throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
}
adapter.pause(position); adapter.pause(position);
} }
}
@Override @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
@ -593,7 +586,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter { private static class SingleItemPagerAdapter extends MediaItemAdapter {
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final Window window; private final Window window;
@ -665,7 +658,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter { private static class CursorPagerAdapter extends MediaItemAdapter {
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>(); private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
@ -675,7 +668,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private final Cursor cursor; private final Cursor cursor;
private final boolean leftIsRecent; private final boolean leftIsRecent;
private boolean active;
private int autoPlayPosition; private int autoPlayPosition;
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
@ -690,15 +682,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
this.leftIsRecent = leftIsRecent; this.leftIsRecent = leftIsRecent;
} }
public void setActive(boolean active) {
this.active = active;
notifyDataSetChanged();
}
@Override @Override
public int getCount() { public int getCount() {
if (!active) return 0; return cursor.getCount();
else return cursor.getCount();
} }
@Override @Override
@ -771,8 +757,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
private int getCursorPosition(int position) { private int getCursorPosition(int position) {
if (leftIsRecent) return position; int unclamped = leftIsRecent ? position : cursor.getCount() - 1 - position;
else return cursor.getCount() - 1 - position; return Math.max(Math.min(unclamped, cursor.getCount() - 1), 0);
} }
} }
@ -800,9 +786,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
} }
} }
interface MediaItemAdapter { abstract static class MediaItemAdapter extends PagerAdapter {
MediaItem getMediaItemFor(int position); abstract MediaItem getMediaItemFor(int position);
void pause(int position); abstract void pause(int position);
@Nullable View getPlaybackControls(int position); @Nullable abstract View getPlaybackControls(int position);
} }
} }

View File

@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
throws IOException throws IOException
{ {
try { try {
this.context = context; this.context = context.getApplicationContext();
this.attachment = attachment; this.attachment = attachment;
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1})); this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
this.port = socket.getLocalPort(); this.port = socket.getLocalPort();

View File

@ -186,7 +186,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
override fun deleteMessage(messageID: Long, isSms: Boolean) { override fun deleteMessage(messageID: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).mmsDatabase()
val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null) val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null)
messagingDatabase.deleteMessage(messageID) messagingDatabase.deleteMessage(messageID)

View File

@ -45,7 +45,8 @@ public class AudioRecorder {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try { try {
if (audioCodec != null) { if (audioCodec != null) {
throw new AssertionError("We can only record once at a time."); Log.e(TAG, "Trying to start recording while another recording is in progress, exiting...");
return;
} }
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();

View File

@ -122,7 +122,7 @@ class ProfilePictureView @JvmOverloads constructor(
glide.clear(imageView) 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 != "") { if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.load(signalProfilePicture) glide.load(signalProfilePicture)

View File

@ -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)
} }
@ -359,7 +361,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION
private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE private var recyclerScrollState: Int = RecyclerView.SCROLL_STATE_IDLE
// region Settings // region Settings
companion object { companion object {
// Extras // Extras
@ -832,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onDestroy() { override fun onDestroy() {
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "") viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
cancelVoiceMessage()
tearDownRecipientObserver() tearDownRecipientObserver()
super.onDestroy() super.onDestroy()
binding = null binding = null
@ -1020,7 +1022,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun showVoiceMessageUI() { override fun showVoiceMessageUI() {
binding?.inputBarRecordingView?.show() binding?.inputBarRecordingView?.show(lifecycleScope)
binding?.inputBar?.alpha = 0.0f binding?.inputBar?.alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
animation.duration = 250L animation.duration = 250L

View File

@ -22,10 +22,12 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -57,6 +59,7 @@ class ConversationAdapter(
private val contactCache = SparseArray<Contact>(100) private val contactCache = SparseArray<Contact>(100)
private val contactLoadedCache = SparseBooleanArray(100) private val contactLoadedCache = SparseBooleanArray(100)
private val lastSeen = AtomicLong(originalLastSeen) private val lastSeen = AtomicLong(originalLastSeen)
private var lastSentMessageId: Long = -1L
init { init {
lifecycleCoroutineScope.launch(IO) { lifecycleCoroutineScope.launch(IO) {
@ -136,7 +139,8 @@ class ConversationAdapter(
senderId, senderId,
lastSeen.get(), lastSeen.get(),
visibleMessageViewDelegate, visibleMessageViewDelegate,
onAttachmentNeedsDownload onAttachmentNeedsDownload,
lastSentMessageId
) )
if (!message.isDeleted) { if (!message.isDeleted) {

View File

@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
import android.animation.IntEvaluator import android.animation.IntEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.ImageView import android.widget.ImageView
@ -14,6 +12,11 @@ import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible 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.R
import network.loki.messenger.databinding.ViewInputBarRecordingBinding import network.loki.messenger.databinding.ViewInputBarRecordingBinding
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
@ -25,10 +28,10 @@ import java.util.Date
class InputBarRecordingView : RelativeLayout { class InputBarRecordingView : RelativeLayout {
private lateinit var binding: ViewInputBarRecordingBinding private lateinit var binding: ViewInputBarRecordingBinding
private var startTimestamp = 0L private var startTimestamp = 0L
private val snHandler = Handler(Looper.getMainLooper())
private var dotViewAnimation: ValueAnimator? = null private var dotViewAnimation: ValueAnimator? = null
private var pulseAnimation: ValueAnimator? = null private var pulseAnimation: ValueAnimator? = null
var delegate: InputBarRecordingViewDelegate? = null var delegate: InputBarRecordingViewDelegate? = null
private var timerJob: Job? = null
val lockView: LinearLayout val lockView: LinearLayout
get() = binding.lockView get() = binding.lockView
@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true) binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
binding.inputBarMiddleContentContainer.disableClipping() binding.inputBarMiddleContentContainer.disableClipping()
binding.inputBarCancelButton.setOnClickListener { hide() } binding.inputBarCancelButton.setOnClickListener { hide() }
} }
fun show() { fun show(scope: CoroutineScope) {
startTimestamp = Date().time startTimestamp = Date().time
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme)) binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
binding.inputBarCancelButton.alpha = 0.0f binding.inputBarCancelButton.alpha = 0.0f
@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
animateDotView() animateDotView()
pulse() pulse()
animateLockViewUp() animateLockViewUp()
updateTimer() startTimer(scope)
} }
fun hide() { fun hide() {
@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
} }
animation.start() animation.start()
delegate?.handleVoiceMessageUIHidden() 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() { private fun animateDotView() {
@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
animation.start() animation.start()
} }
private fun updateTimer() {
val duration = (Date().time - startTimestamp) / 1000L
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
snHandler.postDelayed({ updateTimer() }, 500)
}
fun lock() { fun lock() {
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
fadeOutAnimation.duration = 250L fadeOutAnimation.duration = 250L

View File

@ -31,10 +31,12 @@ import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.LastSentTimestampCache import org.thoughtcrime.securesms.database.LastSentTimestampCache
import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
@ -133,7 +135,8 @@ class VisibleMessageView : LinearLayout {
senderSessionID: String, senderSessionID: String,
lastSeen: Long, lastSeen: Long,
delegate: VisibleMessageViewDelegate? = null, delegate: VisibleMessageViewDelegate? = null,
onAttachmentNeedsDownload: (Long, Long) -> Unit onAttachmentNeedsDownload: (Long, Long) -> Unit,
lastSentMessageId: Long
) { ) {
replyDisabled = message.isOpenGroupInvitation replyDisabled = message.isOpenGroupInvitation
val threadID = message.threadId val threadID = message.threadId

View File

@ -40,18 +40,16 @@ object ResendMessageUtilities {
message.recipient = messageRecord.recipient.address.serialize() message.recipient = messageRecord.recipient.address.serialize()
} }
message.threadID = messageRecord.threadId message.threadID = messageRecord.threadId
if (messageRecord.isMms) { if (messageRecord.isMms && messageRecord is MmsMessageRecord) {
val mmsMessageRecord = messageRecord as MmsMessageRecord messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) }
if (mmsMessageRecord.linkPreviews.isNotEmpty()) { messageRecord.quote?.quoteModel?.let {
message.linkPreview = LinkPreview.from(mmsMessageRecord.linkPreviews[0]) message.quote = Quote.from(it)?.apply {
} if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) {
if (mmsMessageRecord.quote != null) { publicKey = userBlindedKey
message.quote = Quote.from(mmsMessageRecord.quote!!.quoteModel)
if (userBlindedKey != null && messageRecord.quote!!.author.serialize() == TextSecurePreferences.getLocalNumber(context)) {
message.quote!!.publicKey = userBlindedKey
} }
} }
message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments()) }
message.addSignalAttachments(messageRecord.slideDeck.asAttachments())
} }
val sentTimestamp = message.sentTimestamp val sentTimestamp = message.sentTimestamp
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()

View File

@ -5,9 +5,9 @@ import android.content.Context
import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Debouncer
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
class ConversationNotificationDebouncer(private val context: Context) { class ConversationNotificationDebouncer(private val context: ApplicationContext) {
private val threadIDs = mutableSetOf<Long>() private val threadIDs = mutableSetOf<Long>()
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler private val handler = context.conversationListNotificationHandler
private val debouncer = Debouncer(handler, 100) private val debouncer = Debouncer(handler, 100)
companion object { companion object {
@ -17,20 +17,28 @@ class ConversationNotificationDebouncer(private val context: Context) {
@Synchronized @Synchronized
fun get(context: Context): ConversationNotificationDebouncer { fun get(context: Context): ConversationNotificationDebouncer {
if (::shared.isInitialized) { return shared } if (::shared.isInitialized) { return shared }
shared = ConversationNotificationDebouncer(context) shared = ConversationNotificationDebouncer(context.applicationContext as ApplicationContext)
return shared return shared
} }
} }
fun notify(threadID: Long) { fun notify(threadID: Long) {
synchronized(threadIDs) {
threadIDs.add(threadID) threadIDs.add(threadID)
}
debouncer.publish { publish() } debouncer.publish { publish() }
} }
private fun publish() { private fun publish() {
for (threadID in threadIDs.toList()) { val toNotify = synchronized(threadIDs) {
val copy = threadIDs.toList()
threadIDs.clear()
copy
}
for (threadID in toNotify) {
context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null) context.contentResolver.notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadID), null)
} }
threadIDs.clear()
} }
} }

View File

@ -1147,13 +1147,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
} }
fun readerFor(cursor: Cursor?): Reader { fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote)
return Reader(cursor)
}
fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader { fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId)
return OutgoingMessageReader(message, threadId)
}
fun setQuoteMissing(messageId: Long): Int { fun setQuoteMissing(messageId: Long): Int {
val contentValues = ContentValues() val contentValues = ContentValues()
@ -1217,7 +1213,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
inner class Reader(private val cursor: Cursor?) : Closeable { inner class Reader(private val cursor: Cursor?, private val getQuote: Boolean = true) : Closeable {
val next: MessageRecord? val next: MessageRecord?
get() = if (cursor == null || !cursor.moveToNext()) null else current get() = if (cursor == null || !cursor.moveToNext()) null else current
val current: MessageRecord val current: MessageRecord
@ -1226,7 +1222,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) { return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) {
getNotificationMmsMessageRecord(cursor) getNotificationMmsMessageRecord(cursor)
} else { } else {
getMediaMmsMessageRecord(cursor) getMediaMmsMessageRecord(cursor, getQuote)
} }
} }
@ -1253,20 +1249,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
DELIVERY_RECEIPT_COUNT DELIVERY_RECEIPT_COUNT
) )
) )
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) val readReceiptCount = if (isReadReceiptsEnabled(context)) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1) val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
if (!isReadReceiptsEnabled(context)) { val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
readReceiptCount = 0 val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
}
var contentLocationBytes: ByteArray? = null
var transactionIdBytes: ByteArray? = null
if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes(
contentLocation
)
if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes(
transactionId
)
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize)) val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
return NotificationMmsMessageRecord( return NotificationMmsMessageRecord(
id, recipient, recipient, id, recipient, recipient,
@ -1277,7 +1263,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
) )
} }
private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord { private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
val dateReceived = cursor.getLong( val dateReceived = cursor.getLong(
@ -1328,7 +1314,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
.filterNot { o: DatabaseAttachment? -> o in contactAttachments } .filterNot { o: DatabaseAttachment? -> o in contactAttachments }
.filterNot { o: DatabaseAttachment? -> o in previewAttachments } .filterNot { o: DatabaseAttachment? -> o in previewAttachments }
) )
val quote = getQuote(cursor) val quote = if (getQuote) getQuote(cursor) else null
val reactions = get(context).reactionDatabase().getReactions(cursor) val reactions = get(context).reactionDatabase().getReactions(cursor)
return MediaMmsMessageRecord( return MediaMmsMessageRecord(
id, recipient, recipient, id, recipient, recipient,
@ -1381,7 +1367,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)) val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor) val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor, false)
val quoteText = retrievedQuote?.body val quoteText = retrievedQuote?.body
val quoteMissing = retrievedQuote == null val quoteMissing = retrievedQuote == null
val quoteDeck = ( val quoteDeck = (

View File

@ -97,9 +97,13 @@ public class MmsSmsDatabase extends Database {
} }
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
return getMessageFor(timestamp, serializedAuthor, true);
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor, boolean getQuote) {
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
MmsSmsDatabase.Reader reader = readerFor(cursor); MmsSmsDatabase.Reader reader = readerFor(cursor, getQuote);
MessageRecord messageRecord; MessageRecord messageRecord;
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor); boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
@ -635,7 +639,11 @@ public class MmsSmsDatabase extends Database {
} }
public Reader readerFor(@NonNull Cursor cursor) { public Reader readerFor(@NonNull Cursor cursor) {
return new Reader(cursor); return readerFor(cursor, true);
}
public Reader readerFor(@NonNull Cursor cursor, boolean getQuote) {
return new Reader(cursor, getQuote);
} }
@NotNull @NotNull
@ -658,11 +666,13 @@ public class MmsSmsDatabase extends Database {
public class Reader implements Closeable { public class Reader implements Closeable {
private final Cursor cursor; private final Cursor cursor;
private final boolean getQuote;
private SmsDatabase.Reader smsReader; private SmsDatabase.Reader smsReader;
private MmsDatabase.Reader mmsReader; private MmsDatabase.Reader mmsReader;
public Reader(Cursor cursor) { public Reader(Cursor cursor, boolean getQuote) {
this.cursor = cursor; this.cursor = cursor;
this.getQuote = getQuote;
} }
private SmsDatabase.Reader getSmsReader() { private SmsDatabase.Reader getSmsReader() {
@ -675,7 +685,7 @@ public class MmsSmsDatabase extends Database {
private MmsDatabase.Reader getMmsReader() { private MmsDatabase.Reader getMmsReader() {
if (mmsReader == null) { if (mmsReader == null) {
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor); mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor, getQuote);
} }
return mmsReader; return mmsReader;

View File

@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
this.cursor = cursor; this.cursor = cursor;
} }
public int getCount() {
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;

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.glide package org.thoughtcrime.securesms.glide
import android.content.Context
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import com.bumptech.glide.load.Options import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader 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 com.bumptech.glide.load.model.MultiModelLoaderFactory
import org.session.libsession.avatars.PlaceholderAvatarPhoto import org.session.libsession.avatars.PlaceholderAvatarPhoto
class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> { class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
override fun buildLoadData( override fun buildLoadData(
model: PlaceholderAvatarPhoto, model: PlaceholderAvatarPhoto,
@ -16,14 +17,14 @@ class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawa
height: Int, height: Int,
options: Options options: Options
): LoadData<BitmapDrawable> { ): LoadData<BitmapDrawable> {
return LoadData(model, PlaceholderAvatarFetcher(model.context, model)) return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
} }
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true 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> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
return PlaceholderAvatarLoader() return PlaceholderAvatarLoader(appContext)
} }
override fun teardown() {} override fun teardown() {}
} }

View File

@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home
import android.Manifest import android.Manifest
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString import android.text.SpannableString
@ -18,19 +15,18 @@ import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
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
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.ConfigBase
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -76,14 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country 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 javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -99,7 +92,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
@ -117,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
get() = textSecurePreferences.getLocalNumber()!! get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy { private val homeAdapter: HomeAdapter by lazy {
HomeAdapter(context = this, configFactory = configFactory, listener = this) HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests)
} }
private val globalSearchAdapter = GlobalSearchAdapter { model -> private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -189,7 +181,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
} }
setupMessageRequestsBanner()
// Set up recycler view // Set up recycler view
binding.globalSearchInputLayout.listener = this binding.globalSearchInputLayout.listener = this
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
@ -205,18 +196,10 @@ 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()
// 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 +210,26 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
} }
// Subscribe to threads and update the UI
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.data
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
.collectLatest { data ->
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 = data
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
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
@ -332,34 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButton.isVisible = !isShown binding.newConversationButton.isVisible = !isShown
} }
private fun setupMessageRequestsBanner() {
val messageRequestCount = threadDb.unapprovedConversationCount
// Set up message requests
if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) {
with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) {
unreadCountTextView.text = messageRequestCount.toString()
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(
this@HomeActivity,
Locale.getDefault(),
threadDb.latestUnapprovedConversationTimestamp
)
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = root
if (hadHeader) homeAdapter.notifyItemChanged(0)
else homeAdapter.notifyItemInserted(0)
}
} else {
val hadHeader = homeAdapter.hasHeaderView()
homeAdapter.header = null
if (hadHeader) {
homeAdapter.notifyItemRemoved(0)
}
}
}
private fun updateLegacyConfigView() { private fun updateLegacyConfigView() {
binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
&& textSecurePreferences.getHasLegacyConfig() && textSecurePreferences.getHasLegacyConfig()
@ -385,52 +360,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
@ -441,7 +384,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (event.recipient.isLocalNumber) { if (event.recipient.isLocalNumber) {
updateProfileButton() updateProfileButton()
} else { } else {
homeViewModel.tryUpdateChannel() homeViewModel.tryReload()
} }
} }
@ -612,7 +555,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun setConversationPinned(threadId: Long, pinned: Boolean) { private fun setConversationPinned(threadId: Long, pinned: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
storage.setPinned(threadId, pinned) storage.setPinned(threadId, pinned)
homeViewModel.tryUpdateChannel() homeViewModel.tryReload()
} }
} }
@ -687,8 +630,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
text("Hide message requests?") text("Hide message requests?")
button(R.string.yes) { button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests() textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner() homeViewModel.tryReload()
homeViewModel.tryUpdateChannel()
} }
button(R.string.no) button(R.string.no)
} }

View File

@ -9,14 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.database.model.ThreadRecord import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class HomeAdapter( class HomeAdapter(
private val context: Context, private val context: Context,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val listener: ConversationClickListener private val listener: ConversationClickListener,
private val showMessageRequests: () -> Unit,
private val hideMessageRequests: () -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback {
companion object { companion object {
@ -24,23 +28,32 @@ class HomeAdapter(
private const val ITEM = 1 private const val ITEM = 1
} }
var header: View? = null var messageRequests: HomeViewModel.MessageRequests? = null
set(value) {
if (field == value) return
val hadHeader = hasHeaderView()
field = value
if (value != null) {
if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0)
} else if (hadHeader) notifyItemRemoved(0)
}
private var _data: List<ThreadRecord> = emptyList() var data: HomeViewModel.Data = HomeViewModel.Data()
var data: List<ThreadRecord>
get() = _data.toList()
set(newData) { set(newData) {
val previousData = _data.toList() if (field === newData) return
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
messageRequests = newData.messageRequests
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 = messageRequests != null
private val headerCount: Int private val headerCount: Int
get() = if (header == null) 0 else 1 get() = if (messageRequests == null) 0 else 1
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position + headerCount, count) notifyItemRangeInserted(position + headerCount, count)
@ -61,23 +74,19 @@ 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.threads[offsetPosition].threadId
} }
lateinit var glide: GlideRequests 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 = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) { when (viewType) {
HEADER -> { HEADER -> {
HeaderFooterViewHolder(header!!) ViewMessageRequestBannerBinding.inflate(LayoutInflater.from(parent.context)).apply {
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}.let(::HeaderFooterViewHolder)
} }
ITEM -> { ITEM -> {
val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView
@ -93,19 +102,27 @@ class HomeAdapter(
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ConversationViewHolder) { when (holder) {
is HeaderFooterViewHolder -> {
holder.binding.run {
messageRequests?.let {
unreadCountTextView.text = it.count
timestampTextView.text = it.timestamp
}
}
}
is ConversationViewHolder -> {
val offset = if (hasHeaderView()) position - 1 else position val offset = if (hasHeaderView()) position - 1 else position
val thread = data[offset] val thread = data.threads[offset]
val isTyping = typingThreadIDs.contains(thread.threadId) val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide) holder.view.bind(thread, isTyping, glide)
} }
} }
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ConversationViewHolder) { if (holder is ConversationViewHolder) {
holder.view.recycle() holder.view.recycle()
} else {
super.onViewRecycled(holder)
} }
} }
@ -113,10 +130,9 @@ class HomeAdapter(
if (hasHeaderView() && position == 0) HEADER if (hasHeaderView() && position == 0) HEADER
else ITEM 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) class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root)
} }

View File

@ -2,27 +2,26 @@ package org.thoughtcrime.securesms.home
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.getConversationUnread import org.thoughtcrime.securesms.util.getConversationUnread
class HomeDiffUtil( class HomeDiffUtil(
private val old: List<ThreadRecord>, private val old: HomeViewModel.Data,
private val new: List<ThreadRecord>, private val new: HomeViewModel.Data,
private val context: Context, private val context: Context,
private val configFactory: ConfigFactory private val configFactory: ConfigFactory
): DiffUtil.Callback() { ): 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 = 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 { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = old[oldItemPosition] val oldItem = old.threads[oldItemPosition]
val newItem = new[newItemPosition] val newItem = new.threads[newItemPosition]
// return early to save getDisplayBody or expensive calls // return early to save getDisplayBody or expensive calls
var isSameItem = true var isSameItem = true
@ -47,7 +46,8 @@ class HomeDiffUtil(
oldItem.isSent == newItem.isSent && oldItem.isSent == newItem.isSent &&
oldItem.isPending == newItem.isPending && oldItem.isPending == newItem.isPending &&
oldItem.lastSeen == newItem.lastSeen && oldItem.lastSeen == newItem.lastSeen &&
configFactory.convoVolatile?.getConversationUnread(newItem) != true configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
) )
} }

View File

@ -1,71 +1,131 @@
package org.thoughtcrime.securesms.home package org.thoughtcrime.securesms.home
import android.content.ContentResolver
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collect 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.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
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.DateUtils
import org.thoughtcrime.securesms.util.observeChanges
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() { class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase,
private val executor = viewModelScope + SupervisorJob() private val contentResolver: ContentResolver,
private var lastContext: WeakReference<Context>? = null private val prefs: TextSecurePreferences,
private var updateJobs: MutableList<Job> = mutableListOf() @ApplicationContextQualifier private val context: Context,
) : ViewModel() {
private val _conversations = MutableLiveData<List<ThreadRecord>>() // SharedFlow that emits whenever the user asks us to reload the conversation
val conversations: LiveData<List<ThreadRecord>> = _conversations private val manualReloadTrigger = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED) onBufferOverflow = BufferOverflow.DROP_OLDEST
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) { * 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 data: StateFlow<Data?> = combine(
observeConversationList(),
observeTypingStatus(),
messageRequests(),
::Data
)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private fun hasHiddenMessageRequests() = TextSecurePreferences.events
.filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
.flowOn(Dispatchers.IO)
.map { prefs.hasHiddenMessageRequests() }
.onStart { emit(prefs.hasHiddenMessageRequests()) }
private fun observeTypingStatus(): Flow<Set<Long>> =
ApplicationContext.getInstance(context).typingStatusRepository
.typingThreads
.asFlow()
.onStart { emit(emptySet()) }
.distinctUntilChanged()
private fun messageRequests() = combine(
unapprovedConversationCount(),
hasHiddenMessageRequests(),
latestUnapprovedConversationTimestamp(),
::createMessageRequests
)
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
.map { threadDb.unapprovedConversationCount }
private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
.map { threadDb.latestUnapprovedConversationTimestamp }
@Suppress("OPT_IN_USAGE")
private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
.mapLatest { _ ->
threadDb.approvedConversationList.use { openCursor -> threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor) threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
val threads = mutableListOf<ThreadRecord>()
while (true) {
threads += reader.next ?: break
} }
withContext(Dispatchers.Main) {
_conversations.value = threads
}
}
}
}
)
}
return conversations
} }
@OptIn(FlowPreview::class)
private fun reloadTriggersAndContentChanges() = merge(
manualReloadTrigger,
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)
)
.flowOn(Dispatchers.IO)
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) }
fun tryReload() = manualReloadTrigger.tryEmit(Unit)
data class Data(
val threads: List<ThreadRecord> = emptyList(),
val typingThreadIDs: Set<Long> = emptySet(),
val messageRequests: MessageRequests? = null
)
fun createMessageRequests(
count: Int,
hidden: Boolean,
timestamp: Long
) = if (count > 0 && !hidden) MessageRequests(
count.toString(),
DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), timestamp)
) else null
data class MessageRequests(val count: String, val timestamp: String)
companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
}
} }

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
@ -17,11 +16,17 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.localbroadcastmanager.content.LocalBroadcastManager 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.R
import network.loki.messenger.databinding.ActivityPathBinding import network.loki.messenger.databinding.ActivityPathBinding
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.GlowViewUtilities
@ -184,6 +189,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
private lateinit var location: Location private lateinit var location: Location
private var dotAnimationStartDelay: Long = 0 private var dotAnimationStartDelay: Long = 0
private var dotAnimationRepeatInterval: Long = 0 private var dotAnimationRepeatInterval: Long = 0
private var job: Job? = null
private val dotView by lazy { private val dotView by lazy {
val result = PathDotView(context) val result = PathDotView(context)
@ -240,19 +246,38 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
dotViewLayoutParams.addRule(CENTER_IN_PARENT) dotViewLayoutParams.addRule(CENTER_IN_PARENT)
dotView.layoutParams = dotViewLayoutParams dotView.layoutParams = dotViewLayoutParams
addView(dotView) addView(dotView)
Handler().postDelayed({
performAnimation()
}, dotAnimationStartDelay)
} }
private fun performAnimation() { 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() expand()
Handler().postDelayed({ delay(EXPAND_ANIM_DELAY_MILLS)
collapse() collapse()
Handler().postDelayed({ delay(dotAnimationRepeatInterval)
performAnimation() }
}, dotAnimationRepeatInterval) }
}, 1000) }
}
private fun stopAnimation() {
job?.cancel()
job = null
} }
private fun expand() { private fun expand() {
@ -270,6 +295,10 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
val endColor = context.resources.getColorWithID(endColorID, context.theme) val endColor = context.resources.getColorWithID(endColorID, context.theme)
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor) GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
} }
companion object {
private const val EXPAND_ANIM_DELAY_MILLS = 1000L
}
} }
// endregion // endregion
} }

View File

@ -73,7 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.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()); registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
} }

View File

@ -37,6 +37,7 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.* import org.session.libsession.utilities.*
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.getProperty
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.components.ProfilePictureView
@ -107,7 +108,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
helpButton.setOnClickListener { showHelp() } helpButton.setOnClickListener { showHelp() }
seedButton.setOnClickListener { showSeed() } seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() } clearAllDataButton.setOnClickListener { clearAllData() }
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)")
} }
} }

View File

@ -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)
}
}
}

View File

@ -55,7 +55,7 @@ class IP2Country private constructor(private val context: Context) {
public fun configureIfNeeded(context: Context) { public fun configureIfNeeded(context: Context) {
if (isInitialized) { return; } if (isInitialized) { return; }
shared = IP2Country(context) shared = IP2Country(context.applicationContext)
} }
} }

View File

@ -7,7 +7,7 @@
<string name="ban">مسدود</string> <string name="ban">مسدود</string>
<string name="save">ذخیره</string> <string name="save">ذخیره</string>
<string name="note_to_self">یادداشت به خود</string> <string name="note_to_self">یادداشت به خود</string>
<string name="version_s">نسخه</string> <string name="version_s">%s نسخه</string>
<!-- AbstractNotificationBuilder --> <!-- AbstractNotificationBuilder -->
<string name="AbstractNotificationBuilder_new_message">پیام جدید</string> <string name="AbstractNotificationBuilder_new_message">پیام جدید</string>
<!-- AlbumThumbnailView --> <!-- AlbumThumbnailView -->

View File

@ -1,5 +1,6 @@
#include "config_base.h" #include "config_base.h"
#include "util.h" #include "util.h"
#include "jni_utils.h"
extern "C" { extern "C" {
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
@ -85,10 +86,11 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *en
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
jobjectArray to_merge) { jobjectArray to_merge) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto conf = ptrToConfigBase(env, thiz); auto conf = ptrToConfigBase(env, thiz);
size_t number = env->GetArrayLength(to_merge); size_t number = env->GetArrayLength(to_merge);
std::vector<std::pair<std::string,session::ustring>> configs = {}; std::vector<std::pair<std::string, session::ustring>> configs = {};
for (int i = 0; i < number; i++) { for (int i = 0; i < number; i++) {
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i); auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
auto pair = extractHashAndData(env, jElement); auto pair = extractHashAndData(env, jElement);
@ -97,17 +99,21 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(
auto returned = conf->merge(configs); auto returned = conf->merge(configs);
auto string_stack = util::build_string_stack(env, returned); auto string_stack = util::build_string_stack(env, returned);
return string_stack; return string_stack;
});
} }
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
jobject to_merge) { jobject to_merge) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto conf = ptrToConfigBase(env, thiz); auto conf = ptrToConfigBase(env, thiz);
std::vector<std::pair<std::string, session::ustring>> configs = {extractHashAndData(env, to_merge)}; std::vector<std::pair<std::string, session::ustring>> configs = {
extractHashAndData(env, to_merge)};
auto returned = conf->merge(configs); auto returned = conf->merge(configs);
auto string_stack = util::build_string_stack(env, returned); auto string_stack = util::build_string_stack(env, returned);
return string_stack; return string_stack;
});
} }
#pragma clang diagnostic pop #pragma clang diagnostic pop

View File

@ -1,10 +1,14 @@
#include "contacts.h" #include "contacts.h"
#include "util.h" #include "util.h"
#include "jni_utils.h"
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
jstring session_id) { jstring session_id) {
// If an exception is thrown, return nullptr
return jni_utils::run_catching_cxx_exception_or<jobject>(
[=]() -> jobject {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto contacts = ptrToContacts(env, thiz); auto contacts = ptrToContacts(env, thiz);
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
@ -13,34 +17,42 @@ Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject t
if (!contact) return nullptr; if (!contact) return nullptr;
jobject j_contact = serialize_contact(env, contact.value()); jobject j_contact = serialize_contact(env, contact.value());
return j_contact; return j_contact;
},
[](const char *) -> jobject { return nullptr; }
);
} }
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
jstring session_id) { jstring session_id) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto contacts = ptrToContacts(env, thiz); auto contacts = ptrToContacts(env, thiz);
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
auto contact = contacts->get_or_construct(session_id_chars); auto contact = contacts->get_or_construct(session_id_chars);
env->ReleaseStringUTFChars(session_id, session_id_chars); env->ReleaseStringUTFChars(session_id, session_id_chars);
return serialize_contact(env, contact); return serialize_contact(env, contact);
});
} }
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
jobject contact) { jobject contact) {
jni_utils::run_catching_cxx_exception_or_throws<void>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto contacts = ptrToContacts(env, thiz); auto contacts = ptrToContacts(env, thiz);
auto contact_info = deserialize_contact(env, contact, contacts); auto contact_info = deserialize_contact(env, contact, contacts);
contacts->set(contact_info); contacts->set(contact_info);
});
} }
extern "C" extern "C"
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz, Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
jstring session_id) { jstring session_id) {
return jni_utils::run_catching_cxx_exception_or_throws<jboolean>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto contacts = ptrToContacts(env, thiz); auto contacts = ptrToContacts(env, thiz);
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
@ -48,6 +60,7 @@ Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject
bool result = contacts->erase(session_id_chars); bool result = contacts->erase(session_id_chars);
env->ReleaseStringUTFChars(session_id, session_id_chars); env->ReleaseStringUTFChars(session_id, session_id_chars);
return result; return result;
});
} }
extern "C" extern "C"
#pragma clang diagnostic push #pragma clang diagnostic push
@ -56,45 +69,53 @@ JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env, Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
jobject thiz, jobject thiz,
jbyteArray ed25519_secret_key) { jbyteArray ed25519_secret_key) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
auto* contacts = new session::config::Contacts(secret_key, std::nullopt); auto *contacts = new session::config::Contacts(secret_key, std::nullopt);
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V"); jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts)); jobject newConfig = env->NewObject(contactsClass, constructor,
reinterpret_cast<jlong>(contacts));
return newConfig; return newConfig;
});
} }
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B( Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
auto initial = util::ustring_from_bytes(env, initial_dump); auto initial = util::ustring_from_bytes(env, initial_dump);
auto* contacts = new session::config::Contacts(secret_key, initial); auto *contacts = new session::config::Contacts(secret_key, initial);
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V"); jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts)); jobject newConfig = env->NewObject(contactsClass, constructor,
reinterpret_cast<jlong>(contacts));
return newConfig; return newConfig;
});
} }
#pragma clang diagnostic pop #pragma clang diagnostic pop
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) { Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto contacts = ptrToContacts(env, thiz); auto contacts = ptrToContacts(env, thiz);
jclass stack = env->FindClass("java/util/Stack"); jclass stack = env->FindClass("java/util/Stack");
jmethodID init = env->GetMethodID(stack, "<init>", "()V"); jmethodID init = env->GetMethodID(stack, "<init>", "()V");
jobject our_stack = env->NewObject(stack, init); jobject our_stack = env->NewObject(stack, init);
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
for (const auto& contact : *contacts) { for (const auto &contact: *contacts) {
auto contact_obj = serialize_contact(env, contact); auto contact_obj = serialize_contact(env, contact);
env->CallObjectMethod(our_stack, push, contact_obj); env->CallObjectMethod(our_stack, push, contact_obj);
} }
return our_stack; return our_stack;
});
} }

View File

@ -0,0 +1,54 @@
#ifndef SESSION_ANDROID_JNI_UTILS_H
#define SESSION_ANDROID_JNI_UTILS_H
#include <jni.h>
#include <exception>
namespace jni_utils {
/**
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught,
* and returning a default-constructed value of the specified type.
*
* @tparam RetT The return type of the function
* @tparam Func The function type
* @param f The function to run
* @param fallbackRun The function to run if an exception is caught. The optional exception message reference will be passed to this function.
* @return The return value of the function, or the return value of the fallback function if an exception was caught
*/
template<class RetT, class Func, class FallbackRun>
RetT run_catching_cxx_exception_or(Func f, FallbackRun fallbackRun) {
try {
return f();
} catch (const std::exception &e) {
return fallbackRun(e.what());
} catch (...) {
return fallbackRun(nullptr);
}
}
/**
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught.
*
* @tparam RetT The return type of the function
* @tparam Func The function type
* @param env The JNI environment
* @param f The function to run
* @return The return value of the function, or a default-constructed value of the specified type if an exception was caught
*/
template<class RetT, class Func>
RetT run_catching_cxx_exception_or_throws(JNIEnv *env, Func f) {
return run_catching_cxx_exception_or<RetT>(f, [env](const char *msg) {
jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
if (msg) {
auto formatted_message = std::string("libsession: C++ exception: ") + msg;
env->ThrowNew(exceptionClass, formatted_message.c_str());
} else {
env->ThrowNew(exceptionClass, "libsession: Unknown C++ exception");
}
return RetT();
});
}
}
#endif //SESSION_ANDROID_JNI_UTILS_H

View File

@ -1,13 +1,10 @@
package org.session.libsession.avatars package org.session.libsession.avatars
import android.content.Context
import com.bumptech.glide.load.Key import com.bumptech.glide.load.Key
import java.security.MessageDigest import java.security.MessageDigest
class PlaceholderAvatarPhoto(val context: Context, class PlaceholderAvatarPhoto(val hashString: String,
val hashString: String,
val displayName: String): Key { val displayName: String): Key {
override fun updateDiskCacheKey(messageDigest: MessageDigest) { override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(hashString.encodeToByteArray()) messageDigest.update(hashString.encodeToByteArray())
messageDigest.update(displayName.encodeToByteArray()) messageDigest.update(displayName.encodeToByteArray())

View File

@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
private final @NonNull Address address; private final @NonNull Address address;
private final @NonNull List<Recipient> participants = new LinkedList<>(); private final @NonNull List<Recipient> participants = new LinkedList<>();
private Context context; private final Context context;
private @Nullable String name; private @Nullable String name;
private @Nullable String customLabel; private @Nullable String customLabel;
private boolean resolving; private boolean resolving;
@ -132,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
@NonNull Optional<RecipientDetails> details, @NonNull Optional<RecipientDetails> details,
@NonNull ListenableFutureTask<RecipientDetails> future) @NonNull ListenableFutureTask<RecipientDetails> future)
{ {
this.context = context; this.context = context.getApplicationContext();
this.address = address; this.address = address;
this.color = null; this.color = null;
this.resolving = true; this.resolving = true;
@ -259,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
} }
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) { Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
this.context = context; this.context = context.getApplicationContext();
this.address = address; this.address = address;
this.contactUri = details.contactUri; this.contactUri = details.contactUri;
this.name = details.name; this.name = details.name;

View File

@ -1,23 +1,60 @@
package org.session.libsignal.utilities package org.session.libsignal.utilities
import android.os.Process import android.os.Process
import java.util.concurrent.* import java.util.concurrent.ExecutorService
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.SynchronousQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
object ThreadUtils { object ThreadUtils {
const val TAG = "ThreadUtils"
const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE
val executorPool: ExecutorService = Executors.newCachedThreadPool() // Paraphrased from: https://www.baeldung.com/kotlin/create-thread-pool
// "A cached thread pool such as one created via:
// `val executorPool: ExecutorService = Executors.newCachedThreadPool()`
// will utilize resources according to the requirements of submitted tasks. It will try to reuse
// existing threads for submitted tasks but will create as many threads as it needs if new tasks
// keep pouring in (with a memory usage of at least 1MB per created thread). These threads will
// live for up to 60 seconds of idle time before terminating by default. As such, it presents a
// very sharp tool that doesn't include any backpressure mechanism - and a sudden peak in load
// can bring the system down with an OutOfMemory error. We can achieve a similar effect but with
// better control by creating a ThreadPoolExecutor manually."
private val corePoolSize = Runtime.getRuntime().availableProcessors() // Default thread pool size is our CPU core count
private val maxPoolSize = corePoolSize * 4 // Allow a maximum pool size of up to 4 threads per core
private val keepAliveTimeSecs = 100L // How long to keep idle threads in the pool before they are terminated
private val workQueue = SynchronousQueue<Runnable>()
val executorPool: ExecutorService = ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTimeSecs, TimeUnit.SECONDS, workQueue)
// Note: To see how many threads are running in our app at any given time we can use:
// val threadCount = getAllStackTraces().size
@JvmStatic @JvmStatic
fun queue(target: Runnable) { fun queue(target: Runnable) {
executorPool.execute(target) executorPool.execute {
try {
target.run()
} catch (e: Exception) {
Log.e(TAG, e)
}
}
} }
fun queue(target: () -> Unit) { fun queue(target: () -> Unit) {
executorPool.execute(target) executorPool.execute {
try {
target()
} catch (e: Exception) {
Log.e(TAG, e)
}
}
} }
// Thread executor used by the audio recorder only
@JvmStatic @JvmStatic
fun newDynamicSingleThreadedExecutor(): ExecutorService { fun newDynamicSingleThreadedExecutor(): ExecutorService {
val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue()) val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())