Merge branch 'dev' into on

This commit is contained in:
Andrew 2024-05-30 11:09:51 +09:30
commit 59910eeb48
45 changed files with 665 additions and 461 deletions

View File

@ -31,8 +31,8 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 371 def canonicalVersionCode = 373
def canonicalVersionName = "1.18.2" def canonicalVersionName = "1.18.4"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -57,6 +57,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities; import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase; import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.LastSentTimestampCache;
import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage; import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -149,6 +150,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject TextSecurePreferences textSecurePreferences; @Inject TextSecurePreferences textSecurePreferences;
@Inject PushRegistry pushRegistry; @Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory; @Inject ConfigFactory configFactory;
@Inject LastSentTimestampCache lastSentTimestampCache;
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
@ -221,7 +223,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
device, device,
messageDataProvider, messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this), ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
configFactory configFactory,
lastSentTimestampCache
); );
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage); callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");

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;
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
if (adapter != null) {
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
} else {
return null;
}
} }
public static boolean isContentTypeSupported(final String contentType) { public static boolean isContentTypeSupported(final String contentType) {
@ -526,23 +514,28 @@ 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.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, data.first, leftIsRecent); mediaPager.removeOnPageChangeListener(viewPagerListener);
if (restartItem >= 0 || data.second >= 0) { adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
int item = restartItem >= 0 ? restartItem : data.second; mediaPager.setAdapter(adapter);
mediaPager.setCurrentItem(item);
if (item == 0) { viewModel.setCursor(this, data.first, leftIsRecent);
viewPagerListener.onPageSelected(0);
} int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
} else {
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception"); viewPagerListener = new ViewPagerListener();
} mediaPager.addOnPageChangeListener(viewPagerListener);
try {
mediaPager.setCurrentItem(item);
} catch (CursorIndexOutOfBoundsException e) {
throw new RuntimeException("restartItem = " + restartItem + ", data.second = " + data.second + " leftIsRecent = " + leftIsRecent, e);
}
if (item == 0) {
viewPagerListener.onPageSelected(0);
} }
} }
@ -560,26 +553,26 @@ 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) {
adapter.pause(position); throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
} }
adapter.pause(position);
} }
@Override @Override
@ -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

@ -5,6 +5,8 @@ import android.text.TextUtils
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
@ -184,10 +186,15 @@ 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)
messagingDatabase.deleteMessage(messageID) messagingDatabase.deleteMessage(messageID)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, isSms)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHash(messageID, mms = !isSms)
threadId ?: return
timestamp ?: return
MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(threadId, timestamp)
} }
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) { override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
@ -195,12 +202,17 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
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 messages = messageIDs.mapNotNull { runCatching { messagingDatabase.getMessageRecord(it) }.getOrNull() }
// Perform local delete // Perform local delete
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId) messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
// Perform online delete // Perform online delete
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms) DatabaseComponent.get(context).lokiMessageDatabase().deleteMessageServerHashes(messageIDs, mms = !isSms)
val threadId = messages.firstOrNull()?.threadId
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
} }
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? { override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {

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)
} }
@ -325,7 +327,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
}, },
onAttachmentNeedsDownload = { attachmentId, mmsId -> onAttachmentNeedsDownload = { attachmentId, mmsId ->
// Start download (on IO thread)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId)) JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
} }
@ -335,8 +336,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
) )
adapter.visibleMessageViewDelegate = this adapter.visibleMessageViewDelegate = this
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're // Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView for if
// already near the the bottom and the data changes. // we're already near the the bottom and the data changes.
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter)) adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
adapter adapter
@ -374,7 +375,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
const val PICK_GIF = 10 const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12 const val PICK_FROM_LIBRARY = 12
const val INVITE_CONTACTS = 124 const val INVITE_CONTACTS = 124
} }
// endregion // endregion
@ -833,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
@ -1021,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

