mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge remote-tracking branch 'upstream/dev' into ses-637-voice-message-keeps-playing
This commit is contained in:
commit
2c90717235
@ -31,8 +31,8 @@ configurations.all {
|
||||
exclude module: "commons-logging"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 371
|
||||
def canonicalVersionName = "1.18.2"
|
||||
def canonicalVersionCode = 373
|
||||
def canonicalVersionName = "1.18.4"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -57,6 +57,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.LastSentTimestampCache;
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.database.Storage;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@ -149,6 +150,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
@Inject TextSecurePreferences textSecurePreferences;
|
||||
@Inject PushRegistry pushRegistry;
|
||||
@Inject ConfigFactory configFactory;
|
||||
@Inject LastSentTimestampCache lastSentTimestampCache;
|
||||
CallMessageProcessor callMessageProcessor;
|
||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||
|
||||
@ -218,7 +220,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
||||
device,
|
||||
messageDataProvider,
|
||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||
configFactory
|
||||
configFactory,
|
||||
lastSentTimestampCache
|
||||
);
|
||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||
Log.i(TAG, "onCreate()");
|
||||
|
@ -21,6 +21,7 @@ import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorIndexOutOfBoundsException;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
@ -145,6 +146,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
||||
}
|
||||
};
|
||||
private MediaItemAdapter adapter;
|
||||
|
||||
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
public void onModified(Recipient recipient) {
|
||||
Util.runOnMain(this::updateActionBar);
|
||||
@ -285,9 +280,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
mediaPager = findViewById(R.id.media_pager);
|
||||
mediaPager.setOffscreenPageLimit(1);
|
||||
|
||||
viewPagerListener = new ViewPagerListener();
|
||||
mediaPager.addOnPageChangeListener(viewPagerListener);
|
||||
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
@ -378,7 +370,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
if (conversationRecipient != null) {
|
||||
getSupportLoaderManager().restartLoader(0, null, this);
|
||||
} 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) {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
@ -506,13 +499,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
}
|
||||
|
||||
private @Nullable MediaItem getCurrentMediaItem() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (adapter == null) return null;
|
||||
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
||||
}
|
||||
|
||||
public static boolean isContentTypeSupported(final String contentType) {
|
||||
@ -526,23 +514,28 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
||||
if (data != null) {
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
if (data == null) return;
|
||||
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
mediaPager.removeOnPageChangeListener(viewPagerListener);
|
||||
|
||||
if (restartItem >= 0 || data.second >= 0) {
|
||||
int item = restartItem >= 0 ? restartItem : data.second;
|
||||
mediaPager.setCurrentItem(item);
|
||||
adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "one of restartItem "+restartItem+" and data.second "+data.second+" would cause OOB exception");
|
||||
}
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 && restartItem < adapter.getCount() ? restartItem : Math.max(Math.min(data.second, adapter.getCount() - 1), 0);
|
||||
|
||||
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);
|
||||
currentPage = position;
|
||||
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
if (adapter == null) return;
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
|
||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||
updateActionBar();
|
||||
}
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this);
|
||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||
updateActionBar();
|
||||
}
|
||||
|
||||
|
||||
public void onPageUnselected(int position) {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
if (adapter == null) return;
|
||||
|
||||
if (adapter != null) {
|
||||
try {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.removeListener(MediaPreviewActivity.this);
|
||||
|
||||
adapter.pause(position);
|
||||
} catch (CursorIndexOutOfBoundsException e) {
|
||||
throw new RuntimeException("position = " + position + " leftIsRecent = " + leftIsRecent, e);
|
||||
}
|
||||
|
||||
adapter.pause(position);
|
||||
}
|
||||
|
||||
@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 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<>();
|
||||
|
||||
@ -675,7 +668,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
private final Cursor cursor;
|
||||
private final boolean leftIsRecent;
|
||||
|
||||
private boolean active;
|
||||
private int autoPlayPosition;
|
||||
|
||||
CursorPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@ -690,15 +682,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (!active) return 0;
|
||||
else return cursor.getCount();
|
||||
return cursor.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -771,8 +757,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
if (leftIsRecent) return position;
|
||||
else return cursor.getCount() - 1 - position;
|
||||
int unclamped = leftIsRecent ? position : 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 {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(int position);
|
||||
abstract static class MediaItemAdapter extends PagerAdapter {
|
||||
abstract MediaItem getMediaItemFor(int position);
|
||||
abstract void pause(int position);
|
||||
@Nullable abstract View getPlaybackControls(int position);
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.attachment = attachment;
|
||||
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
||||
this.port = socket.getLocalPort();
|
||||
|
@ -5,6 +5,8 @@ import android.text.TextUtils
|
||||
import com.google.protobuf.ByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
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.AttachmentId
|
||||
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) {
|
||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||
else DatabaseComponent.get(context).mmsDatabase()
|
||||
val (threadId, timestamp) = runCatching { messagingDatabase.getMessageRecord(messageID).run { threadId to timestamp } }.getOrNull() ?: (null to null)
|
||||
|
||||
messagingDatabase.deleteMessage(messageID)
|
||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessage(messageID, 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) {
|
||||
@ -195,12 +202,17 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||
else DatabaseComponent.get(context).mmsDatabase()
|
||||
|
||||
val messages = messageIDs.mapNotNull { runCatching { messagingDatabase.getMessageRecord(it) }.getOrNull() }
|
||||
|
||||
// Perform local delete
|
||||
messagingDatabase.deleteMessages(messageIDs.toLongArray(), threadId)
|
||||
|
||||
// Perform online delete
|
||||
DatabaseComponent.get(context).lokiMessageDatabase().deleteMessages(messageIDs)
|
||||
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? {
|
||||
|
@ -45,7 +45,8 @@ public class AudioRecorder {
|
||||
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||
try {
|
||||
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();
|
||||
|
@ -122,7 +122,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
||||
|
||||
glide.clear(imageView)
|
||||
|
||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||
glide.load(signalProfilePicture)
|
||||
|
@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
if (hexEncodedSeed == null) {
|
||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
|
||||
val appContext = applicationContext
|
||||
val loadFileContents: (String) -> String = { fileName ->
|
||||
MnemonicUtilities.loadFileContents(this, fileName)
|
||||
MnemonicUtilities.loadFileContents(appContext, fileName)
|
||||
}
|
||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||
}
|
||||
@ -325,7 +327,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
},
|
||||
onAttachmentNeedsDownload = { attachmentId, mmsId ->
|
||||
// Start download (on IO thread)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||
}
|
||||
@ -335,8 +336,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
)
|
||||
adapter.visibleMessageViewDelegate = this
|
||||
|
||||
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView if we're
|
||||
// already near the the bottom and the data changes.
|
||||
// Register an AdapterDataObserver to scroll us to the bottom of the RecyclerView for if
|
||||
// we're already near the the bottom and the data changes.
|
||||
adapter.registerAdapterDataObserver(ConversationAdapterDataObserver(binding?.conversationRecyclerView!!, adapter))
|
||||
|
||||
adapter
|
||||
@ -374,7 +375,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
const val PICK_GIF = 10
|
||||
const val PICK_FROM_LIBRARY = 12
|
||||
const val INVITE_CONTACTS = 124
|
||||
|
||||
}
|
||||
// endregion
|
||||
|
||||
@ -572,7 +572,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
binding!!.conversationRecyclerView.layoutManager = layoutManager
|
||||
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
binding!!.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
// The unreadCount check is to prevent us scrolling to the bottom when we first enter a conversation
|
||||
@ -833,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
override fun onDestroy() {
|
||||
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
||||
cancelVoiceMessage()
|
||||
tearDownRecipientObserver()
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
@ -1021,7 +1022,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
override fun showVoiceMessageUI() {
|
||||
binding?.inputBarRecordingView?.show()
|
||||
binding?.inputBarRecordingView?.show(lifecycleScope)
|
||||
binding?.inputBar?.alpha = 0.0f
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
animation.duration = 250L
|
||||
@ -1885,7 +1886,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
||||
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
|
||||
|
||||
|
@ -209,20 +209,6 @@ class ConversationAdapter(
|
||||
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?) {
|
||||
super.changeCursor(cursor)
|
||||
|
||||
@ -243,11 +229,6 @@ class ConversationAdapter(
|
||||
toDeselect.iterator().forEach { (pos, record) ->
|
||||
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? {
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
||||
|
||||
@ -12,6 +13,7 @@ class ConversationLoader(
|
||||
) : AbstractCursorLoader(context) {
|
||||
|
||||
override fun getCursor(): Cursor {
|
||||
MessagingModuleConfiguration.shared.lastSentTimestampCache.refresh(threadID)
|
||||
return DatabaseComponent.get(context).mmsSmsDatabase().getConversation(threadID, reverse)
|
||||
}
|
||||
}
|
@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
|
||||
import android.animation.IntEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
@ -14,6 +12,11 @@ import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
@ -25,10 +28,10 @@ import java.util.Date
|
||||
class InputBarRecordingView : RelativeLayout {
|
||||
private lateinit var binding: ViewInputBarRecordingBinding
|
||||
private var startTimestamp = 0L
|
||||
private val snHandler = Handler(Looper.getMainLooper())
|
||||
private var dotViewAnimation: ValueAnimator? = null
|
||||
private var pulseAnimation: ValueAnimator? = null
|
||||
var delegate: InputBarRecordingViewDelegate? = null
|
||||
private var timerJob: Job? = null
|
||||
|
||||
val lockView: LinearLayout
|
||||
get() = binding.lockView
|
||||
@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
|
||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
binding.inputBarMiddleContentContainer.disableClipping()
|
||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||
|
||||
}
|
||||
|
||||
fun show() {
|
||||
fun show(scope: CoroutineScope) {
|
||||
startTimestamp = Date().time
|
||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||
binding.inputBarCancelButton.alpha = 0.0f
|
||||
@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
|
||||
animateDotView()
|
||||
pulse()
|
||||
animateLockViewUp()
|
||||
updateTimer()
|
||||
startTimer(scope)
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
|
||||
}
|
||||
animation.start()
|
||||
delegate?.handleVoiceMessageUIHidden()
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
private fun startTimer(scope: CoroutineScope) {
|
||||
timerJob?.cancel()
|
||||
timerJob = scope.launch {
|
||||
while (isActive) {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
private fun animateDotView() {
|
||||
@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
|
||||
animation.start()
|
||||
}
|
||||
|
||||
private fun updateTimer() {
|
||||
val duration = (Date().time - startTimestamp) / 1000L
|
||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||
snHandler.postDelayed({ updateTimer() }, 500)
|
||||
}
|
||||
|
||||
fun lock() {
|
||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||
fadeOutAnimation.duration = 250L
|
||||
|
@ -22,7 +22,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
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.Log
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.LastSentTimestampCache
|
||||
import org.thoughtcrime.securesms.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
@ -73,6 +73,7 @@ class VisibleMessageView : LinearLayout {
|
||||
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
|
||||
@Inject lateinit var smsDb: SmsDatabase
|
||||
@Inject lateinit var mmsDb: MmsDatabase
|
||||
@Inject lateinit var lastSentTimestampCache: LastSentTimestampCache
|
||||
|
||||
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
||||
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 ---
|
||||
|
||||
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 -----
|
||||
if (!scheduledToDisappear) {
|
||||
// If this isn't a disappearing message then we never show the timer
|
||||
@ -319,9 +316,11 @@ class VisibleMessageView : LinearLayout {
|
||||
} else {
|
||||
// ..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.
|
||||
binding.messageStatusTextView.isVisible = isLastSentMessage
|
||||
binding.messageStatusImageView.isVisible = isLastSentMessage
|
||||
if (isLastSentMessage) { binding.messageStatusImageView.bringToFront() }
|
||||
val lastSentTimestamp = lastSentTimestampCache.getTimestamp(message.threadId)
|
||||
val isLastSent = lastSentTimestamp == message.timestamp
|
||||
binding.messageStatusTextView.isVisible = isLastSent
|
||||
binding.messageStatusImageView.isVisible = isLastSent
|
||||
if (isLastSent) { binding.messageStatusImageView.bringToFront() }
|
||||
}
|
||||
}
|
||||
else // ----- Case iii.) Message is outgoing AND scheduled to disappear -----
|
||||
|
@ -40,18 +40,16 @@ object ResendMessageUtilities {
|
||||
message.recipient = messageRecord.recipient.address.serialize()
|
||||
}
|
||||
message.threadID = messageRecord.threadId
|
||||
if (messageRecord.isMms) {
|
||||
val mmsMessageRecord = messageRecord as MmsMessageRecord
|
||||
if (mmsMessageRecord.linkPreviews.isNotEmpty()) {
|
||||
message.linkPreview = LinkPreview.from(mmsMessageRecord.linkPreviews[0])
|
||||
}
|
||||
if (mmsMessageRecord.quote != null) {
|
||||
message.quote = Quote.from(mmsMessageRecord.quote!!.quoteModel)
|
||||
if (userBlindedKey != null && messageRecord.quote!!.author.serialize() == TextSecurePreferences.getLocalNumber(context)) {
|
||||
message.quote!!.publicKey = userBlindedKey
|
||||
if (messageRecord.isMms && messageRecord is MmsMessageRecord) {
|
||||
messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) }
|
||||
messageRecord.quote?.quoteModel?.let {
|
||||
message.quote = Quote.from(it)?.apply {
|
||||
if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) {
|
||||
publicKey = userBlindedKey
|
||||
}
|
||||
}
|
||||
}
|
||||
message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments())
|
||||
message.addSignalAttachments(messageRecord.slideDeck.asAttachments())
|
||||
}
|
||||
val sentTimestamp = message.sentTimestamp
|
||||
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
|
||||
|
@ -1,10 +1,9 @@
|
||||
package org.thoughtcrime.securesms.crypto
|
||||
|
||||
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.KeyPair
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
@ -13,8 +12,6 @@ import org.session.libsignal.utilities.Hex
|
||||
|
||||
object KeyPairUtilities {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
fun generate(): KeyPairGenerationResult {
|
||||
val seed = sodium.randomBytesBuf(16)
|
||||
try {
|
||||
|
@ -5,9 +5,9 @@ import android.content.Context
|
||||
import org.session.libsession.utilities.Debouncer
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
|
||||
class ConversationNotificationDebouncer(private val context: Context) {
|
||||
class ConversationNotificationDebouncer(private val context: ApplicationContext) {
|
||||
private val threadIDs = mutableSetOf<Long>()
|
||||
private val handler = (context.applicationContext as ApplicationContext).conversationListNotificationHandler
|
||||
private val handler = context.conversationListNotificationHandler
|
||||
private val debouncer = Debouncer(handler, 100)
|
||||
|
||||
companion object {
|
||||
@ -17,20 +17,28 @@ class ConversationNotificationDebouncer(private val context: Context) {
|
||||
@Synchronized
|
||||
fun get(context: Context): ConversationNotificationDebouncer {
|
||||
if (::shared.isInitialized) { return shared }
|
||||
shared = ConversationNotificationDebouncer(context)
|
||||
shared = ConversationNotificationDebouncer(context.applicationContext as ApplicationContext)
|
||||
return shared
|
||||
}
|
||||
}
|
||||
|
||||
fun notify(threadID: Long) {
|
||||
threadIDs.add(threadID)
|
||||
synchronized(threadIDs) {
|
||||
threadIDs.add(threadID)
|
||||
}
|
||||
|
||||
debouncer.publish { 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)
|
||||
}
|
||||
threadIDs.clear()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1147,13 +1147,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
}
|
||||
}
|
||||
|
||||
fun readerFor(cursor: Cursor?): Reader {
|
||||
return Reader(cursor)
|
||||
}
|
||||
fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote)
|
||||
|
||||
fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader {
|
||||
return OutgoingMessageReader(message, threadId)
|
||||
}
|
||||
fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId)
|
||||
|
||||
fun setQuoteMissing(messageId: Long): Int {
|
||||
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?
|
||||
get() = if (cursor == null || !cursor.moveToNext()) null else current
|
||||
val current: MessageRecord
|
||||
@ -1226,7 +1222,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) {
|
||||
getNotificationMmsMessageRecord(cursor)
|
||||
} else {
|
||||
getMediaMmsMessageRecord(cursor)
|
||||
getMediaMmsMessageRecord(cursor, getQuote)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1253,20 +1249,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
DELIVERY_RECEIPT_COUNT
|
||||
)
|
||||
)
|
||||
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT))
|
||||
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
|
||||
val readReceiptCount = if (isReadReceiptsEnabled(context)) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
|
||||
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
|
||||
if (!isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0
|
||||
}
|
||||
var contentLocationBytes: ByteArray? = null
|
||||
var transactionIdBytes: ByteArray? = null
|
||||
if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes(
|
||||
contentLocation
|
||||
)
|
||||
if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes(
|
||||
transactionId
|
||||
)
|
||||
val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
|
||||
val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
|
||||
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
|
||||
return NotificationMmsMessageRecord(
|
||||
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 dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
|
||||
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 previewAttachments }
|
||||
)
|
||||
val quote = getQuote(cursor)
|
||||
val quote = if (getQuote) getQuote(cursor) else null
|
||||
val reactions = get(context).reactionDatabase().getReactions(cursor)
|
||||
return MediaMmsMessageRecord(
|
||||
id, recipient, recipient,
|
||||
@ -1381,7 +1367,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
|
||||
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
|
||||
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 quoteMissing = retrievedQuote == null
|
||||
val quoteDeck = (
|
||||
|
@ -9,7 +9,11 @@ public interface MmsSmsColumns {
|
||||
public static final String THREAD_ID = "thread_id";
|
||||
public static final String READ = "read";
|
||||
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_DEVICE_ID = "address_device_id";
|
||||
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
|
||||
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
||||
|
@ -97,9 +97,13 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
|
||||
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)) {
|
||||
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||
MmsSmsDatabase.Reader reader = readerFor(cursor, getQuote);
|
||||
|
||||
MessageRecord messageRecord;
|
||||
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
|
||||
@ -295,15 +299,7 @@ public class MmsSmsDatabase extends Database {
|
||||
return identifiedMessages;
|
||||
}
|
||||
|
||||
public long getLastSentMessageFromSender(long threadId, String serializedAuthor) {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public long getLastOutgoingTimestamp(long threadId) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
@ -311,8 +307,13 @@ public class MmsSmsDatabase extends Database {
|
||||
try (Cursor cursor = queryTables(PROJECTION, selection, order, null)) {
|
||||
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
|
||||
MessageRecord messageRecord;
|
||||
long attempts = 0;
|
||||
long maxAttempts = 20;
|
||||
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) {
|
||||
return new Reader(cursor);
|
||||
return readerFor(cursor, true);
|
||||
}
|
||||
|
||||
public Reader readerFor(@NonNull Cursor cursor, boolean getQuote) {
|
||||
return new Reader(cursor, getQuote);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -661,11 +666,13 @@ public class MmsSmsDatabase extends Database {
|
||||
public class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final boolean getQuote;
|
||||
private SmsDatabase.Reader smsReader;
|
||||
private MmsDatabase.Reader mmsReader;
|
||||
|
||||
public Reader(Cursor cursor) {
|
||||
public Reader(Cursor cursor, boolean getQuote) {
|
||||
this.cursor = cursor;
|
||||
this.getQuote = getQuote;
|
||||
}
|
||||
|
||||
private SmsDatabase.Reader getSmsReader() {
|
||||
@ -678,7 +685,7 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
private MmsDatabase.Reader getMmsReader() {
|
||||
if (mmsReader == null) {
|
||||
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor);
|
||||
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor, getQuote);
|
||||
}
|
||||
|
||||
return mmsReader;
|
||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return cursor == null ? 0 : cursor.getCount();
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
if (cursor == null || !cursor.moveToNext())
|
||||
return null;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
@ -8,7 +9,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||
|
||||
class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: PlaceholderAvatarPhoto,
|
||||
@ -16,14 +17,14 @@ class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawa
|
||||
height: Int,
|
||||
options: Options
|
||||
): LoadData<BitmapDrawable> {
|
||||
return LoadData(model, PlaceholderAvatarFetcher(model.context, model))
|
||||
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
|
||||
}
|
||||
|
||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||
|
||||
class Factory() : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
class Factory(private val appContext: Context) : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||
return PlaceholderAvatarLoader()
|
||||
return PlaceholderAvatarLoader(appContext)
|
||||
}
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
@ -18,19 +15,18 @@ import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityHomeBinding
|
||||
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
@ -76,14 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
|
||||
import org.thoughtcrime.securesms.showMuteDialog
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.IP2Country
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.push
|
||||
import org.thoughtcrime.securesms.util.show
|
||||
import org.thoughtcrime.securesms.util.themeState
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@ -99,7 +92,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
private lateinit var glide: GlideRequests
|
||||
private var broadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
@Inject lateinit var threadDb: ThreadDatabase
|
||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||
@ -117,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
get() = textSecurePreferences.getLocalNumber()!!
|
||||
|
||||
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 ->
|
||||
@ -189,7 +181,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
}
|
||||
setupMessageRequestsBanner()
|
||||
// Set up recycler view
|
||||
binding.globalSearchInputLayout.listener = this
|
||||
homeAdapter.setHasStableIds(true)
|
||||
@ -205,18 +196,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
// Set up empty state view
|
||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
startObservingUpdates()
|
||||
|
||||
// Set up new conversation button
|
||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||
// Observe blocked contacts changed events
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
this.broadcastReceiver = broadcastReceiver
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
||||
|
||||
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||
lifecycleScope.launch {
|
||||
@ -227,6 +210,26 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to threads and update the UI
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
homeViewModel.data
|
||||
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
|
||||
.collectLatest { data ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||
val offsetTop = if(firstPos >= 0) {
|
||||
manager.findViewByPosition(firstPos)?.let { view ->
|
||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||
} ?: 0
|
||||
} else 0
|
||||
homeAdapter.data = data
|
||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||
updateEmptyState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launchWhenStarted {
|
||||
launch(Dispatchers.IO) {
|
||||
// Double check that the long poller is up
|
||||
@ -332,34 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
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() {
|
||||
binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
|
||||
&& textSecurePreferences.getHasLegacyConfig()
|
||||
@ -385,52 +360,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
||||
}
|
||||
}
|
||||
|
||||
// If the theme hasn't changed then start observing updates again (if it does change then we
|
||||
// will recreate the activity resulting in it responding to changes multiple times)
|
||||
if (currentThemeState == textSecurePreferences.themeState() && !homeViewModel.getObservable(this).hasActiveObservers()) {
|
||||
startObservingUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||
|
||||
homeViewModel.getObservable(this).removeObservers(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val broadcastReceiver = this.broadcastReceiver
|
||||
if (broadcastReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
super.onDestroy()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun startObservingUpdates() {
|
||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||
val offsetTop = if(firstPos >= 0) {
|
||||
manager.findViewByPosition(firstPos)?.let { view ->
|
||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||
} ?: 0
|
||||
} else 0
|
||||
homeAdapter.data = newData
|
||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||
setupMessageRequestsBanner()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEmptyState() {
|
||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||
@ -441,7 +384,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
if (event.recipient.isLocalNumber) {
|
||||
updateProfileButton()
|
||||
} else {
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,7 +555,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
storage.setPinned(threadId, pinned)
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -687,8 +630,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
text("Hide message requests?")
|
||||
button(R.string.yes) {
|
||||
textSecurePreferences.setHasHiddenMessageRequests()
|
||||
setupMessageRequestsBanner()
|
||||
homeViewModel.tryUpdateChannel()
|
||||
homeViewModel.tryReload()
|
||||
}
|
||||
button(R.string.no)
|
||||
}
|
||||
|
@ -9,14 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
class HomeAdapter(
|
||||
private val context: Context,
|
||||
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 {
|
||||
|
||||
companion object {
|
||||
@ -24,23 +28,32 @@ class HomeAdapter(
|
||||
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: List<ThreadRecord>
|
||||
get() = _data.toList()
|
||||
var data: HomeViewModel.Data = HomeViewModel.Data()
|
||||
set(newData) {
|
||||
val previousData = _data.toList()
|
||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
||||
if (field === newData) return
|
||||
|
||||
messageRequests = newData.messageRequests
|
||||
|
||||
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||
val diffResult = DiffUtil.calculateDiff(diff)
|
||||
_data = newData
|
||||
field = newData
|
||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||
}
|
||||
|
||||
fun hasHeaderView(): Boolean = header != null
|
||||
fun hasHeaderView(): Boolean = messageRequests != null
|
||||
|
||||
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) {
|
||||
notifyItemRangeInserted(position + headerCount, count)
|
||||
@ -61,23 +74,19 @@ class HomeAdapter(
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (hasHeaderView() && position == 0) return NO_ID
|
||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||
return _data[offsetPosition].threadId
|
||||
return data.threads[offsetPosition].threadId
|
||||
}
|
||||
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) {
|
||||
if (field == value) { return }
|
||||
|
||||
field = value
|
||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||
when (viewType) {
|
||||
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 -> {
|
||||
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) {
|
||||
if (holder is ConversationViewHolder) {
|
||||
val offset = if (hasHeaderView()) position - 1 else position
|
||||
val thread = data[offset]
|
||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
||||
holder.view.bind(thread, isTyping, glide)
|
||||
when (holder) {
|
||||
is HeaderFooterViewHolder -> {
|
||||
holder.binding.run {
|
||||
messageRequests?.let {
|
||||
unreadCountTextView.text = it.count
|
||||
timestampTextView.text = it.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
is ConversationViewHolder -> {
|
||||
val offset = if (hasHeaderView()) position - 1 else position
|
||||
val thread = data.threads[offset]
|
||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
||||
holder.view.bind(thread, isTyping, glide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is ConversationViewHolder) {
|
||||
holder.view.recycle()
|
||||
} else {
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,10 +130,9 @@ class HomeAdapter(
|
||||
if (hasHeaderView() && position == 0) HEADER
|
||||
else ITEM
|
||||
|
||||
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0
|
||||
override fun getItemCount(): Int = data.threads.size + if (hasHeaderView()) 1 else 0
|
||||
|
||||
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
}
|
||||
class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
@ -2,27 +2,26 @@ package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||
|
||||
class HomeDiffUtil(
|
||||
private val old: List<ThreadRecord>,
|
||||
private val new: List<ThreadRecord>,
|
||||
private val context: Context,
|
||||
private val configFactory: ConfigFactory
|
||||
private val old: HomeViewModel.Data,
|
||||
private val new: HomeViewModel.Data,
|
||||
private val context: Context,
|
||||
private val configFactory: ConfigFactory
|
||||
): DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = old.size
|
||||
override fun getOldListSize(): Int = old.threads.size
|
||||
|
||||
override fun getNewListSize(): Int = new.size
|
||||
override fun getNewListSize(): Int = new.threads.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
old[oldItemPosition].threadId == new[newItemPosition].threadId
|
||||
old.threads[oldItemPosition].threadId == new.threads[newItemPosition].threadId
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = old[oldItemPosition]
|
||||
val newItem = new[newItemPosition]
|
||||
val oldItem = old.threads[oldItemPosition]
|
||||
val newItem = new.threads[newItemPosition]
|
||||
|
||||
// return early to save getDisplayBody or expensive calls
|
||||
var isSameItem = true
|
||||
@ -47,7 +46,8 @@ class HomeDiffUtil(
|
||||
oldItem.isSent == newItem.isSent &&
|
||||
oldItem.isPending == newItem.isPending &&
|
||||
oldItem.lastSeen == newItem.lastSeen &&
|
||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true
|
||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
|
||||
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,71 +1,131 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
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.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.ThreadDatabase
|
||||
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 dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val contentResolver: ContentResolver,
|
||||
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
|
||||
private var updateJobs: MutableList<Job> = mutableListOf()
|
||||
/**
|
||||
* A [StateFlow] that emits the list of threads and the typing status of each thread.
|
||||
*
|
||||
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||
* whenever the conversation list changes.
|
||||
*/
|
||||
val data: StateFlow<Data?> = combine(
|
||||
observeConversationList(),
|
||||
observeTypingStatus(),
|
||||
messageRequests(),
|
||||
::Data
|
||||
)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
||||
private fun hasHiddenMessageRequests() = TextSecurePreferences.events
|
||||
.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>> {
|
||||
// If the context has changed (eg. the activity gets recreated) then
|
||||
// we need to cancel the old executors and recreate them to prevent
|
||||
// the app from triggering extra updates when data changes
|
||||
if (context != lastContext?.get()) {
|
||||
lastContext = WeakReference(context)
|
||||
updateJobs.forEach { it.cancel() }
|
||||
updateJobs.clear()
|
||||
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
|
||||
.map { threadDb.unapprovedConversationCount }
|
||||
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
context.contentResolver
|
||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
||||
.onEach { listUpdateChannel.trySend(Unit) }
|
||||
.collect()
|
||||
}
|
||||
)
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
for (update in listUpdateChannel) {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<ThreadRecord>()
|
||||
while (true) {
|
||||
threads += reader.next ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_conversations.value = threads
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
|
||||
.map { threadDb.latestUnapprovedConversationTimestamp }
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
|
||||
.mapLatest { _ ->
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
@ -17,11 +16,17 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityPathBinding
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.getColorFromAttr
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Snode
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
@ -184,6 +189,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var location: Location
|
||||
private var dotAnimationStartDelay: Long = 0
|
||||
private var dotAnimationRepeatInterval: Long = 0
|
||||
private var job: Job? = null
|
||||
|
||||
private val dotView by lazy {
|
||||
val result = PathDotView(context)
|
||||
@ -240,19 +246,38 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
||||
dotView.layoutParams = dotViewLayoutParams
|
||||
addView(dotView)
|
||||
Handler().postDelayed({
|
||||
performAnimation()
|
||||
}, dotAnimationStartDelay)
|
||||
}
|
||||
|
||||
private fun performAnimation() {
|
||||
expand()
|
||||
Handler().postDelayed({
|
||||
collapse()
|
||||
Handler().postDelayed({
|
||||
performAnimation()
|
||||
}, dotAnimationRepeatInterval)
|
||||
}, 1000)
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
startAnimation()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
stopAnimation()
|
||||
}
|
||||
|
||||
private fun startAnimation() {
|
||||
job?.cancel()
|
||||
job = GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
while (isActive) {
|
||||
delay(dotAnimationStartDelay)
|
||||
expand()
|
||||
delay(EXPAND_ANIM_DELAY_MILLS)
|
||||
collapse()
|
||||
delay(dotAnimationRepeatInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopAnimation() {
|
||||
job?.cancel()
|
||||
job = null
|
||||
}
|
||||
|
||||
private fun expand() {
|
||||
@ -270,6 +295,10 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
||||
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPAND_ANIM_DELAY_MILLS = 1000L
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -73,7 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory());
|
||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
|
||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||
}
|
||||
|
||||
|
@ -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.utilities.MessageWrapper
|
||||
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.BencodeList
|
||||
import org.session.libsession.utilities.bencode.BencodeString
|
||||
@ -28,7 +29,6 @@ import javax.inject.Inject
|
||||
private const val TAG = "PushHandler"
|
||||
|
||||
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun onPush(dataMap: Map<String, String>?) {
|
||||
|
@ -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.UnsubscribeResponse
|
||||
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.SnodeAPI
|
||||
import org.session.libsession.snode.Version
|
||||
@ -34,8 +36,6 @@ private const val maxRetryCount = 4
|
||||
|
||||
@Singleton
|
||||
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
token: String,
|
||||
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.CheckResult
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
/**
|
||||
* Observe changes to a content Uri. This function will emit the Uri whenever the content or
|
||||
* its descendants change, according to the parameter [notifyForDescendants].
|
||||
*/
|
||||
@CheckResult
|
||||
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
|
||||
return callbackFlow {
|
||||
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
trySend(uri)
|
||||
}
|
||||
}
|
||||
|
||||
registerContentObserver(uri, notifyForDescendants, observer)
|
||||
awaitClose {
|
||||
unregisterContentObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,7 @@ class IP2Country private constructor(private val context: Context) {
|
||||
|
||||
public fun configureIfNeeded(context: Context) {
|
||||
if (isInitialized) { return; }
|
||||
shared = IP2Country(context)
|
||||
shared = IP2Country(context.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include "config_base.h"
|
||||
#include "util.h"
|
||||
#include "jni_utils.h"
|
||||
|
||||
extern "C" {
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@ -85,29 +86,34 @@ Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *en
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
||||
jobjectArray to_merge) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto conf = ptrToConfigBase(env, thiz);
|
||||
size_t number = env->GetArrayLength(to_merge);
|
||||
std::vector<std::pair<std::string,session::ustring>> configs = {};
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
|
||||
auto pair = extractHashAndData(env, jElement);
|
||||
configs.push_back(pair);
|
||||
}
|
||||
auto returned = conf->merge(configs);
|
||||
auto string_stack = util::build_string_stack(env, returned);
|
||||
return string_stack;
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto conf = ptrToConfigBase(env, thiz);
|
||||
size_t number = env->GetArrayLength(to_merge);
|
||||
std::vector<std::pair<std::string, session::ustring>> configs = {};
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i);
|
||||
auto pair = extractHashAndData(env, jElement);
|
||||
configs.push_back(pair);
|
||||
}
|
||||
auto returned = conf->merge(configs);
|
||||
auto string_stack = util::build_string_stack(env, returned);
|
||||
return string_stack;
|
||||
});
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_ConfigBase_merge__Lkotlin_Pair_2(JNIEnv *env, jobject thiz,
|
||||
jobject to_merge) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto conf = ptrToConfigBase(env, thiz);
|
||||
std::vector<std::pair<std::string, session::ustring>> configs = {extractHashAndData(env, to_merge)};
|
||||
auto returned = conf->merge(configs);
|
||||
auto string_stack = util::build_string_stack(env, returned);
|
||||
return string_stack;
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto conf = ptrToConfigBase(env, thiz);
|
||||
std::vector<std::pair<std::string, session::ustring>> configs = {
|
||||
extractHashAndData(env, to_merge)};
|
||||
auto returned = conf->merge(configs);
|
||||
auto string_stack = util::build_string_stack(env, returned);
|
||||
return string_stack;
|
||||
});
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
@ -1,100 +1,121 @@
|
||||
#include "contacts.h"
|
||||
#include "util.h"
|
||||
#include "jni_utils.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
if (!contact) return nullptr;
|
||||
jobject j_contact = serialize_contact(env, contact.value());
|
||||
return j_contact;
|
||||
// If an exception is thrown, return nullptr
|
||||
return jni_utils::run_catching_cxx_exception_or<jobject>(
|
||||
[=]() -> jobject {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get(session_id_chars);
|
||||
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"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get_or_construct(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return serialize_contact(env, contact);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get_or_construct(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return serialize_contact(env, contact);
|
||||
});
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
|
||||
jobject contact) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto contact_info = deserialize_contact(env, contact, contacts);
|
||||
contacts->set(contact_info);
|
||||
jni_utils::run_catching_cxx_exception_or_throws<void>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto contact_info = deserialize_contact(env, contact, contacts);
|
||||
contacts->set(contact_info);
|
||||
});
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jboolean>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
|
||||
bool result = contacts->erase(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return result;
|
||||
bool result = contacts->erase(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
extern "C"
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
|
||||
jobject thiz,
|
||||
jbyteArray ed25519_secret_key) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto* contacts = new session::config::Contacts(secret_key, std::nullopt);
|
||||
jobject thiz,
|
||||
jbyteArray ed25519_secret_key) {
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
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");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||
reinterpret_cast<jlong>(contacts));
|
||||
|
||||
return newConfig;
|
||||
return newConfig;
|
||||
});
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
|
||||
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto initial = util::ustring_from_bytes(env, initial_dump);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
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");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||
reinterpret_cast<jlong>(contacts));
|
||||
|
||||
return newConfig;
|
||||
return newConfig;
|
||||
});
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
jclass stack = env->FindClass("java/util/Stack");
|
||||
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
||||
jobject our_stack = env->NewObject(stack, init);
|
||||
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
for (const auto& contact : *contacts) {
|
||||
auto contact_obj = serialize_contact(env, contact);
|
||||
env->CallObjectMethod(our_stack, push, contact_obj);
|
||||
}
|
||||
return our_stack;
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
jclass stack = env->FindClass("java/util/Stack");
|
||||
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
||||
jobject our_stack = env->NewObject(stack, init);
|
||||
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
for (const auto &contact: *contacts) {
|
||||
auto contact_obj = serialize_contact(env, contact);
|
||||
env->CallObjectMethod(our_stack, push, contact_obj);
|
||||
}
|
||||
return our_stack;
|
||||
});
|
||||
}
|
54
libsession-util/src/main/cpp/jni_utils.h
Normal file
54
libsession-util/src/main/cpp/jni_utils.h
Normal 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
|
@ -1,13 +1,10 @@
|
||||
package org.session.libsession.avatars
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
class PlaceholderAvatarPhoto(val context: Context,
|
||||
val hashString: String,
|
||||
class PlaceholderAvatarPhoto(val hashString: String,
|
||||
val displayName: String): Key {
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(hashString.encodeToByteArray())
|
||||
messageDigest.update(displayName.encodeToByteArray())
|
||||
|
@ -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)
|
||||
}
|
@ -13,7 +13,8 @@ class MessagingModuleConfiguration(
|
||||
val device: Device,
|
||||
val messageDataProvider: MessageDataProvider,
|
||||
val getUserED25519KeyPair: () -> KeyPair?,
|
||||
val configFactory: ConfigFactoryProtocol
|
||||
val configFactory: ConfigFactoryProtocol,
|
||||
val lastSentTimestampCache: LastSentTimestampCache
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
@ -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.utilities.SessionId
|
||||
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.OnionResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
@ -48,7 +49,6 @@ object OpenGroupApi {
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
|
||||
private var hasUpdatedLastOpenDate = false
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
private val timeSinceLastOpen by lazy {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
|
||||
|
@ -8,6 +8,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
||||
import org.session.libsession.messaging.utilities.SessionId
|
||||
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.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
@ -17,8 +18,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
|
||||
object MessageDecrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`.
|
||||
*
|
||||
|
@ -7,6 +7,7 @@ import com.goterl.lazysodium.interfaces.Sign
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
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.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
@ -14,8 +15,6 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
|
||||
object MessageEncrypter {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
/**
|
||||
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
|
||||
*
|
||||
|
@ -73,6 +73,7 @@ object MessageSender {
|
||||
|
||||
// Convenience
|
||||
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) {
|
||||
sendToOpenGroupDestination(destination, message)
|
||||
} else {
|
||||
@ -372,6 +373,7 @@ object MessageSender {
|
||||
|
||||
// Result Handling
|
||||
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 userPublicKey = storage.getUserPublicKey()!!
|
||||
val timestamp = message.sentTimestamp!!
|
||||
|
@ -290,6 +290,7 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
): Long? {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
message.takeIf { it.isSenderSelf }?.sentTimestamp?.let { MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(threadId, it) }
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val messageSender: String? = message.sender
|
||||
|
||||
@ -410,12 +411,7 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
message.hasMention = listOf(userPublicKey, userBlindedKey)
|
||||
.filterNotNull()
|
||||
.any { key ->
|
||||
return@any (
|
||||
messageText != null &&
|
||||
messageText.contains("@$key")
|
||||
) || (
|
||||
(quoteModel?.author?.serialize() ?: "") == key
|
||||
)
|
||||
messageText?.contains("@$key") == true || key == (quoteModel?.author?.serialize() ?: "")
|
||||
}
|
||||
|
||||
// Persist the message
|
||||
|
@ -14,7 +14,7 @@ import org.whispersystems.curve25519.Curve25519
|
||||
import kotlin.experimental.xor
|
||||
|
||||
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 const val SCALAR_LENGTH: Int = 32 // crypto_core_ed25519_scalarbytes
|
||||
|
@ -3,8 +3,6 @@
|
||||
package org.session.libsession.snode
|
||||
|
||||
import android.os.Build
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.exceptions.SodiumException
|
||||
import com.goterl.lazysodium.interfaces.GenericHash
|
||||
import com.goterl.lazysodium.interfaces.PwHash
|
||||
@ -19,6 +17,7 @@ import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
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.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
@ -41,7 +40,6 @@ import kotlin.collections.set
|
||||
import kotlin.properties.Delegates.observable
|
||||
|
||||
object SnodeAPI {
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
internal val database: LokiAPIDatabaseProtocol
|
||||
get() = SnodeModule.shared.storage
|
||||
private val broadcaster: Broadcaster
|
||||
|
@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final @NonNull Address address;
|
||||
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private @Nullable String name;
|
||||
private @Nullable String customLabel;
|
||||
private boolean resolving;
|
||||
@ -132,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
@NonNull Optional<RecipientDetails> details,
|
||||
@NonNull ListenableFutureTask<RecipientDetails> future)
|
||||
{
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.color = null;
|
||||
this.resolving = true;
|
||||
@ -259,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
|
||||
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
||||
this.context = context;
|
||||
this.context = context.getApplicationContext();
|
||||
this.address = address;
|
||||
this.contactUri = details.contactUri;
|
||||
this.name = details.name;
|
||||
|
@ -1,23 +1,60 @@
|
||||
package org.session.libsignal.utilities
|
||||
|
||||
import android.os.Process
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.SynchronousQueue
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object ThreadUtils {
|
||||
|
||||
const val TAG = "ThreadUtils"
|
||||
|
||||
const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE
|
||||
|
||||
val executorPool: ExecutorService = Executors.newCachedThreadPool()
|
||||
// Paraphrased from: https://www.baeldung.com/kotlin/create-thread-pool
|
||||
// "A cached thread pool such as one created via:
|
||||
// `val executorPool: ExecutorService = Executors.newCachedThreadPool()`
|
||||
// will utilize resources according to the requirements of submitted tasks. It will try to reuse
|
||||
// existing threads for submitted tasks but will create as many threads as it needs if new tasks
|
||||
// keep pouring in (with a memory usage of at least 1MB per created thread). These threads will
|
||||
// live for up to 60 seconds of idle time before terminating by default. As such, it presents a
|
||||
// very sharp tool that doesn't include any backpressure mechanism - and a sudden peak in load
|
||||
// can bring the system down with an OutOfMemory error. We can achieve a similar effect but with
|
||||
// better control by creating a ThreadPoolExecutor manually."
|
||||
|
||||
private val corePoolSize = Runtime.getRuntime().availableProcessors() // Default thread pool size is our CPU core count
|
||||
private val maxPoolSize = corePoolSize * 4 // Allow a maximum pool size of up to 4 threads per core
|
||||
private val keepAliveTimeSecs = 100L // How long to keep idle threads in the pool before they are terminated
|
||||
private val workQueue = SynchronousQueue<Runnable>()
|
||||
val executorPool: ExecutorService = ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTimeSecs, TimeUnit.SECONDS, workQueue)
|
||||
|
||||
// Note: To see how many threads are running in our app at any given time we can use:
|
||||
// val threadCount = getAllStackTraces().size
|
||||
|
||||
@JvmStatic
|
||||
fun queue(target: Runnable) {
|
||||
executorPool.execute(target)
|
||||
executorPool.execute {
|
||||
try {
|
||||
target.run()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun queue(target: () -> Unit) {
|
||||
executorPool.execute(target)
|
||||
executorPool.execute {
|
||||
try {
|
||||
target()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thread executor used by the audio recorder only
|
||||
@JvmStatic
|
||||
fun newDynamicSingleThreadedExecutor(): ExecutorService {
|
||||
val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())
|
||||
|
Loading…
Reference in New Issue
Block a user