Browse Source

Performance improvements and bug fixes (#869)

* refactor: fail on testSnode instead of recursively using up snode list. add call timeout on http client

* refactor: refactoring batch message receives and pollers

* refactor: reduce thread utils pool count to a 2 thread fixed pool. Do a check against pubkey instead of room names for oxenHostedOpenGroup

* refactor: caching lib with potential loader fixes and no-cache for giphy

* refactor: remove store and instead use ConcurrentHashMap with a backing update coroutine

* refactor: queue trim thread jobs instead of add every message processed

* fix: wrapping auth token and initial sync for open groups in a threadutils queued runnable, getting initial sync times down

* fix: fixing the user contacts cache in ConversationAdapter.kt

* refactor: improve polling and initial sync, move group joins from config messages into a background job fetching image.

* refactor: improving the job queuing for open groups, replacing placeholder avatar generation with a custom glide loader and archiving initial sync of open groups

* feat: add OpenGroupDeleteJob.kt

* feat: add open group delete job to process deletions after batch adding

* feat: add vacuum and fix job queue re-adding jobs forever, only try to set message hash values in DB if they have changed

* refactor: remove redundant inflation for profile image views throughout app

* refactor(wip): reducing layout inflation and starting to refactor the open group deletion issues taking a long time

* refactor(wip): refactoring group deletion to not iterate through and delete messages individually

* refactor(wip): refactoring group deletion to not iterate through and delete messages individually

* fix: group deletion optimisation

* build: bump build number

* build: bump build number and fix batch message receive retry logic

* fix: clear out open group deletes

* fix: update visible ConversationAdapter.kt binding for initial contact fetching and better traces for debugging background jobs

* fix: add in check for / force sync latest encryption key pair from linked devices if we already have that closed group

* Rename .java to .kt

* refactor: change MmsDatabase to kotlin to make list operations easier

* fix: nullable type

* fix: compilation issues and constants in .kt instead of .java

* fix: bug fix expiration timer on closed group recipient

* feat: use the job queue properly across executors

* feat: start on open group dispatcher-specific logic, probably a queue factory based on openGroupId if that is the same across new message and deletion jobs to ensure consistent entry and removal

* refactor: removing redundant code and fixing jobqueue per opengroup

* fix: allow attachments in note to self

* fix: make the minWidth in quote view bind max of text / title and body, wrapped ?

* fix: fixing up layouts and code view layouts

* fix: remove TODO, remove timestamp binding

* feat: fix view logic, avatars and padding, downloading attachments lazily (on bind), fixing potential crash, add WindowDebouncer.kt

* fix: NPE on viewModel recipient from removed thread while tearing down the Recipient observer in ConversationActivityV2.kt

* refactor: replace conversation notification debouncer handler with handlerthread, same as conversation list debouncer

* refactor: UI for groups and poller improvements

* fix: revert some changes in poller

* feat: add header back in for message requests

* refactor: remove Trace calls, add more conditions to the HomeDiffUtil for updating more efficiently

* feat: try update the home adapter if we get a profile picture modified event

* feat: bump build numbers

* fix: try to start with list in homeViewModel if we don't have already, render quotes to be width of attachment slide view instead of fixed

* fix: set channel to be conflated instead of no buffer

* fix: set unreads based off last local user message vs incrementing unreads to be all amount

* feat: add profile update flag, update build number

* fix: link preview thumbnails download on bind

* fix: centercrop placeholder in glide request

* feat: recycle the contact selection list and profile image in unbind

* fix: try to prevent user KP crash at weird times

* fix: remove additional log, improve attachment download success rate, fix share logs dialog issue
pull/905/head
Harris 2 months ago committed by GitHub
parent
commit
6ddefb7a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/build.gradle
  2. 44
      app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
  3. 11
      app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
  4. 52
      app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt
  5. 32
      app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt
  6. 20
      app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
  7. 43
      app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
  8. 2
      app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java
  9. 7
      app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt
  10. 6
      app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
  11. 163
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
  12. 76
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
  13. 11
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
  14. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt
  15. 10
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
  16. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
  17. 7
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
  18. 10
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
  19. 12
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt
  20. 13
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt
  21. 13
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt
  22. 99
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt
  23. 12
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt
  24. 121
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
  25. 141
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
  26. 18
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
  27. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt
  28. 16
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
  29. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java
  30. 53
      app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java
  31. 8
      app/src/main/java/org/thoughtcrime/securesms/database/ConversationNotificationDebouncer.kt
  32. 13
      app/src/main/java/org/thoughtcrime/securesms/database/Database.java
  33. 3
      app/src/main/java/org/thoughtcrime/securesms/database/DatabaseContentProviders.java
  34. 5
      app/src/main/java/org/thoughtcrime/securesms/database/DatabaseUtilities.kt
  35. 2
      app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
  36. 17
      app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java
  37. 4
      app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
  38. 5
      app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt
  39. 1367
      app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
  40. 1607
      app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
  41. 8
      app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt
  42. 29
      app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
  43. 55
      app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
  44. 11
      app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
  45. 12
      app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
  46. 4
      app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java
  47. 17
      app/src/main/java/org/thoughtcrime/securesms/database/model/MmsAttachmentInfo.kt
  48. 3
      app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
  49. 30
      app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java
  50. 34
      app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarFetcher.kt
  51. 31
      app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarLoader.kt
  52. 3
      app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
  53. 2
      app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt
  54. 42
      app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
  55. 17
      app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
  56. 105
      app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
  57. 45
      app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt
  58. 42
      app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt
  59. 7
      app/src/main/java/org/thoughtcrime/securesms/home/HomeLoader.kt
  60. 59
      app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt
  61. 114
      app/src/main/java/org/thoughtcrime/securesms/home/NewHomeAdapter.kt
  62. 10
      app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
  63. 7
      app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt
  64. 16
      app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt
  65. 3
      app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java
  66. 7
      app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt
  67. 7
      app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java
  68. 4
      app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java
  69. 4
      app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
  70. 4
      app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java
  71. 2
      app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
  72. 7
      app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt
  73. 16
      app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
  74. 24
      app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
  75. 11
      app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
  76. 4
      app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java
  77. 3
      app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt
  78. 2
      app/src/main/res/layout/activity_conversation_v2_action_bar.xml
  79. 2
      app/src/main/res/layout/activity_home.xml
  80. 2
      app/src/main/res/layout/activity_settings.xml
  81. 2
      app/src/main/res/layout/fragment_call_bottom_sheet.xml
  82. 2
      app/src/main/res/layout/fragment_user_details_bottom_sheet.xml
  83. 1
      app/src/main/res/layout/thumbnail_view.xml
  84. 2
      app/src/main/res/layout/view_conversation.xml
  85. 4
      app/src/main/res/layout/view_deleted_message.xml
  86. 4
      app/src/main/res/layout/view_document.xml
  87. 2
      app/src/main/res/layout/view_global_search_result.xml
  88. 2
      app/src/main/res/layout/view_link_preview.xml
  89. 2
      app/src/main/res/layout/view_mention_candidate.xml
  90. 2
      app/src/main/res/layout/view_mention_candidate_v2.xml
  91. 2
      app/src/main/res/layout/view_message_request.xml
  92. 4
      app/src/main/res/layout/view_open_group_invitation.xml
  93. 8
      app/src/main/res/layout/view_profile_picture.xml
  94. 90
      app/src/main/res/layout/view_quote.xml
  95. 114
      app/src/main/res/layout/view_quote_draft.xml
  96. 4
      app/src/main/res/layout/view_untrusted_attachment.xml
  97. 2
      app/src/main/res/layout/view_user.xml
  98. 189
      app/src/main/res/layout/view_visible_message.xml
  99. 30
      app/src/main/res/layout/view_visible_message_content.xml
  100. 4
      app/src/main/res/layout/view_voice_message.xml
  101. Some files were not shown because too many files have changed in this diff Show More

5
app/build.gradle

@ -110,6 +110,7 @@ dependencies {
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation 'app.cash.copper:copper-flow:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
@ -158,8 +159,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4'
}
def canonicalVersionCode = 279
def canonicalVersionName = "1.13.1"
def canonicalVersionCode = 282
def canonicalVersionName = "1.13.4"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

44
app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java

@ -24,7 +24,7 @@ import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.HandlerThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
@ -44,6 +44,7 @@ import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.Log;
@ -93,6 +94,7 @@ import java.security.Security;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import javax.inject.Inject;
@ -127,7 +129,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
public Poller poller = null;
public Broadcaster broadcaster = null;
private Job firebaseInstanceIdJob;
private Handler conversationListNotificationHandler;
private WindowDebouncer conversationListDebouncer;
private HandlerThread conversationListHandlerThread;
private Handler conversationListHandler;
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase;
@ -136,9 +140,18 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
private volatile boolean isAppVisible;
@Override
public Object getSystemService(String name) {
if (MessagingModuleConfiguration.MESSAGING_MODULE_SERVICE.equals(name)) {
return messagingModuleConfiguration;
}
return super.getSystemService(name);
}
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext) context.getApplicationContext();
}
@ -148,10 +161,21 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
public Handler getConversationListNotificationHandler() {
if (this.conversationListNotificationHandler == null) {
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
if (this.conversationListHandlerThread == null) {
conversationListHandlerThread = new HandlerThread("ConversationListHandler");
conversationListHandlerThread.start();
}
if (this.conversationListHandler == null) {
conversationListHandler = new Handler(conversationListHandlerThread.getLooper());
}
return conversationListHandler;
}
public WindowDebouncer getConversationListDebouncer() {
if (conversationListDebouncer == null) {
conversationListDebouncer = new WindowDebouncer(1000, new Timer());
}
return this.conversationListNotificationHandler;
return conversationListDebouncer;
}
public PersistentLogger getPersistentLogger() {
@ -161,7 +185,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Override
public void onCreate() {
DatabaseModule.init(this);
MessagingModuleConfiguration.configure(this);
super.onCreate();
messagingModuleConfiguration = new MessagingModuleConfiguration(this,
storage,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@ -174,11 +203,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
broadcaster = new Broadcaster(this);
LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase();
MessagingModuleConfiguration.Companion.configure(this,
storage,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
);
SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) {

11
app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt

@ -5,7 +5,14 @@ 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.sending_receiving.attachments.*
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
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.UploadResult
import org.session.libsession.utilities.Util
@ -126,7 +133,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
val mmsDb = DatabaseComponent.get(context).mmsDatabase()
return mmsDb.getMessage(mmsMessageId).use { cursor ->
mmsDb.readerFor(cursor).next
}.isOutgoing
}?.isOutgoing ?: false
}
override fun isOutgoingMessage(timestamp: Long): Boolean {

52
app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt

@ -10,29 +10,51 @@ import com.annimon.stream.function.Predicate
import com.google.protobuf.ByteString
import net.sqlcipher.database.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Conversions
import org.thoughtcrime.securesms.backup.BackupProtos.*
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.Header
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.*
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.PushDatabase
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import java.io.*
import java.lang.Exception
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.Flushable
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.*
import java.util.LinkedList
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -245,8 +267,8 @@ object FullBackupExporter {
}
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
val columns = arrayOf(MmsDatabase.EXPIRES_IN)
val where = MmsDatabase.ID + " = ?"
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
val where = MmsSmsColumns.ID + " = ?"
val args = arrayOf(mmsId.toString())
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
if (mmsCursor != null && mmsCursor.moveToFirst()) {

32
app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt

@ -15,19 +15,39 @@ import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.*
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.*
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.*
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.crypto.*
import java.util.LinkedList
import java.util.Locale
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -172,7 +192,7 @@ object FullBackupImporter {
}
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
val trimmedCondition = " NOT IN (SELECT ${MmsDatabase.ID} FROM ${MmsDatabase.TABLE_NAME})"
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
val where = AttachmentDatabase.MMS_ID + trimmedCondition

20
app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java

@ -8,28 +8,26 @@ import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.avatars.ContactColors;
import org.session.libsession.avatars.ContactPhoto;
import org.session.libsession.avatars.ResourceContactPhoto;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientExporter;
import org.session.libsession.utilities.ThemeUtil;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import java.util.Objects;
@ -139,7 +137,7 @@ public class AvatarImageView extends AppCompatImageView {
requestManager.load(photo.contactPhoto)
.fallback(photoPlaceholderDrawable)
.error(photoPlaceholderDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(this);
} else {

43
app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
@ -10,16 +9,20 @@ import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
class ProfilePictureView : RelativeLayout {
private lateinit var binding: ViewProfilePictureBinding
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
lateinit var glide: GlideRequests
var publicKey: String? = null
var displayName: String? = null
@ -28,16 +31,9 @@ class ProfilePictureView : RelativeLayout {
var isLarge = false
private val profilePicturesCache = mutableMapOf<String, String?>()
private val unknownRecipientDrawable = ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() }
private fun initialize() {
binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion
// region Updating
@ -105,21 +101,24 @@ class ProfilePictureView : RelativeLayout {
if (profilePicturesCache.containsKey(publicKey) && profilePicturesCache[publicKey] == recipient.profileAvatar) return
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.clear(imageView)
glide.load(signalProfilePicture)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.circleCrop()
.error(AvatarPlaceholderGenerator.generate(context,sizeInPX, publicKey, displayName))
.into(imageView)
profilePicturesCache[publicKey] = recipient.profileAvatar
.placeholder(unknownRecipientDrawable)
.centerCrop()
.error(unknownRecipientDrawable)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
} else {
glide.clear(imageView)
glide.load(AvatarPlaceholderGenerator.generate(context, sizeInPX, publicKey, displayName))
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
profilePicturesCache[publicKey] = recipient.profileAvatar
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
}
profilePicturesCache[publicKey] = recipient.profileAvatar
} else {
imageView.setImageDrawable(null)
}

2
app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java

@ -264,7 +264,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(thumbnailView);
} else if (!documentSlides.isEmpty()){
thumbnailView.setVisibility(GONE);

7
app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt

@ -35,6 +35,13 @@ class ContactSelectionListAdapter(private val context: Context, private val mult
return items.size
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
if (holder is UserViewHolder) {
holder.view.unbind()
}
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is ContactSelectionListItem.Header -> ViewType.Divider

6
app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt

@ -54,8 +54,8 @@ class UserView : LinearLayout {
val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
val address = user.address.serialize()
binding.profilePictureView.glide = glide
binding.profilePictureView.update(user)
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@ -83,7 +83,7 @@ class UserView : LinearLayout {
}
fun unbind() {
binding.profilePictureView.root.recycle()
}
// endregion
}

163
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

@ -49,6 +49,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
import network.loki.messenger.databinding.ActivityConversationV2Binding
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention
@ -241,7 +242,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionMode?.let {
onDeselect(message, position, it)
}
}
},
lifecycleCoroutineScope = lifecycleScope
)
adapter.visibleMessageContentViewDelegate = this
adapter
@ -314,11 +316,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
scrollToFirstUnreadMessageIfNeeded()
showOrHideInputIfNeeded()
setUpMessageRequestsBar()
if (viewModel.recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
viewModel.recipient?.let { recipient ->
if (recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
}
}
}
@ -326,7 +330,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
threadDb.markAllAsRead(viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val recipient = viewModel.recipient ?: return
threadDb.markAllAsRead(viewModel.threadId, recipient.isOpenGroupRecipient)
}
override fun onPause() {
@ -391,17 +396,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
actionBar.title = ""
actionBar.customView = actionBarBinding!!.root
actionBar.setDisplayShowCustomEnabled(true)
actionBarBinding!!.conversationTitleView.text = viewModel.recipient.toShortString()
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
actionBarBinding!!.conversationTitleView.text = viewModel.recipient?.toShortString()
@DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) {
R.dimen.medium_profile_picture_size
} else {
R.dimen.small_profile_picture_size
}
val size = resources.getDimension(sizeID).roundToInt()
actionBarBinding!!.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
actionBarBinding!!.profilePictureView.glide = glide
actionBarBinding!!.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
actionBarBinding!!.profilePictureView.root.glide = glide
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
actionBarBinding!!.profilePictureView.update(viewModel.recipient)
val profilePictureView = actionBarBinding!!.profilePictureView.root
viewModel.recipient?.let { recipient ->
profilePictureView.update(recipient)
}
}
// called from onCreate
@ -437,7 +445,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (mediaURI != null && mediaType != null) {
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, ""), PICK_FROM_LIBRARY)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY)
return
} else {
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
@ -491,11 +499,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun setUpRecipientObserver() {
viewModel.recipient.addListener(this)
viewModel.recipient?.addListener(this)
}
private fun tearDownRecipientObserver() {
viewModel.recipient.removeListener(this)
viewModel.recipient?.removeListener(this)
}
private fun getLatestOpenGroupInfoIfNeeded() {
@ -505,12 +513,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// called from onCreate
private fun setUpBlockedBanner() {
if (viewModel.recipient.isGroupRecipient) { return }
val sessionID = viewModel.recipient.address.toString()
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) { return }
val sessionID = recipient.address.toString()
val contact = sessionContactDb.getContactWithSessionID(sessionID)
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
binding?.blockedBannerTextView?.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
binding?.blockedBanner?.isVisible = recipient.isBlocked
binding?.blockedBanner?.setOnClickListener { viewModel.unblock() }
}
@ -558,13 +567,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
if (!isMessageRequestThread()) {
ConversationMenuHelper.onPrepareOptionsMenu(
menu,
menuInflater,
viewModel.recipient,
viewModel.threadId,
this
) { onOptionsItemSelected(it) }
val recipient = viewModel.recipient
if (recipient != null) {
ConversationMenuHelper.onPrepareOptionsMenu(
menu,
menuInflater,
recipient,
viewModel.threadId,
this
) { onOptionsItemSelected(it) }
}
}
super.onPrepareOptionsMenu(menu)
return true
@ -582,21 +594,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// region Animation & Updating
override fun onModified(recipient: Recipient) {
runOnUiThread {
if (viewModel.recipient.isContactRecipient) {
binding?.blockedBanner?.isVisible = viewModel.recipient.isBlocked
val recipient = viewModel.recipient
if (recipient != null && recipient.isContactRecipient) {
binding?.blockedBanner?.isVisible = recipient.isBlocked
}
setUpMessageRequestsBar()
invalidateOptionsMenu()
updateSubtitle()
showOrHideInputIfNeeded()
actionBarBinding?.profilePictureView?.update(recipient)
actionBarBinding?.conversationTitleView?.text = recipient.toShortString()
if (recipient != null) {
actionBarBinding?.profilePictureView?.root?.update(recipient)
}
actionBarBinding?.conversationTitleView?.text = recipient?.toShortString()
}
}
private fun showOrHideInputIfNeeded() {
if (viewModel.recipient.isClosedGroupRecipient) {
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
val recipient = viewModel.recipient
if (recipient != null && recipient.isClosedGroupRecipient) {
val group = groupDb.getGroup(recipient.address.toGroupString()).orNull()
val isActive = (group?.isActive == true)
binding?.inputBar?.showInput = isActive
} else {
@ -632,17 +648,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun isMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient && !viewModel.recipient.isApproved
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient && !recipient.isApproved
}
private fun isOutgoingMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient &&
!(viewModel.recipient.hasApprovedMe() || viewModel.hasReceived())
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient &&
!recipient.isLocalNumber &&
!(recipient.hasApprovedMe() || viewModel.hasReceived())
}
private fun isIncomingMessageRequestThread(): Boolean {
return !viewModel.recipient.isGroupRecipient &&
!viewModel.recipient.isApproved &&
val recipient = viewModel.recipient ?: return false
return !recipient.isGroupRecipient &&
!recipient.isApproved &&
!recipient.isLocalNumber &&
!threadDb.getLastSeenAndHasSent(viewModel.threadId).second() &&
threadDb.getMessageCount(viewModel.threadId) > 0
}
@ -701,17 +722,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
val additionalContentContainer = binding?.additionalContentContainer ?: return
val recipient = viewModel.recipient ?: return
if (!isShowingMentionCandidatesView) {
additionalContentContainer.removeAllViews()
val view = MentionCandidatesView(this)
view.glide = glide
view.onCandidateSelected = { handleMentionSelected(it) }
additionalContentContainer.addView(view)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId)
} else {
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, recipient.isOpenGroupRecipient)
this.mentionCandidatesView!!.setMentionCandidates(candidates)
}
isShowingMentionCandidatesView = true
@ -839,15 +861,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun updateSubtitle() {
val actionBarBinding = actionBarBinding ?: return
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
val recipient = viewModel.recipient ?: return
actionBarBinding.muteIconImageView.isVisible = recipient.isMuted
actionBarBinding.conversationSubtitleView.isVisible = true
if (viewModel.recipient.isMuted) {
if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(viewModel.recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
if (recipient.isMuted) {
if (recipient.mutedUntil != Long.MAX_VALUE) {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
} else {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
}
} else if (viewModel.recipient.isGroupRecipient) {
} else if (recipient.isGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
if (openGroup != null) {
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
@ -866,7 +889,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (item.itemId == android.R.id.home) {
return false
}
return ConversationMenuHelper.onOptionItemSelected(this, item, viewModel.recipient)
return viewModel.recipient?.let { recipient ->
ConversationMenuHelper.onOptionItemSelected(this, item, recipient)
} ?: false
}
// `position` is the adapter position; not the visual position
@ -896,7 +921,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
binding?.inputBar?.draftQuote(viewModel.recipient, message, glide)
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, message, glide)
}
// `position` is the adapter position; not the visual position
@ -1002,12 +1028,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return
viewHolder.view.playVoiceMessage()
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
visibleMessageView.playVoiceMessage()
}
override fun sendMessage() {
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
val recipient = viewModel.recipient ?: return
if (recipient.isContactRecipient && recipient.isBlocked) {
BlockedDialog(recipient).show(supportFragmentManager, "Blocked Dialog")
return
}
val binding = binding ?: return
@ -1019,24 +1047,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun commitInputContent(contentUri: Uri) {
val recipient = viewModel.recipient ?: return
val media = Media(contentUri, MediaUtil.getMimeType(this, contentUri)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, getMessageBody()), PICK_FROM_LIBRARY)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY)
}
private fun processMessageRequestApproval() {
if (isIncomingMessageRequestThread()) {
acceptMessageRequest()
} else if (!viewModel.recipient.isApproved) {
} else if (viewModel.recipient?.isApproved == false) {
// edge case for new outgoing thread on new recipient without sending approval messages
viewModel.setRecipientApproved()
}
}
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
val recipient = viewModel.recipient ?: return
processMessageRequestApproval()
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (viewModel.recipient.isContactRecipient && viewModel.recipient.address.toString() == userPublicKey)
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
return dialog.show(supportFragmentManager, "Send Seed Dialog")
@ -1045,7 +1075,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
message.text = text
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
val outgoingTextMessage = OutgoingTextMessage.from(message, recipient)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@ -1055,14 +1085,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
currentMentionStartIndex = -1
mentions.clear()
// Put the message in the database
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!) { }
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!, null, true)
// Send it
MessageSender.send(message, viewModel.recipient.address)
MessageSender.send(message, recipient.address)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
}
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
val recipient = viewModel.recipient ?: return
processMessageRequestApproval()
// Create the message
val message = VisibleMessage()
@ -1073,7 +1104,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
}
val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview)
val outgoingTextMessage = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview)
// Clear the input bar
binding?.inputBar?.text = ""
binding?.inputBar?.cancelQuoteDraft()
@ -1087,9 +1118,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Reset attachments button if needed
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
// Put the message in the database
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false) { }
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false, null, runThreadUpdate = true)
// Send it
MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview)
MessageSender.send(message, recipient.address, attachments, quote, linkPreview)
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
}
@ -1119,8 +1150,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun pickFromLibrary() {
val recipient = viewModel.recipient ?: return
binding?.inputBar?.text?.trim()?.let { text ->
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, text)
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, recipient, text)
}
}
@ -1187,7 +1219,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendAttachments(slideDeck.asAttachments(), body)
}
INVITE_CONTACTS -> {
if (!viewModel.recipient.isOpenGroupRecipient) { return }
if (viewModel.recipient?.isOpenGroupRecipient != true) { return }
val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return }
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
@ -1268,13 +1300,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun deleteMessages(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient ?: return
if (!IS_UNSEND_REQUESTS_ENABLED) {
deleteMessagesWithoutUnsendRequest(messages)
return
}
val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
if (viewModel.recipient.isOpenGroupRecipient) {
if (recipient.isOpenGroupRecipient) {
val messageCount = messages.size
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
@ -1293,7 +1326,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
builder.show()
} else if (allSentByCurrentUser && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = viewModel.recipient
bottomSheet.recipient = recipient
bottomSheet.onDeleteForMeTapped = {
for (message in messages) {
viewModel.deleteLocally(message)
@ -1452,16 +1485,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun reply(messages: Set<MessageRecord>) {
binding?.inputBar?.draftQuote(viewModel.recipient, messages.first(), glide)
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
endActionMode()
}
private fun sendMediaSavedNotification() {
if (viewModel.recipient.isGroupRecipient) { return }
val recipient = viewModel.recipient ?: return
if (recipient.isGroupRecipient) { return }
val timestamp = System.currentTimeMillis()
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
val message = DataExtractionNotification(kind)
MessageSender.send(message, viewModel.recipient.address)
MessageSender.send(message, recipient.address)
}
private fun endActionMode() {

76
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt

@ -4,10 +4,25 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.util.SparseArray
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
@ -19,13 +34,33 @@ import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit,
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit)
private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit, lifecycleCoroutineScope: LifecycleCoroutineScope)
: CursorRecyclerViewAdapter<ViewHolder>(context, cursor) {
private val messageDB = DatabaseComponent.get(context).mmsSmsDatabase()
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
var selectedItems = mutableSetOf<MessageRecord>()
private var searchQuery: String? = null
var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null
private val updateQueue = Channel<String>(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val contactCache = SparseArray<Contact>(100)
private val contactLoadedCache = SparseBooleanArray(100)
init {
lifecycleCoroutineScope.launch(IO) {
while (isActive) {
val item = updateQueue.receive()
val contact = getSenderInfo(item) ?: continue
contactCache[item.hashCode()] = contact
contactLoadedCache[item.hashCode()] = true
}
}
}
@WorkerThread
private fun getSenderInfo(sender: String): Contact? {
return contactDB.getContactWithSessionID(sender)
}
sealed class ViewType(val rawValue: Int) {
object Visible : ViewType(0)
object Control : ViewType(1)
@ -39,7 +74,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
}
}
class VisibleMessageViewHolder(val view: VisibleMessageView) : ViewHolder(view)
class VisibleMessageViewHolder(val view: View) : ViewHolder(view)
class ControlMessageViewHolder(val view: ControlMessageView) : ViewHolder(view)
override fun getItemViewType(cursor: Cursor): Int {
@ -52,7 +87,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
@Suppress("NAME_SHADOWING")
val viewType = ViewType.allValues[viewType]
return when (viewType) {
ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context))
ViewType.Visible -> VisibleMessageViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_visible_message, parent, false))
ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context))
else -> throw IllegalStateException("Unexpected view type: $viewType.")
}
@ -65,20 +100,31 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
when (viewHolder) {
is VisibleMessageViewHolder -> {
val view = viewHolder.view
val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView
val isSelected = selectedItems.contains(message)
view.snIsSelected = isSelected
view.indexInAdapter = position
view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery)
visibleMessageView.snIsSelected = isSelected
visibleMessageView.indexInAdapter = position
val senderId = message.individualRecipient.address.serialize()
val senderIdHash = senderId.hashCode()
updateQueue.trySend(senderId)
if (contactCache[senderIdHash] == null && !contactLoadedCache.getOrDefault(senderIdHash, false)) {
getSenderInfo(senderId)?.let { contact ->
contactCache[senderIdHash] = contact
}
}
val contact = contactCache[senderIdHash]
visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId)
if (!message.isDeleted) {
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) }
visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
} else {
view.onPress = null
view.onSwipeToReply = null
view.onLongPress = null
visibleMessageView.onPress = null
visibleMessageView.onSwipeToReply = null
visibleMessageView.onLongPress = null
}
view.contentViewDelegate = visibleMessageContentViewDelegate
visibleMessageView.contentViewDelegate = visibleMessageContentViewDelegate
}
is ControlMessageViewHolder -> {
viewHolder.view.bind(message, messageBefore)
@ -105,7 +151,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
override fun onItemViewRecycled(viewHolder: ViewHolder?) {
when (viewHolder) {
is VisibleMessageViewHolder -> viewHolder.view.recycle()
is VisibleMessageViewHolder -> viewHolder.view.findViewById<VisibleMessageView>(R.id.visibleMessageView).recycle()
is ControlMessageViewHolder -> viewHolder.view.recycle()
}
super.onItemViewRecycled(viewHolder)

11
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID
@ -22,8 +23,8 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> = _uiState
val recipient: Recipient
get() = repository.getRecipientForThreadId(threadId)
val recipient: Recipient?
get() = repository.maybeGetRecipientForThreadId(threadId)
init {
_uiState.update {
@ -44,20 +45,24 @@ class ConversationViewModel(
}
fun unblock() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for unblock action")
if (recipient.isContactRecipient) {
repository.unblock(recipient)
}
}
fun deleteLocally(message: MessageRecord) {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
repository.deleteLocally(recipient, message)
}
fun setRecipientApproved() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
repository.setApproved(recipient, true)
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
val recipient = recipient ?: return@launch
repository.deleteForEveryone(threadId, recipient, message)
.onFailure {
showMessage("Couldn't delete message due to error: $it")
@ -92,6 +97,7 @@ class ConversationViewModel(
}
fun acceptMessageRequest() = viewModelScope.launch {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for accept message request action")
repository.acceptMessageRequest(threadId, recipient)
.onSuccess {
_uiState.update {
@ -104,6 +110,7 @@ class ConversationViewModel(
}
fun declineMessageRequest() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for decline message request action")
repository.declineMessageRequest(threadId, recipient)
}

2
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt

@ -118,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
this.slideSize = slides.size
}
// iterate binding
slides.take(5).forEachIndexed { position, slide ->
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
}