@ -209,20 +209,6 @@ class ConversationAdapter(
return messageDB.readerFor(cursor).current return messageDB.readerFor(cursor).current
} }
private fun getLastSentMessageId(cursor: Cursor): Long {
// If we don't move to first (or at least step backwards) we can step off the end of the
// cursor and any query will return an "Index = -1" error.
val cursorHasContent = cursor.moveToFirst()
if (cursorHasContent) {
val thisThreadId = cursor.getLong(4) // Column index 4 is "thread_id"
if (thisThreadId != -1L) {
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
return messageDB.getLastSentMessageFromSender(thisThreadId, thisUsersSessionId)
}
}
return -1L
}
override fun changeCursor(cursor: Cursor?) { override fun changeCursor(cursor: Cursor?) {
super.changeCursor(cursor) super.changeCursor(cursor)
@ -243,11 +229,6 @@ class ConversationAdapter(
toDeselect.iterator().forEach { (pos, record) -> toDeselect.iterator().forEach { (pos, record) ->
onDeselect(record, pos) onDeselect(record, pos)
} }
// This value gets updated here ONLY when the cursor changes, and the value is then passed
// through to `VisibleMessageView.bind` each time we bind via `onBindItemViewHolder`, above.
// If there are no messages then lastSentMessageId is assigned the value -1L.
if (cursor != null) { lastSentMessageId = getLastSentMessageId(cursor) }
} }
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? { fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.AbstractCursorLoader import org.thoughtcrime.securesms.util.AbstractCursorLoader
@ -12,6 +13,7 @@ class ConversationLoader(
) : AbstractCursorLoader(context) { ) : AbstractCursorLoader(context) {
override fun getCursor(): Cursor { override fun getCursor(): Cursor {
MessagingModuleConfiguration.shared.lastSentTimestampCache.refresh(threadID)
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse) return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
} }
} }

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

@ -22,7 +22,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -39,6 +38,7 @@ 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.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.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsDatabase
@ -73,6 +73,7 @@ class VisibleMessageView : LinearLayout {
@Inject lateinit var mmsSmsDb: MmsSmsDatabase @Inject lateinit var mmsSmsDb: MmsSmsDatabase
@Inject lateinit var smsDb: SmsDatabase @Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var mmsDb: MmsDatabase
@Inject lateinit var lastSentTimestampCache: LastSentTimestampCache
private val binding by lazy { ViewVisibleMessageBinding.bind(this) } private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
@ -303,10 +304,6 @@ class VisibleMessageView : LinearLayout {
// --- If we got here then we know the message is outgoing --- // --- If we got here then we know the message is outgoing ---
val thisUsersSessionId = TextSecurePreferences.getLocalNumber(context)
val lastSentMessageId = mmsSmsDb.getLastSentMessageFromSender(message.threadId, thisUsersSessionId)
val isLastSentMessage = lastSentMessageId == message.id
// ----- Case ii.) Message is outgoing but NOT scheduled to disappear ----- // ----- Case ii.) Message is outgoing but NOT scheduled to disappear -----
if (!scheduledToDisappear) { if (!scheduledToDisappear) {
// If this isn't a disappearing message then we never show the timer // If this isn't a disappearing message then we never show the timer
@ -319,9 +316,11 @@ class VisibleMessageView : LinearLayout {
} else { } else {
// ..but if the message HAS been successfully sent or read then only display the delivery status // ..but if the message HAS been successfully sent or read then only display the delivery status
// text and image if this is the last sent message. // text and image if this is the last sent message.
binding.messageStatusTextView.isVisible = isLastSentMessage val lastSentTimestamp = lastSentTimestampCache.getTimestamp(message.threadId)
binding.messageStatusImageView.isVisible = isLastSentMessage val isLastSent = lastSentTimestamp == message.timestamp
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() } binding.messageStatusTextView.isVisible = isLastSent
binding.messageStatusImageView.isVisible = isLastSent
if (isLastSent) { binding.messageStatusImageView.bringToFront() }
} }
} }
else // ----- Case iii.) Message is outgoing AND scheduled to disappear ----- else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----

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

@ -1,10 +1,9 @@
package org.thoughtcrime.securesms.crypto package org.thoughtcrime.securesms.crypto
import android.content.Context import android.content.Context
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
@ -13,8 +12,6 @@ import org.session.libsignal.utilities.Hex
object KeyPairUtilities { object KeyPairUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun generate(): KeyPairGenerationResult { fun generate(): KeyPairGenerationResult {
val seed = sodium.randomBytesBuf(16) val seed = sodium.randomBytesBuf(16)
try { try {

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) {
threadIDs.add(threadID) synchronized(threadIDs) {
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

@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.database
import org.session.libsession.messaging.LastSentTimestampCache
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LastSentTimestampCache @Inject constructor(
val mmsSmsDatabase: MmsSmsDatabase
): LastSentTimestampCache {
private val map = mutableMapOf<Long, Long>()
@Synchronized
override fun getTimestamp(threadId: Long): Long? = map[threadId]
@Synchronized
override fun submitTimestamp(threadId: Long, timestamp: Long) {
if (map[threadId]?.let { timestamp <= it } == true) return
map[threadId] = timestamp
}
@Synchronized
override fun delete(threadId: Long, timestamps: List<Long>) {
if (map[threadId]?.let { it !in timestamps } == true) return
map.remove(threadId)
refresh(threadId)
}
@Synchronized
override fun refresh(threadId: Long) {
if (map[threadId]?.let { it > 0 } == true) return
val lastOutgoingTimestamp = mmsSmsDatabase.getLastOutgoingTimestamp(threadId)
if (lastOutgoingTimestamp <= 0) return
map[threadId] = lastOutgoingTimestamp
}
}

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

@ -9,7 +9,11 @@ public interface MmsSmsColumns {
public static final String THREAD_ID = "thread_id"; public static final String THREAD_ID = "thread_id";
public static final String READ = "read"; public static final String READ = "read";
public static final String BODY = "body"; public static final String BODY = "body";
// This is the address of the message recipient, which may be a single user, a group, or a community!
// It is NOT the address of the sender of any given message!
public static final String ADDRESS = "address"; public static final String ADDRESS = "address";
public static final String ADDRESS_DEVICE_ID = "address_device_id"; public static final String ADDRESS_DEVICE_ID = "address_device_id";
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count"; public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
public static final String READ_RECEIPT_COUNT = "read_receipt_count"; public static final String READ_RECEIPT_COUNT = "read_receipt_count";

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);
@ -295,15 +299,7 @@ public class MmsSmsDatabase extends Database {
return identifiedMessages; return identifiedMessages;
} }
public long getLastSentMessageFromSender(long threadId, String serializedAuthor) { public long getLastOutgoingTimestamp(long threadId) {
// Early exit
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
if (!isOwnNumber) {
Log.i(TAG, "Asked to find last sent message but sender isn't us - returning null.");
return -1;
}
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@ -311,8 +307,13 @@ public class MmsSmsDatabase extends Database {
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) { try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) { try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
MessageRecord messageRecord; MessageRecord messageRecord;
long attempts = 0;
long maxAttempts = 20;
while ((messageRecord = reader.getNext()) != null) { while ((messageRecord = reader.getNext()) != null) {
if (messageRecord.isOutgoing()) { return messageRecord.id; } // Note: We rely on the message order to get us the most recent outgoing message - so we
// take the first outgoing message we find as the last outgoing message.
if (messageRecord.isOutgoing()) return messageRecord.getTimestamp();
if (attempts++ > maxAttempts) break;
} }
} }
} }
@ -638,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
@ -661,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() {
@ -678,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.widget.Toast import android.widget.Toast
@ -41,19 +38,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
@ -106,14 +102,11 @@ import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.ui.small
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
@ -128,7 +121,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
@ -146,7 +138,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 ->
@ -211,7 +203,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (!textSecurePreferences.getHasViewedSeed()) SeedReminder() if (!textSecurePreferences.getHasViewedSeed()) SeedReminder()
} }
} }
setupMessageRequestsBanner()
// Set up recycler view // Set up recycler view
binding.globalSearchInputLayout.listener = this binding.globalSearchInputLayout.listener = this
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
@ -228,18 +219,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.emptyStateContainer.setContent { EmptyView(ApplicationContext.getInstance(this).newAccount) } binding.emptyStateContainer.setContent { EmptyView(ApplicationContext.getInstance(this).newAccount) }
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 {
@ -250,6 +233,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
@ -428,34 +431,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()
@ -481,52 +456,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
@ -537,7 +480,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (event.recipient.isLocalNumber) { if (event.recipient.isLocalNumber) {
updateProfileButton() updateProfileButton()
} else { } else {
homeViewModel.tryUpdateChannel() homeViewModel.tryReload()
} }
} }
@ -703,7 +646,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()
} }
} }
@ -778,8 +721,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
text(getString(R.string.hide_message_requests)) text(getString(R.string.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) {
val offset = if (hasHeaderView()) position - 1 else position is HeaderFooterViewHolder -> {
val thread = data[offset] holder.binding.run {
val isTyping = typingThreadIDs.contains(thread.threadId) messageRequests?.let {
holder.view.bind(thread, isTyping, glide) unreadCountTextView.text = it.count
timestampTextView.text = it.timestamp
}
}
}
is ConversationViewHolder -> {
val offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId)
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 contentResolver: ContentResolver,
private val prefs: TextSecurePreferences,
@ApplicationContextQualifier private val context: Context,
) : ViewModel() {
// SharedFlow that emits whenever the user asks us to reload the conversation
private val manualReloadTrigger = MutableSharedFlow<Unit>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
private val executor = viewModelScope + SupervisorJob() /**
private var lastContext: WeakReference<Context>? = null * A [StateFlow] that emits the list of threads and the typing status of each thread.
private var updateJobs: MutableList<Job> = mutableListOf() *
* 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 val _conversations = MutableLiveData<List<ThreadRecord>>() private fun hasHiddenMessageRequests() = TextSecurePreferences.events
val conversations: LiveData<List<ThreadRecord>> = _conversations .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
.flowOn(Dispatchers.IO)
.map { prefs.hasHiddenMessageRequests() }
.onStart { emit(prefs.hasHiddenMessageRequests()) }
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED) private fun observeTypingStatus(): Flow<Set<Long>> =
ApplicationContext.getInstance(context).typingStatusRepository
.typingThreads
.asFlow()
.onStart { emit(emptySet()) }
.distinctUntilChanged()
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit) private fun messageRequests() = combine(
unapprovedConversationCount(),
hasHiddenMessageRequests(),
latestUnapprovedConversationTimestamp(),
::createMessageRequests
)
fun getObservable(context: Context): LiveData<List<ThreadRecord>> { private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
// If the context has changed (eg. the activity gets recreated) then .map { threadDb.unapprovedConversationCount }
// 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( private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
executor.launch(Dispatchers.IO) { .map { threadDb.latestUnapprovedConversationTimestamp }
context.contentResolver
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI) @Suppress("OPT_IN_USAGE")
.onEach { listUpdateChannel.trySend(Unit) } private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
.collect() .mapLatest { _ ->
} threadDb.approvedConversationList.use { openCursor ->
) threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
updateJobs.add( }
executor.launch(Dispatchers.IO) {
for (update in listUpdateChannel) {
threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor)
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() {
expand() super.onAttachedToWindow()
Handler().postDelayed({
collapse() startAnimation()
Handler().postDelayed({ }
performAnimation()
}, dotAnimationRepeatInterval) override fun onDetachedFromWindow() {
}, 1000) super.onDetachedFromWindow()
stopAnimation()
}
private fun startAnimation() {
job?.cancel()
job = GlobalScope.launch {
withContext(Dispatchers.Main) {
while (isActive) {
delay(dotAnimationStartDelay)
expand()
delay(EXPAND_ANIM_DELAY_MILLS)
collapse()
delay(dotAnimationRepeatInterval)
}
}
}
}
private fun stopAnimation() {
job?.cancel()
job = null
} }
private fun expand() { 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

@ -17,6 +17,7 @@ import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.Bencode
import org.session.libsession.utilities.bencode.BencodeList import org.session.libsession.utilities.bencode.BencodeList
import org.session.libsession.utilities.bencode.BencodeString import org.session.libsession.utilities.bencode.BencodeString
@ -28,7 +29,6 @@ import javax.inject.Inject
private const val TAG = "PushHandler" private const val TAG = "PushHandler"
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) { class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
private val sodium = LazySodiumAndroid(SodiumAndroid())
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
fun onPush(dataMap: Map<String, String>?) { fun onPush(dataMap: Map<String, String>?) {

View File

@ -18,6 +18,8 @@ import org.session.libsession.messaging.sending_receiving.notifications.Subscrip
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.Version import org.session.libsession.snode.Version
@ -34,8 +36,6 @@ private const val maxRetryCount = 4
@Singleton @Singleton
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) { class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
private val sodium = LazySodiumAndroid(SodiumAndroid())
fun register( fun register(
device: Device, device: Device,
token: String, token: String,

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

@ -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,29 +86,34 @@ 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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto conf = ptrToConfigBase(env, thiz); std::lock_guard lock{util::util_mutex_};
size_t number = env->GetArrayLength(to_merge); auto conf = ptrToConfigBase(env, thiz);
std::vector<std::pair<std::string,session::ustring>> configs = {}; size_t number = env->GetArrayLength(to_merge);
for (int i = 0; i < number; i++) { std::vector<std::pair<std::string, session::ustring>> configs = {};
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i); for (int i = 0; i < number; i++) {
auto pair = extractHashAndData(env, jElement); auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
configs.push_back(pair); auto pair = extractHashAndData(env, jElement);
} configs.push_back(pair);
auto returned = conf->merge(configs); }
auto string_stack = util::build_string_stack(env, returned); auto returned = conf->merge(configs);
return string_stack; auto string_stack = util::build_string_stack(env, returned);
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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto conf = ptrToConfigBase(env, thiz); std::lock_guard lock{util::util_mutex_};
std::vector<std::pair<std::string, session::ustring>> configs = {extractHashAndData(env, to_merge)}; auto conf = ptrToConfigBase(env, thiz);
auto returned = conf->merge(configs); std::vector<std::pair<std::string, session::ustring>> configs = {
auto string_stack = util::build_string_stack(env, returned); extractHashAndData(env, to_merge)};
return string_stack; auto returned = conf->merge(configs);
auto string_stack = util::build_string_stack(env, returned);
return string_stack;
});
} }
#pragma clang diagnostic pop #pragma clang diagnostic pop

View File

@ -1,100 +1,121 @@
#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) {
std::lock_guard lock{util::util_mutex_}; // If an exception is thrown, return nullptr
auto contacts = ptrToContacts(env, thiz); return jni_utils::run_catching_cxx_exception_or<jobject>(
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); [=]() -> jobject {
auto contact = contacts->get(session_id_chars); std::lock_guard lock{util::util_mutex_};
env->ReleaseStringUTFChars(session_id, session_id_chars); auto contacts = ptrToContacts(env, thiz);
if (!contact) return nullptr; auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
jobject j_contact = serialize_contact(env, contact.value()); auto contact = contacts->get(session_id_chars);
return j_contact; env->ReleaseStringUTFChars(session_id, session_id_chars);
if (!contact) return nullptr;
jobject j_contact = serialize_contact(env, contact.value());
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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto contacts = ptrToContacts(env, thiz); std::lock_guard lock{util::util_mutex_};
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); auto contacts = ptrToContacts(env, thiz);
auto contact = contacts->get_or_construct(session_id_chars); auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
env->ReleaseStringUTFChars(session_id, session_id_chars); auto contact = contacts->get_or_construct(session_id_chars);
return serialize_contact(env, contact); env->ReleaseStringUTFChars(session_id, session_id_chars);
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) {
std::lock_guard lock{util::util_mutex_}; jni_utils::run_catching_cxx_exception_or_throws<void>(env, [=] {
auto contacts = ptrToContacts(env, thiz); std::lock_guard lock{util::util_mutex_};
auto contact_info = deserialize_contact(env, contact, contacts); auto contacts = ptrToContacts(env, thiz);
contacts->set(contact_info); auto contact_info = deserialize_contact(env, contact, contacts);
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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jboolean>(env, [=] {
auto contacts = ptrToContacts(env, thiz); std::lock_guard lock{util::util_mutex_};
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr); auto contacts = ptrToContacts(env, thiz);
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
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
#pragma ide diagnostic ignored "bugprone-reserved-identifier" #pragma ide diagnostic ignored "bugprone-reserved-identifier"
JNIEXPORT jobject JNICALL 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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); std::lock_guard lock{util::util_mutex_};
auto* contacts = new session::config::Contacts(secret_key, std::nullopt); auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); std::lock_guard lock{util::util_mutex_};
auto initial = util::ustring_from_bytes(env, initial_dump); auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
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) {
std::lock_guard lock{util::util_mutex_}; return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
auto contacts = ptrToContacts(env, thiz); std::lock_guard lock{util::util_mutex_};
jclass stack = env->FindClass("java/util/Stack"); auto contacts = ptrToContacts(env, thiz);
jmethodID init = env->GetMethodID(stack, "<init>", "()V"); jclass stack = env->FindClass("java/util/Stack");
jobject our_stack = env->NewObject(stack, init); jmethodID init = env->GetMethodID(stack, "<init>", "()V");
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); jobject our_stack = env->NewObject(stack, init);
for (const auto& contact : *contacts) { jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
auto contact_obj = serialize_contact(env, contact); for (const auto &contact: *contacts) {
env->CallObjectMethod(our_stack, push, contact_obj); auto contact_obj = serialize_contact(env, contact);
} 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

@ -0,0 +1,9 @@
package org.session.libsession.messaging
interface LastSentTimestampCache {
fun getTimestamp(threadId: Long): Long?
fun submitTimestamp(threadId: Long, timestamp: Long)
fun delete(threadId: Long, timestamps: List<Long>)
fun delete(threadId: Long, timestamp: Long) = delete(threadId, listOf(timestamp))
fun refresh(threadId: Long)
}

View File

@ -13,7 +13,8 @@ class MessagingModuleConfiguration(
val device: Device, val device: Device,
val messageDataProvider: MessageDataProvider, val messageDataProvider: MessageDataProvider,
val getUserED25519KeyPair: () -> KeyPair?, val getUserED25519KeyPair: () -> KeyPair?,
val configFactory: ConfigFactoryProtocol val configFactory: ConfigFactoryProtocol,
val lastSentTimestampCache: LastSentTimestampCache
) { ) {
companion object { companion object {

View File

@ -21,6 +21,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod
import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.OnionResponse import org.session.libsession.snode.OnionResponse
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
@ -48,7 +49,6 @@ object OpenGroupApi {
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1) val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>() private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
private var hasUpdatedLastOpenDate = false private var hasUpdatedLastOpenDate = false
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
private val timeSinceLastOpen by lazy { private val timeSinceLastOpen by lazy {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context) val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)

View File

@ -8,6 +8,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
@ -17,8 +18,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
object MessageDecrypter { object MessageDecrypter {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
/** /**
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`. * Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
* *

View File

@ -7,6 +7,7 @@ import com.goterl.lazysodium.interfaces.Sign
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -14,8 +15,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
object MessageEncrypter { object MessageEncrypter {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
/** /**
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`. * Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
* *

View File

@ -73,6 +73,7 @@ object MessageSender {
// Convenience // Convenience
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> { fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!)
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
sendToOpenGroupDestination(destination, message) sendToOpenGroupDestination(destination, message)
} else { } else {
@ -372,6 +373,7 @@ object MessageSender {
// Result Handling // Result Handling
private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) { private fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, openGroupSentTimestamp)
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val timestamp = message.sentTimestamp!! val timestamp = message.sentTimestamp!!

View File

@ -290,6 +290,7 @@ fun MessageReceiver.handleVisibleMessage(
): Long? { ): Long? {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
message.takeIf { it.isSenderSelf }?.sentTimestamp?.let { MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(threadId, it) }
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
val messageSender: String? = message.sender val messageSender: String? = message.sender
@ -410,12 +411,7 @@ fun MessageReceiver.handleVisibleMessage(
message.hasMention = listOf(userPublicKey, userBlindedKey) message.hasMention = listOf(userPublicKey, userBlindedKey)
.filterNotNull() .filterNotNull()
.any { key -> .any { key ->
return@any ( messageText?.contains("@$key") == true || key == (quoteModel?.author?.serialize() ?: "")
messageText != null &&
messageText.contains("@$key")
) || (
(quoteModel?.author?.serialize() ?: "") == key
)
} }
// Persist the message // Persist the message

View File

@ -14,7 +14,7 @@ import org.whispersystems.curve25519.Curve25519
import kotlin.experimental.xor import kotlin.experimental.xor
object SodiumUtilities { object SodiumUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) } private val curve by lazy { Curve25519.getInstance(Curve25519.BEST) }
private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes private const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes

View File

@ -3,8 +3,6 @@
package org.session.libsession.snode package org.session.libsession.snode
import android.os.Build import android.os.Build
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.exceptions.SodiumException import com.goterl.lazysodium.exceptions.SodiumException
import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.PwHash import com.goterl.lazysodium.interfaces.PwHash
@ -19,6 +17,7 @@ import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
@ -41,7 +40,6 @@ import kotlin.collections.set
import kotlin.properties.Delegates.observable import kotlin.properties.Delegates.observable
object SnodeAPI { object SnodeAPI {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
internal val database: LokiAPIDatabaseProtocol internal val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage get() = SnodeModule.shared.storage
private val broadcaster: Broadcaster private val broadcaster: Broadcaster

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;