Merge branch 'dev' into trusted_attachment_download

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
This commit is contained in:
jubb 2021-07-12 09:28:14 +10:00
commit 842cfc25a1
172 changed files with 665 additions and 2034 deletions

View File

@ -143,8 +143,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2'
}
def canonicalVersionCode = 193
def canonicalVersionName = "1.11.2"
def canonicalVersionCode = 198
def canonicalVersionName = "1.11.3"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -55,6 +55,12 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
<!-- The allowBackup="false" below is important to guard against potential malicious backups -->
<application
@ -87,80 +93,80 @@
android:value="false" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.LandingActivity"
android:name="org.thoughtcrime.securesms.onboarding.LandingActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.RegisterActivity"
android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.RecoveryPhraseRestoreActivity"
android:name="org.thoughtcrime.securesms.onboarding.RecoveryPhraseRestoreActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.LinkDeviceActivity"
android:name="org.thoughtcrime.securesms.onboarding.LinkDeviceActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.DisplayNameActivity"
android:name="org.thoughtcrime.securesms.onboarding.DisplayNameActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.PNModeActivity"
android:name="org.thoughtcrime.securesms.onboarding.PNModeActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.HomeActivity"
android:name="org.thoughtcrime.securesms.home.HomeActivity"
android:screenOrientation="portrait"
android:launchMode="singleTask"
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.SettingsActivity"
android:name="org.thoughtcrime.securesms.preferences.SettingsActivity"
android:screenOrientation="portrait"
android:label="@string/activity_settings_title" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.PathActivity"
android:name="org.thoughtcrime.securesms.home.PathActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.QRCodeActivity"
android:name="org.thoughtcrime.securesms.preferences.QRCodeActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.CreatePrivateChatActivity"
android:name="org.thoughtcrime.securesms.dms.CreatePrivateChatActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.CreateClosedGroupActivity"
android:name="org.thoughtcrime.securesms.groups.CreateClosedGroupActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity"
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
android:label="@string/activity_edit_closed_group_title"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.JoinPublicChatActivity"
android:name="org.thoughtcrime.securesms.groups.JoinPublicChatActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.SeedActivity"
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.SelectContactsActivity"
android:name="org.thoughtcrime.securesms.contacts.SelectContactsActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.PrivacySettingsActivity"
android:name="org.thoughtcrime.securesms.preferences.PrivacySettingsActivity"
android:label="@string/activity_privacy_settings_title"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.NotificationSettingsActivity"
android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity"
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.loki.activities.ChatSettingsActivity"
android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity"
android:screenOrientation="portrait" />
<activity
@ -192,7 +198,7 @@
<activity-alias
android:name=".RoutingActivity"
android:exported="true"
android:targetActivity="org.thoughtcrime.securesms.loki.activities.HomeActivity">
android:targetActivity="org.thoughtcrime.securesms.home.HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -209,14 +215,14 @@
<activity
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait"
android:parentActivityName="org.thoughtcrime.securesms.loki.activities.HomeActivity"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" />
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity>
<activity
android:name="org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity"
android:name="org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.TextSecure.DayNight" />
<activity
@ -285,7 +291,7 @@
android:exported="true"
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
<service
android:name="org.thoughtcrime.securesms.loki.api.PushNotificationService"
android:name="org.thoughtcrime.securesms.notifications.PushNotificationService"
android:enabled="true"
android:exported="false">
<intent-filter>
@ -405,7 +411,7 @@
</intent-filter>
</receiver>
<receiver
android:name="org.thoughtcrime.securesms.loki.api.BackgroundPollWorker$BootBroadcastReceiver"
android:name="org.thoughtcrime.securesms.notifications.BackgroundPollWorker$BootBroadcastReceiver"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

View File

@ -27,10 +27,10 @@ import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt;
import org.session.libsession.avatars.AvatarHelper;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
@ -42,11 +42,11 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
@ -58,17 +58,14 @@ import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
import org.thoughtcrime.securesms.loki.api.OpenGroupManager;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities;
import org.thoughtcrime.securesms.loki.utilities.FcmUtils;
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.notifications.FcmUtils;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
@ -84,12 +81,14 @@ import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.Security;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import dagger.ObjectGraph;
import kotlin.Unit;
import kotlinx.coroutines.Job;
@ -154,8 +153,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
conversationListNotificationHandler = new Handler(Looper.getMainLooper());
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
MessagingModuleConfiguration.Companion.configure(this,
DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this));
DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this),
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)
);
SnodeModule.Companion.configure(apiDB, broadcaster);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey != null) {
@ -181,27 +182,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
Log.i(TAG, "App is now visible.");
KeyCachingService.onAppForegrounded(this);
boolean hasPerformedContactMigration = TextSecurePreferences.INSTANCE.hasPerformedContactMigration(this);
if (!hasPerformedContactMigration) {
TextSecurePreferences.INSTANCE.setPerformedContactMigration(this);
Set<Recipient> allContacts = ContactUtilities.getAllContacts(this);
SessionContactDatabase contactDB = DatabaseFactory.getSessionContactDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
for (Recipient recipient : allContacts) {
if (recipient.isGroupRecipient()) { continue; }
String sessionID = recipient.getAddress().serialize();
Contact contact = contactDB.getContactWithSessionID(sessionID);
if (contact == null) {
contact = new Contact(sessionID);
String name = userDB.getDisplayName(sessionID);
contact.setName(name);
contact.setProfilePictureURL(recipient.getProfileAvatar());
contact.setProfilePictureEncryptionKey(recipient.getProfileKey());
contact.setTrusted(true);
}
contactDB.setContact(contact);
}
}
if (poller != null) {
poller.setCaughtUp(false);
}
@ -487,7 +467,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
boolean isUsingFCM = TextSecurePreferences.isUsingFCM(this);
TextSecurePreferences.clearAll(this);
if (isMigratingToV2KeyPair) {
TextSecurePreferences.setIsMigratingKeyPair(this, true);
TextSecurePreferences.setIsUsingFCM(this, isUsingFCM);
TextSecurePreferences.setProfileName(this, displayName);
}

View File

@ -189,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
CharSequence relativeTimeSpan;
if (mediaItem.date > 0) {
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
} else {
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
}

View File

@ -10,7 +10,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.loki.views.UserView;
import org.thoughtcrime.securesms.contacts.UserView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.Conversions;

View File

@ -39,7 +39,7 @@ import android.widget.ImageView;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.service.KeyCachingService;

View File

@ -12,8 +12,8 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.activities.LandingActivity;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.onboarding.LandingActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.session.libsession.utilities.TextSecurePreferences;

View File

@ -42,9 +42,8 @@ import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListFragment;
import org.thoughtcrime.securesms.loki.fragments.ContactSelectionListLoader.DisplayMode;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.contacts.ContactSelectionListFragment;
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader.DisplayMode;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.session.libsession.utilities.recipients.Recipient;
@ -54,7 +53,6 @@ import org.session.libsession.utilities.ViewUtil;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import network.loki.messenger.R;

View File

@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity;
import android.widget.Toast;
import org.session.libsession.utilities.Address;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.util.CommunicationActions;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.backup;
import android.app.Activity
import android.app.Application
@ -9,17 +9,13 @@ import android.os.Bundle
import android.provider.OpenableColumns
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
@ -28,18 +24,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.backup.FullBackupImporter.DatabaseDowngradeException
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.utilities.show
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.BackupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.home.HomeActivity
class BackupRestoreActivity : BaseActionBarActivity() {
@ -188,7 +183,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(context, true)
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
val application = ApplicationContext.getInstance(context)
BackupRestoreResult.SUCCESS
} catch (e: DatabaseDowngradeException) {

View File

@ -21,8 +21,7 @@ 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.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3

View File

@ -19,7 +19,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator;
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.components
import android.content.Context
import android.graphics.Canvas
@ -9,7 +9,7 @@ import android.view.LayoutInflater
import android.widget.RelativeLayout
import kotlinx.android.synthetic.main.view_separator.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toPx
import org.session.libsession.utilities.ThemeUtil
class LabeledSeparatorView : RelativeLayout {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
@ -17,7 +17,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.mms.GlideRequests
class ProfilePictureView : RelativeLayout {

View File

@ -22,8 +22,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.messaging.contacts.Contact;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;

View File

@ -16,7 +16,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import network.loki.messenger.R;

View File

@ -8,7 +8,7 @@ import org.session.libsession.messaging.messages.control.TypingIndicator;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.Util;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.fragments
package org.thoughtcrime.securesms.contacts
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
@ -7,7 +7,7 @@ import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.contact_selection_list_divider.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.contacts.UserView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.utilities.recipients.Recipient

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.fragments
package org.thoughtcrime.securesms.contacts
import android.os.Bundle
import androidx.fragment.app.Fragment
@ -11,10 +11,11 @@ import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.contact_selection_list_fragment.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem
import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader
class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<List<ContactSelectionListItem>>, ContactClickListener {
private var cursorFilter: String? = null
@ -98,7 +99,7 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
update(listOf())
}
private fun update(items: List<ContactSelectionListItem>) {
private fun update(items: List<ContactSelectionListItem>) {
if (activity?.isDestroyed == true) {
Log.e(ContactSelectionListFragment::class.java.name,
"Received a loader callback after the fragment was detached from the activity.",

View File

@ -1,8 +1,8 @@
package org.thoughtcrime.securesms.loki.fragments
package org.thoughtcrime.securesms.contacts
import android.content.Context
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
import org.thoughtcrime.securesms.util.ContactUtilities
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.util.AsyncLoader

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.contacts
import android.app.Activity
import android.content.Intent
@ -40,8 +40,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
setContentView(R.layout.activity_select_contacts)
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
usersToExclude = intent.getStringArrayExtra(Companion.usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(Companion.emptyStateTextKey)
usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(emptyStateTextKey)
if (emptyStateText != null) {
emptyStateMessageTextView.text = emptyStateText
}

View File

@ -1,10 +1,9 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.contacts
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.ViewGroup
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.utilities.recipients.Recipient

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.contacts
import android.content.Context
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
import org.thoughtcrime.securesms.util.ContactUtilities
import org.thoughtcrime.securesms.util.AsyncLoader
class SelectContactsLoader(context: Context, val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.contacts
import android.content.Context
import android.util.AttributeSet
@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.view_user.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.utilities.recipients.Recipient

View File

@ -23,6 +23,8 @@ import android.widget.RelativeLayout
import android.widget.Toast
import androidx.annotation.DimenRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
@ -40,6 +42,7 @@ import kotlinx.android.synthetic.main.view_conversation.view.*
import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_input_bar_recording.*
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
import kotlinx.android.synthetic.main.view_visible_message.view.*
import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
@ -69,7 +72,6 @@ import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.SettableFuture
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
@ -84,8 +86,7 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCand
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.*
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
@ -100,20 +101,16 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.*
import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.math.*
@ -125,7 +122,7 @@ import kotlin.math.*
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
SearchBottomBar.EventListener {
SearchBottomBar.EventListener, VoiceMessageViewDelegate {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var linkPreviewViewModel: LinkPreviewViewModel? = null
private var threadID: Long = -1
@ -213,6 +210,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(recipient)
}
this.threadID = threadID
val thread = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID)
if (thread == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
setUpRecyclerView()
setUpToolBar()
setUpInputBar()
@ -232,6 +234,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
scrollToFirstUnreadMessageIfNeeded()
markAllAsRead()
showOrHideInputIfNeeded()
if (this.thread.isOpenGroupRecipient) {
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
}
}
override fun onResume() {
@ -849,6 +858,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
override fun playNextAudioIfPossible(current: Int) {
if (current > 0) {
val nextVisibleMessageView = conversationRecyclerView[current - 1] as? VisibleMessageView
nextVisibleMessageView?.let { visibleMessageView ->
visibleMessageView.messageContentView.mainContainer.children.forEach { child ->
val nextVoiceMessageView = child as? VoiceMessageView
nextVoiceMessageView?.let { voiceMessageView ->
voiceMessageView.togglePlayback()
return@forEach
}
}
}
}
}
override fun sendMessage() {
if (thread.isContactRecipient && thread.isBlocked) {
BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog")
@ -1129,7 +1153,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
for (message in sortedMessages) {
val body = MentionUtilities.highlightMentions(message.body, message.threadId, this)
if (TextUtils.isEmpty(body)) { continue }
val formattedTimestamp = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), message.timestamp)
val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp)
builder.append("$formattedTimestamp: $body").append('\n')
}
if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') {
@ -1265,7 +1289,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun saveDraft() {
val text = inputBar.text.trim()
val text = inputBar?.text?.trim() ?: return
if (text.isEmpty()) { return }
val drafts = Drafts()
drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text))

View File

@ -72,6 +72,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
view.snIsSelected = isSelected
view.messageTimestampTextView.isVisible = isSelected
val position = viewHolder.adapterPosition
view.viewHolderIndex = position
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }

View File

@ -8,8 +8,8 @@ import android.view.VelocityTracker
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import org.thoughtcrime.securesms.loki.utilities.disableClipping
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx
import kotlin.math.abs
import kotlin.math.max

View File

@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide

View File

@ -8,7 +8,7 @@ import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_link_preview_draft.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.util.AttributeSet
@ -8,7 +8,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.messaging.mentions.Mention

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.util.AttributeSet

View File

@ -8,8 +8,8 @@ import android.widget.FrameLayout
import kotlinx.android.synthetic.main.view_open_group_guidelines.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.loki.activities.OpenGroupGuidelinesActivity
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity
import org.thoughtcrime.securesms.util.push
class OpenGroupGuidelinesView : FrameLayout {

View File

@ -9,12 +9,11 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.dialog_join_open_group.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
/** Shown upon tapping an open group invitation. */
class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() {
@ -38,7 +37,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B
val activity = requireContext() as AppCompatActivity
ThreadUtils.queue {
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(activity)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
}
dismiss()
}

View File

@ -19,8 +19,8 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView
import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import kotlin.math.max
import kotlin.math.roundToInt

View File

@ -17,10 +17,9 @@ import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.DrawableRes
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.InputBarButtonImageViewContainer
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.InputBarButtonImageViewContainer
import java.util.*
import kotlin.math.abs

View File

@ -9,7 +9,7 @@ import android.util.Log
import android.widget.RelativeLayout
import androidx.appcompat.widget.AppCompatEditText
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toPx
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

View File

@ -13,9 +13,9 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.animateSizeChange
import org.thoughtcrime.securesms.loki.utilities.disableClipping
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.util.DateUtils
import java.util.*

View File

@ -8,7 +8,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.messaging.mentions.Mention

View File

@ -31,6 +31,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (selectedItems.isEmpty()) { return }
val firstMessage = selectedItems.iterator().next()
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
val thread = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
fun userCanDeleteSelectedItems(): Boolean {
if (openGroup == null) { return true }
@ -54,7 +55,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy).isVisible = !containsControlMessage && hasText
// Copy Session ID
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
(openGroup != null && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey)
(thread.isGroupRecipient && selectedItems.size == 1 && firstMessage.recipient.address.toString() != userPublicKey)
// Resend
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
// Save media

View File

@ -38,10 +38,10 @@ import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.*
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException

View File

@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtiliti
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide

View File

@ -1,11 +1,8 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.ContentResolver
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Resources
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.RelativeLayout
@ -13,22 +10,19 @@ import androidx.annotation.ColorInt
import androidx.core.content.res.ResourcesCompat
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import com.google.android.exoplayer2.util.MimeTypes
import kotlinx.android.synthetic.main.view_link_preview.view.*
import kotlinx.android.synthetic.main.view_quote.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.UiModeUtilities
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

View File

@ -6,7 +6,6 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.util.AttributeSet
@ -23,7 +22,6 @@ import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.text.getSpans
import androidx.core.text.toSpannable
import androidx.core.text.util.LinkifyCompat
import kotlinx.android.synthetic.main.view_link_preview.view.*
import kotlinx.android.synthetic.main.view_visible_message_content.view.*
import network.loki.messenger.R
@ -32,15 +30,18 @@ import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
import org.thoughtcrime.securesms.util.UiModeUtilities
import java.util.*
import kotlin.math.roundToInt
@ -48,6 +49,7 @@ class VisibleMessageContentView : LinearLayout {
var onContentClick: ((event: MotionEvent) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageContentViewDelegate? = null
var viewHolderIndex: Int = -1
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
@ -109,7 +111,8 @@ class VisibleMessageContentView : LinearLayout {
// Audio attachment
if (contactIsTrusted || message.isOutgoing) {
val voiceMessageView = VoiceMessageView(context)
voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
voiceMessageView.index = viewHolderIndex
voiceMessageView.delegate = context as? ConversationActivityV2voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster)
mainContainer.addView(voiceMessageView)
// We have to use onContentClick (rather than a click listener directly on the voice
// message view) so as to not interfere with all the other gestures.

View File

@ -24,12 +24,8 @@ import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.disableClipping
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.*
import java.util.*
import kotlin.math.abs
import kotlin.math.min
@ -47,6 +43,7 @@ class VisibleMessageView : LinearLayout {
private var longPressCallback: Runnable? = null
private var onDownTimestamp = 0L
private var onDoubleTap: (() -> Unit)? = null
var viewHolderIndex: Int = -1
var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()}
var onPress: ((event: MotionEvent) -> Unit)? = null
@ -82,7 +79,7 @@ class VisibleMessageView : LinearLayout {
val senderSessionID = sender.address.serialize()
val threadID = message.threadId
val threadDB = DatabaseFactory.getThreadDatabase(context)
val thread = threadDB.getRecipientForThreadId(threadID)!!
val thread = threadDB.getRecipientForThreadId(threadID) ?: return
val contactDB = DatabaseFactory.getSessionContactDatabase(context)
val contact = contactDB.getContactWithSessionID(senderSessionID)
val isGroupThread = thread.isGroupRecipient
@ -96,7 +93,7 @@ class VisibleMessageView : LinearLayout {
profilePictureView.glide = glide
profilePictureView.update()
if (thread.isOpenGroupRecipient) {
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)!!
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
} else {
@ -110,11 +107,11 @@ class VisibleMessageView : LinearLayout {
senderNameTextView.visibility = View.GONE
}
// Date break
val showDateBreak = (previous == null || !DateUtils.isSameDay(message.timestamp, previous.timestamp))
val showDateBreak = (previous == null || !DateUtils.isSameHour(message.timestamp, previous.timestamp))
dateBreakTextView.isVisible = showDateBreak
dateBreakTextView.text = if (showDateBreak) DateUtils.getRelativeDate(context, Locale.getDefault(), message.timestamp) else ""
dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else ""
// Timestamp
messageTimestampTextView.text = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), message.timestamp)
messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp)
// Margins
val startPadding: Int
if (isGroupThread) {
@ -152,6 +149,7 @@ class VisibleMessageView : LinearLayout {
var maxWidth = screenWidth - startPadding - endPadding
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view
messageContentView.viewHolderIndex = viewHolderIndex
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false))
messageContentView.delegate = contentViewDelegate
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
@ -176,10 +174,10 @@ class VisibleMessageView : LinearLayout {
private fun isEndOfMessageCluster(current: MessageRecord, next: MessageRecord?, isGroupThread: Boolean): Boolean {
return if (isGroupThread) {
next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp)
next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp)
|| current.recipient.address != next.recipient.address
} else {
next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp)
next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp)
|| current.isOutgoing != next.isOutgoing
}
}

View File

@ -32,6 +32,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private var duration = 0L
private var player: AudioSlidePlayer? = null
private var isPreparing = false
var delegate: VoiceMessageViewDelegate? = null
var index = -1
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
@ -84,6 +86,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
if (progress == 1.0) {
togglePlayback()
handleProgressChanged(0.0)
delegate?.playNextAudioIfPossible(index)
} else {
handleProgressChanged(progress)
}
@ -133,3 +136,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
}
// endregion
}
interface VoiceMessageViewDelegate {
fun playNextAudioIfPossible(current: Int)
}

View File

@ -6,7 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
import org.thoughtcrime.securesms.util.UiModeUtilities
open class BaseDialog : DialogFragment() {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
import org.session.libsession.utilities.TextSecurePreferences

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context
import android.graphics.Typeface
@ -13,6 +13,7 @@ import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.util.UiModeUtilities
import java.util.regex.Pattern
object MentionUtilities {

View File

@ -15,21 +15,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.utilities;
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Build;
import androidx.annotation.NonNull;
import org.session.libsignal.crypto.ecc.ECPublicKey;
import org.session.libsignal.crypto.IdentityKey;
import org.session.libsignal.crypto.IdentityKeyPair;
import org.session.libsignal.exceptions.InvalidKeyException;
import org.session.libsignal.crypto.ecc.Curve;
import org.session.libsignal.crypto.ecc.ECKeyPair;
import org.session.libsignal.crypto.ecc.ECPrivateKey;
import org.session.libsignal.crypto.ecc.ECPublicKey;
import org.session.libsignal.exceptions.InvalidKeyException;
import org.session.libsignal.utilities.Base64;
import java.io.IOException;
@ -45,19 +46,41 @@ public class IdentityKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
private static final String ENCRYPTED_SUFFIX = "_encrypted";
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
public static final String LOKI_SEED = "loki_seed";
public static final String HAS_MIGRATED_KEY = "has_migrated_keys";
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
}
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
SharedPreferences preferences = getSharedPreferences(context);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
return (preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF))
|| (preferences.contains(IDENTITY_PUBLIC_KEY_PREF+ENCRYPTED_SUFFIX) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF+ENCRYPTED_SUFFIX));
}
public static void checkUpdate(Context context) {
SharedPreferences preferences = getSharedPreferences(context);
// check if any keys are not migrated
if (hasIdentityKey(context) && !preferences.getBoolean(HAS_MIGRATED_KEY, false)) {
// this will retrieve and force upgrade if possible
// retrieve will force upgrade if available
retrieve(context,IDENTITY_PUBLIC_KEY_PREF);
retrieve(context,IDENTITY_PRIVATE_KEY_PREF);
retrieve(context,ED25519_PUBLIC_KEY);
retrieve(context,ED25519_SECRET_KEY);
retrieve(context,LOKI_SEED);
preferences.edit().putBoolean(HAS_MIGRATED_KEY, true).apply();
}
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
@ -94,14 +117,56 @@ public class IdentityKeyUtil {
public static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return preferences.getString(key, null);
String unencryptedSecret = preferences.getString(key, null);
String encryptedSecret = preferences.getString(key+ENCRYPTED_SUFFIX, null);
if (unencryptedSecret != null) return getUnencryptedSecret(key, unencryptedSecret, context);
else if (encryptedSecret != null) return getEncryptedSecret(encryptedSecret);
return null;
}
private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return unencryptedSecret;
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes());
// save the encrypted suffix secret "key_encrypted"
save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize());
// delete the regular secret "key"
delete(context,key);
return unencryptedSecret;
}
}
private static String getEncryptedSecret(String encryptedSecret) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
} else {
KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret);
return new String(KeyStoreHelper.unseal(sealedData));
}
}
public static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean isEncryptedSuffix = key.endsWith(ENCRYPTED_SUFFIX);
if (isEncryptedSuffix) {
preferencesEditor.putString(key, value);
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes());
preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize());
}
} else {
preferencesEditor.putString(key, value);
}
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}

View File

@ -1,4 +1,4 @@
package org.session.libsession.utilities
package org.thoughtcrime.securesms.crypto
import android.content.Context
import com.goterl.lazysodium.LazySodiumAndroid

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.crypto
import android.content.Context

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.net.Uri
import java.util.*

View File

@ -25,13 +25,8 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase;
public class DatabaseFactory {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import androidx.core.database.getStringOrNull

View File

@ -1,23 +1,26 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.Snode
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.Database
import org.session.libsignal.utilities.*
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.*
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.util.*
import java.util.*
import kotlin.Array
import kotlin.Boolean
import kotlin.Int
import kotlin.Long
import kotlin.Pair
import kotlin.String
import kotlin.arrayOf
import kotlin.to
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {

View File

@ -1,10 +1,9 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import java.lang.IllegalArgumentException
import java.util.*

View File

@ -1,14 +1,10 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import net.sqlcipher.database.SQLiteDatabase.CONFLICT_REPLACE
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsignal.database.LokiMessageDatabaseProtocol
import org.session.libsignal.utilities.Log
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@ -63,9 +59,9 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val database = databaseHelper.writableDatabase
val serverID = database.get(messageIDTable,
"${Companion.messageID} = ? AND ${Companion.messageType} = ?",
"${Companion.messageID} = ? AND $messageType = ?",
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
cursor.getInt(Companion.serverID).toLong()
cursor.getInt(serverID).toLong()
} ?: return
database.beginTransaction()
@ -89,7 +85,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
return database.get(messageIDTable,
"$messageID = ? AND ${Companion.serverID} = ?",
arrayOf(mappedID.toString(), mappedServerID.toString())) { cursor ->
cursor.getInt(Companion.messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE)
cursor.getInt(messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE)
}
}
@ -137,18 +133,18 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val database = databaseHelper.writableDatabase
try {
val messages = mutableSetOf<Pair<Long,Long>>()
database.get(messageThreadMappingTable, "${Companion.threadID} = ?", arrayOf(threadId.toString())) { cursor ->
database.get(messageThreadMappingTable, "$threadID = ?", arrayOf(threadId.toString())) { cursor ->
// for each add
while (cursor.moveToNext()) {
messages.add(cursor.getLong(Companion.messageID) to cursor.getLong(Companion.serverID))
messages.add(cursor.getLong(messageID) to cursor.getLong(serverID))
}
}
var deletedCount = 0L
database.beginTransaction()
messages.forEach { (messageId, serverId) ->
deletedCount += database.delete(messageIDTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageId.toString(), serverId.toString()))
deletedCount += database.delete(messageIDTable, "$messageID = ? AND $serverID = ?", arrayOf(messageId.toString(), serverId.toString()))
}
val mappingDeleted = database.delete(messageThreadMappingTable, "${Companion.threadID} = ?", arrayOf(threadId.toString()))
val mappingDeleted = database.delete(messageThreadMappingTable, "$threadID = ?", arrayOf(threadId.toString()))
database.setTransactionSuccessful()
} finally {
database.endTransaction()

View File

@ -1,12 +1,9 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient

View File

@ -1,9 +1,7 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.Context
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.get
import org.session.libsession.utilities.TextSecurePreferences
class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {

View File

@ -883,6 +883,7 @@ public class MmsDatabase extends MessagingDatabase {
}
public void deleteQuotedFromMessages(MessageRecord toDeleteRecord) {
if (toDeleteRecord == null) { return; }
String query = THREAD_ID + " = ?";
Cursor threadMmsCursor = rawQuery(query, new String[]{String.valueOf(toDeleteRecord.getThreadId())});
Reader reader = readerFor(threadMmsCursor);

View File

@ -1,13 +1,11 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import net.sqlcipher.Cursor
import org.session.libsession.messaging.contacts.Contact
import org.session.libsignal.utilities.Base64
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
@ -35,7 +33,7 @@ class SessionContactDatabase(context: Context, helper: SQLCipherOpenHelper) : Da
fun getContactWithSessionID(sessionID: String): Contact? {
val database = databaseHelper.readableDatabase
return database.get(sessionContactTable, "${SessionContactDatabase.sessionID} = ?", arrayOf( sessionID )) { cursor ->
return database.get(sessionContactTable, "${Companion.sessionID} = ?", arrayOf( sessionID )) { cursor ->
contactFromCursor(cursor)
}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.database
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
@ -6,10 +6,10 @@ import net.sqlcipher.Cursor
import org.session.libsession.messaging.jobs.*
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.*
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.util.*
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {

View File

@ -26,13 +26,11 @@ import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.SessionMetaProtocol
import org.thoughtcrime.securesms.mms.PartAuthority
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {

View File

@ -49,7 +49,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;

View File

@ -23,14 +23,13 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SessionContactDatabase;
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase;
import org.thoughtcrime.securesms.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.database.LokiUserDatabase;
import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase;
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@ -182,8 +181,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV12) {
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command());
db.execSQL(ClosedGroupsMigration.getCreateCurrentClosedGroupRatchetTableCommand());
db.execSQL(ClosedGroupsMigration.getCreateClosedGroupPrivateKeyTableCommand());
}
if (oldVersion < lokiV13) {
@ -193,10 +190,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV14_BACKUP_FILES) {
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
}
if (oldVersion < lokiV15) {
db.execSQL(ClosedGroupsMigration.getCreateOldClosedGroupRatchetTableCommand());
}
if (oldVersion < lokiV16) {
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
@ -217,7 +210,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV19) {
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
ClosedGroupsMigration.INSTANCE.perform(db);
db.execSQL("DROP TABLE identities");
deleteJobRecords(db, "RetrieveProfileJob");
deleteJobRecords(db,

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.dms
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@ -32,8 +32,8 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private val adapter = CreatePrivateChatActivityAdapter(this)
@ -191,6 +191,7 @@ class EnterPublicKeyFragment : Fragment() {
}
private fun handleIsKeyboardShowingChanged() {
val optionalContentContainer = optionalContentContainer ?: return
optionalContentContainer.isVisible = !isKeyboardShowing
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.dialogs
package org.thoughtcrime.securesms.groups
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.content.Context
import android.content.Intent
@ -19,14 +19,15 @@ import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
import org.thoughtcrime.securesms.contacts.SelectContactsLoader
//TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {

View File

@ -1,10 +1,11 @@
package org.thoughtcrime.securesms.loki.viewmodel
package org.thoughtcrime.securesms.groups
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.thoughtcrime.securesms.util.State
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
typealias GroupState = State<DefaultGroups>

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.content.Context
import android.content.Intent
@ -24,15 +24,15 @@ import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.content.Context
import org.thoughtcrime.securesms.database.DatabaseFactory

View File

@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.ViewGroup
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.contacts.UserView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@ -25,22 +25,20 @@ import network.loki.messenger.R
import okhttp3.HttpUrl
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.DistributionTypes
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
import org.thoughtcrime.securesms.loki.viewmodel.State
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.State
import java.util.*
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
@ -108,7 +106,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
} else {
throw Exception("No longer supported.")
}
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
withContext(Dispatchers.Main) {
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
openConversationActivity(this@JoinPublicChatActivity, threadID, recipient)

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.groups
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.api
package org.thoughtcrime.securesms.groups
import android.content.Context
import android.graphics.Bitmap
@ -11,7 +11,6 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle
import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.Executors
@ -116,6 +115,7 @@ object OpenGroupManager {
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
lokiThreadDB.removeOpenGroupChat(threadID)
ThreadUtils.queue {
threadDB.deleteConversation(threadID) // Must be invoked on a background thread
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.annotation.WorkerThread

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.dialogs
package org.thoughtcrime.securesms.home
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.home
import android.content.Context
import android.content.res.Resources
@ -15,8 +15,8 @@ import kotlinx.android.synthetic.main.view_conversation.view.*
import network.loki.messenger.R
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.*
@ -58,7 +58,7 @@ class ConversationView : LinearLayout {
profilePictureView.update(thread.recipient, thread.threadId)
val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString()
conversationViewDisplayNameTextView.text = senderDisplayName
timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date)
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE
val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context)

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.home
import android.app.AlertDialog
import android.content.BroadcastReceiver
@ -29,28 +29,31 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
import org.session.libsession.utilities.*
import org.session.libsignal.utilities.toHexString
import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.dialogs.*
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.ConversationView
import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity
import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity
import org.thoughtcrime.securesms.groups.JoinPublicChatActivity
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.util.IP2Country
import java.io.IOException
import java.util.*
class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
private lateinit var glide: GlideRequests
@ -156,8 +159,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
override fun onResume() {
super.onResume()
if (TextSecurePreferences.getLocalNumber(this) == null) {
return; } // This can be the case after a secondary device is auto-cleared
if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared
IdentityKeyUtil.checkUpdate(this)
profileButton.recycle() // clear cached image before update tje profilePictureView
profileButton.update()
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
@ -166,7 +169,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
}
if (TextSecurePreferences.getConfigurationMessageSynced(this)) {
lifecycleScope.launch(Dispatchers.IO) {
MultiDeviceProtocol.syncConfigurationIfNeeded(this@HomeActivity)
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
}
}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.home
import android.content.Context
import android.database.Cursor
@ -7,7 +7,6 @@ import android.view.ViewGroup
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.loki.views.ConversationView
import org.thoughtcrime.securesms.mms.GlideRequests
class HomeAdapter(context: Context, cursor: Cursor) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.home
import android.content.Context
import android.database.Cursor

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.home
import android.animation.FloatEvaluator
import android.animation.PointFEvaluator
@ -16,7 +16,9 @@ import android.widget.RelativeLayout
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.NewConversationButtonImageView
class NewConversationButtonSetView : RelativeLayout {
private var expandedButton: Button? = null

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.activities
package org.thoughtcrime.securesms.home
import android.content.BroadcastReceiver
import android.content.Context
@ -22,9 +22,10 @@ import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.Snode
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.PathDotView
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.PathDotView
class PathActivity : PassphraseRequiredActionBarActivity() {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.views
package org.thoughtcrime.securesms.home
import android.content.BroadcastReceiver
import android.content.Context
@ -12,8 +12,8 @@ import android.view.View
import androidx.annotation.ColorInt
import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toPx
class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.dialogs
package org.thoughtcrime.securesms.home
import android.annotation.SuppressLint
import android.content.ClipData
@ -17,7 +17,6 @@ import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.SSKEnvironment
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.mms.GlideApp

View File

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
import java.util.ArrayList;
import java.util.Arrays;

View File

@ -6,7 +6,7 @@ import org.session.libsession.messaging.utilities.Data;
import org.session.libsignal.utilities.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.loki.database.BackupFileRecord;
import org.thoughtcrime.securesms.database.BackupFileRecord;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.BackupUtil;

View File

@ -1,9 +1,7 @@
package org.thoughtcrime.securesms.loki.api
package org.thoughtcrime.securesms.jobs
import android.media.MediaDataSource
import android.os.Build
import org.session.libsignal.utilities.Log
import androidx.annotation.RequiresApi
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
@ -13,9 +11,7 @@ import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.mms.PartAuthority
import java.io.InputStream
import java.lang.IllegalStateException
import java.util.*
import java.util.concurrent.TimeUnit

View File

@ -1,71 +0,0 @@
package org.thoughtcrime.securesms.loki.api
import android.content.Context
import androidx.work.*
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
/**
* Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager.
*/
class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
companion object {
const val TAG = "PublicChatInfoUpdateWorker"
private const val DATA_KEY_SERVER_URL = "server_uRL"
private const val DATA_KEY_CHANNEL = "channel"
private const val DATA_KEY_ROOM = "room"
@JvmStatic
fun scheduleInstant(context: Context, serverUrl: String, room :String) {
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setInputData(workDataOf(
DATA_KEY_SERVER_URL to serverUrl,
DATA_KEY_ROOM to room
))
.build()
WorkManager
.getInstance(context)
.enqueue(workRequest)
}
@JvmStatic
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setInputData(workDataOf(
DATA_KEY_SERVER_URL to serverURL,
DATA_KEY_CHANNEL to channel
))
.build()
WorkManager
.getInstance(context)
.enqueue(workRequest)
}
}
override fun doWork(): Result {
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
val room = inputData.getString(DATA_KEY_ROOM)
val openGroupId = "$serverUrl.$room"
return try {
Log.v(TAG, "Updating open group info for $openGroupId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
Result.failure()
}
}
}

View File

@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.loki.dialogs
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.*
import network.loki.messenger.R
public class DeviceEditingOptionsBottomSheet : BottomSheetDialogFragment() {
var onEditTapped: (() -> Unit)? = null
var onUnlinkTapped: (() -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
editDisplayNameText.setOnClickListener { onEditTapped?.invoke() }
unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() }
}
}

View File

@ -1,73 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.ContentValues
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getAll
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.session.libsignal.utilities.Hex
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import java.util.*
object ClosedGroupsMigration {
public val closedGroupPublicKey = "closed_group_public_key"
// Ratchets
private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table"
private val currentClosedGroupRatchetTable = "closed_group_ratchet_table"
private val senderPublicKey = "sender_public_key"
private val chainKey = "chain_key"
private val keyIndex = "key_index"
private val messageKeys = "message_keys"
@JvmStatic val createOldClosedGroupRatchetTableCommand
= "CREATE TABLE $oldClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
@JvmStatic val createCurrentClosedGroupRatchetTableCommand
= "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
// Private keys
public val closedGroupPrivateKeyTable = "closed_group_private_key_table"
public val closedGroupPrivateKey = "closed_group_private_key"
@JvmStatic val createClosedGroupPrivateKeyTableCommand
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
fun perform(database: net.sqlcipher.database.SQLiteDatabase) {
val publicKeys = database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
cursor.getString(closedGroupPublicKey)
}.filter {
PublicKeyValidation.isValid(it)
}
val keyPairs = mutableListOf<ECKeyPair>()
for (publicKey in publicKeys) {
val query = "${closedGroupPublicKey} = ?"
val privateKey = database.get(closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor ->
cursor.getString(closedGroupPrivateKey)
}
val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(publicKey.removing05PrefixIfNeeded())), DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
keyPairs.add(keyPair)
val row = ContentValues(1)
row.put(LokiAPIDatabase.groupPublicKey, publicKey)
database.insertOrUpdate(LokiAPIDatabase.closedGroupPublicKeysTable, row, "${LokiAPIDatabase.groupPublicKey} = ?", arrayOf( publicKey ))
}
for (keyPair in keyPairs) {
// In this particular case keyPair.publicKey == groupPublicKey
val timestamp = Date().time.toString()
val index = "${keyPair.publicKey.serialize().toHexString()}-$timestamp"
val encryptionKeyPairPublicKey = keyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
val encryptionKeyPairPrivateKey = keyPair.privateKey.serialize().toHexString()
val row = ContentValues(3)
row.put(LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex, index)
row.put(LokiAPIDatabase.encryptionKeyPairPublicKey, encryptionKeyPairPublicKey)
row.put(LokiAPIDatabase.encryptionKeyPairPrivateKey, encryptionKeyPairPrivateKey)
database.insertOrUpdate(LokiAPIDatabase.closedGroupEncryptionKeyPairsTable, row, "${LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex} = ?", arrayOf( index ))
}
}
}

View File

@ -1,392 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import android.util.Log
import com.google.protobuf.ByteString
import org.session.libsession.messaging.sending_receiving.*
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import java.util.*
object ClosedGroupsProtocolV2 {
@JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
when (closedGroupUpdate.type) {
DataMessage.ClosedGroupControlMessage.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp)
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
else -> {
Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}")
}
}
}
private fun isValid(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long): Boolean {
val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey)
if (record != null) return false
return when (closedGroupUpdate.type) {
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
(!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0)
}
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED,
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> {
closedGroupUpdate.membersCount > 0
}
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
senderPublicKey.isNotEmpty()
}
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
!closedGroupUpdate.name.isNullOrEmpty()
}
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> true
else -> false
}
}
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long) {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Unwrap the message
val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString()
val name = closedGroupUpdate.name
val encryptionKeyPairAsProto = closedGroupUpdate.encryptionKeyPair
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
// Create the group
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val prevGroup = groupDB.getGroup(groupID).orNull()
if (prevGroup != null) {
// Update the group
groupDB.updateTitle(groupID, name)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else {
groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), sentTimestamp)
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Add the group to the user's set of public keys to poll for
apiDB.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user (if we didn't make the group)
if (userPublicKey != senderPublicKey) {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
} else if (prevGroup == null) {
// only notify if we created this group
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
}
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
}
fun handleClosedGroupMembersRemoved(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.toString() }
// Users that are part of this remove update
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
return
}
// If admin leaves the group is disbanded
val didAdminLeave = admins.any { it in updateMembers }
// newMembers to save is old members minus removed members
val newMembers = members - updateMembers
// user should be posting MEMBERS_LEFT so this should not be encountered
val senderLeft = senderPublicKey in updateMembers
if (senderLeft) {
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
}
val wasCurrentUserRemoved = userPublicKey in updateMembers
// admin should send a MEMBERS_LEFT message but handled here in case
if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
} else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
}
val type =
if (senderLeft) SignalServiceGroup.Type.QUIT
else SignalServiceGroup.Type.UPDATE
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
}
}
fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
// Check common group update logic
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
return
}
val name = group.title
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.serialize() }
// Users that are part of this add update
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
// newMembers to save is old members plus members included in this update
val newMembers = members + updateMembers
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
}
}
}
}
fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Check that the sender is a member of the group (before the update)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
// Check common group update logic
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
return
}
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.serialize() }
val name = closedGroupUpdate.name
groupDB.updateTitle(groupID, name)
// Notify the user
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
}
private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Check the user leaving isn't us, will already be handled
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.toString() }
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
return
}
// If the admin leaves the group is disbanded
val didAdminLeave = admins.contains(senderPublicKey)
val updatedMemberList = members - senderPublicKey
val userLeft = (userPublicKey == senderPublicKey)
// if the admin left, we left, or we are the only remaining member: remove the group
if (didAdminLeave || userLeft) {
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
} else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
// Notify user
if (userLeft) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
}
}
private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) {
apiDB.removeClosedGroupPublicKey(groupPublicKey)
// Remove the key pairs
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
// Mark the group as inactive
groupDB.setActive(groupID, false)
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
}
private fun isValidGroupUpdate(group: GroupRecord,
sentTimestamp: Long,
senderPublicKey: String): Boolean {
val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created
// TODO: We should check that formationTimestamp is the sent timestamp of the closed group update that created the group
if (group.formationTimestamp > sentTimestamp) {
Log.d("Loki", "Ignoring closed group update from before thread was created.")
return false
}
// Check that the sender is a member of the group (before the update)
if (senderPublicKey !in oldMembers) {
Log.d("Loki", "Ignoring closed group info message from non-member.")
return false
}
return true
}
private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, groupPublicKey: String, senderPublicKey: String) {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val userKeyPair = apiDB.getUserX25519KeyPair()
// Unwrap the message
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupPublicKeyToUse = when {
groupPublicKey.isNotEmpty() -> groupPublicKey
!closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString()
else -> ""
}
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKeyToUse)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.")
return
}
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
return
}
// Find our wrapper and decrypt it if possible
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
// Parse it
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
// Store it
apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKeyToUse)
Log.d("Loki", "Received a new closed group encryption key pair")
}
// region Deprecated
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Unwrap the message
val name = closedGroupUpdate.name
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val oldMembers = group.members.map { it.serialize() }
// Check common group update logic
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
return
}
// Check that the admin wasn't removed unless the group was destroyed entirely
if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) {
Log.d("Loki", "Ignoring invalid closed group update message.")
return
}
// Remove the group from the user's set of public keys to poll for if the current user was removed
val wasCurrentUserRemoved = !members.contains(userPublicKey)
if (wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
}
// Generate and distribute a new encryption key pair if needed
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
if (wasAnyUserRemoved && isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
}
// Update the group
groupDB.updateTitle(groupID, name)
if (!wasCurrentUserRemoved) {
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
}
// Notify the user
val wasSenderRemoved = !members.contains(senderPublicKey)
val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
val admins = group.admins.map { it.toString() }
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
}
}
// endregion
}

View File

@ -1,335 +0,0 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.AnimatedVectorDrawable
import android.util.AttributeSet
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import kotlinx.coroutines.*
import network.loki.messenger.R
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.thoughtcrime.securesms.ApplicationContext
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.events.PartProgressEvent
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.SlideClickListener
import java.io.IOException
import java.util.*
import java.util.concurrent.TimeUnit
class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
companion object {
private const val TAG = "AudioViewKt"
}
private val controlToggle: AnimatingToggle
private val container: ViewGroup
private val playButton: ImageView
private val pauseButton: ImageView
private val downloadButton: ImageView
private val downloadProgress: ProgressBar
private val seekBar: WaveformSeekBar
private val totalDuration: TextView
private var downloadListener: SlideClickListener? = null
private var audioSlidePlayer: AudioSlidePlayer? = null
/** Background coroutine scope that is available when the view is attached to a window. */
private var asyncCoroutineScope: CoroutineScope? = null
private val loadingAnimation: SeekBarLoadingAnimation
constructor(context: Context): this(context, null)
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
View.inflate(context, R.layout.message_audio_view, this)
container = findViewById(R.id.audio_widget_container)
controlToggle = findViewById(R.id.control_toggle)
playButton = findViewById(R.id.play)
pauseButton = findViewById(R.id.pause)
downloadButton = findViewById(R.id.download)
downloadProgress = findViewById(R.id.download_progress)
seekBar = findViewById(R.id.seek)
totalDuration = findViewById(R.id.total_duration)
playButton.setOnClickListener {
try {
Log.d(TAG, "playbutton onClick")
if (audioSlidePlayer != null) {
togglePlayToPause()
// Restart the playback if progress bar is nearly at the end.
val progress = if (seekBar.progress < 0.99f) seekBar.progress.toDouble() else 0.0
audioSlidePlayer!!.play(progress)
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
pauseButton.setOnClickListener {
Log.d(TAG, "pausebutton onClick")
if (audioSlidePlayer != null) {
togglePauseToPlay()
audioSlidePlayer!!.stop()
}
}
seekBar.isEnabled = false
seekBar.progressChangeListener = object : WaveformSeekBar.ProgressChangeListener {
override fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean) {
if (fromUser && audioSlidePlayer != null) {
synchronized(audioSlidePlayer!!) {
audioSlidePlayer!!.seekTo(progress.toDouble())
}
}
}
}
playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon))
pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon))
playButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp)
pauseButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp)
if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageAudioView, 0, 0)
setTint(typedArray.getColor(R.styleable.MessageAudioView_foregroundTintColor, Color.WHITE),
typedArray.getColor(R.styleable.MessageAudioView_waveformFillColor, Color.WHITE),
typedArray.getColor(R.styleable.MessageAudioView_waveformBackgroundColor, Color.WHITE))
container.setBackgroundColor(typedArray.getColor(R.styleable.MessageAudioView_widgetBackground, Color.TRANSPARENT))
typedArray.recycle()
}
loadingAnimation = SeekBarLoadingAnimation(this, seekBar)
loadingAnimation.start()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
asyncCoroutineScope = CoroutineScope(Job() + Dispatchers.IO)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
EventBus.getDefault().unregister(this)
// Cancel all the background operations.
asyncCoroutineScope!!.cancel()
asyncCoroutineScope = null
}
fun setAudio(audio: AudioSlide, showControls: Boolean) {
when {
showControls && audio.isPendingDownload -> {
controlToggle.displayQuick(downloadButton)
seekBar.isEnabled = false
downloadButton.setOnClickListener { v -> downloadListener?.onClick(v, audio) }
if (downloadProgress.isIndeterminate) {
downloadProgress.isIndeterminate = false
downloadProgress.progress = 0
}
}
(showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> {
controlToggle.displayQuick(downloadProgress)
seekBar.isEnabled = false
downloadProgress.isIndeterminate = true
}
else -> {
controlToggle.displayQuick(playButton)
seekBar.isEnabled = true
if (downloadProgress.isIndeterminate) {
downloadProgress.isIndeterminate = false
downloadProgress.progress = 100
}
// Post to make sure it executes only when the view is attached to a window.
post(::updateFromAttachmentAudioExtras)
}
}
audioSlidePlayer = AudioSlidePlayer.createFor(context, audio, this)
}
fun cleanup() {
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
audioSlidePlayer!!.stop()
}
}
fun setDownloadClickListener(listener: SlideClickListener?) {
downloadListener = listener
}
fun setTint(@ColorInt foregroundTint: Int, @ColorInt waveformFill: Int, @ColorInt waveformBackground: Int) {
playButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
playButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
pauseButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
pauseButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN)
downloadProgress.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
downloadProgress.progressTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
downloadProgress.indeterminateTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
totalDuration.setTextColor(foregroundTint)
seekBar.barProgressColor = waveformFill
seekBar.barBackgroundColor = waveformBackground
}
override fun onPlayerStart(player: AudioSlidePlayer) {
if (pauseButton.visibility != View.VISIBLE) {
togglePlayToPause()
}
}
override fun onPlayerStop(player: AudioSlidePlayer) {
if (playButton.visibility != View.VISIBLE) {
togglePauseToPlay()
}
}
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, millis: Long) {
seekBar.progress = progress.toFloat()
}
override fun setFocusable(focusable: Boolean) {
super.setFocusable(focusable)
playButton.isFocusable = focusable
pauseButton.isFocusable = focusable
seekBar.isFocusable = focusable
seekBar.isFocusableInTouchMode = focusable
downloadButton.isFocusable = focusable
}
override fun setClickable(clickable: Boolean) {
super.setClickable(clickable)
playButton.isClickable = clickable
pauseButton.isClickable = clickable
seekBar.isClickable = clickable
seekBar.setOnTouchListener(if (clickable) null else
OnTouchListener { _, _ -> return@OnTouchListener true }) // Suppress touch events.
downloadButton.isClickable = clickable
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
playButton.isEnabled = enabled
pauseButton.isEnabled = enabled
downloadButton.isEnabled = enabled
}
private fun togglePlayToPause() {
controlToggle.displayQuick(pauseButton)
val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable
pauseButton.setImageDrawable(playToPauseDrawable)
playToPauseDrawable.start()
}
private fun togglePauseToPlay() {
controlToggle.displayQuick(playButton)
val pauseToPlayDrawable = ContextCompat.getDrawable(context, R.drawable.pause_to_play_animation) as AnimatedVectorDrawable
playButton.setImageDrawable(pauseToPlayDrawable)
pauseToPlayDrawable.start()
}
private fun obtainDatabaseAttachment(): DatabaseAttachment? {
audioSlidePlayer ?: return null
val attachment = audioSlidePlayer!!.audioSlide.asAttachment()
return if (attachment is DatabaseAttachment) attachment else null
}
private fun updateFromAttachmentAudioExtras() {
val attachment = obtainDatabaseAttachment() ?: return
val audioExtras = DatabaseFactory.getAttachmentDatabase(context)
.getAttachmentAudioExtras(attachment.attachmentId)
// Schedule a job request if no audio extras were generated yet.
if (audioExtras == null) {
ApplicationContext.getInstance(context).jobManager
.add(PrepareAttachmentAudioExtrasJob(attachment.attachmentId))
return
}
loadingAnimation.stop()
seekBar.sampleData = audioExtras.visualSamples
if (audioExtras.durationMs > 0) {
totalDuration.visibility = View.VISIBLE
totalDuration.text = String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEvent(event: PartProgressEvent) {
if (audioSlidePlayer != null && event.attachment == audioSlidePlayer!!.audioSlide.asAttachment()) {
val progress = ((event.progress.toFloat() / event.total) * 100f).toInt()
downloadProgress.progress = progress
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent) {
if (event.attachmentId == obtainDatabaseAttachment()?.attachmentId) {
updateFromAttachmentAudioExtras()
}
}
private class SeekBarLoadingAnimation(
private val hostView: View,
private val seekBar: WaveformSeekBar): Runnable {
private var active = false
companion object {
private const val UPDATE_PERIOD = 250L // In milliseconds.
private val random = Random()
}
fun start() {
stop()
active = true
hostView.postDelayed(this, UPDATE_PERIOD)
}
fun stop() {
active = false
hostView.removeCallbacks(this)
}
override fun run() {
if (!active) return
// Generate a random samples with values up to the 50% of the maximum value.
seekBar.sampleData = ByteArray(PrepareAttachmentAudioExtrasJob.VISUAL_RMS_FRAMES)
{ (random.nextInt(127) - 64).toByte() }
hostView.postDelayed(this, UPDATE_PERIOD)
}
}
}

View File

@ -1,71 +0,0 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.OpenGroupUrlParser
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
class OpenGroupInvitationView : FrameLayout {
private val joinButton: ImageView
private val openGroupIconContainer: RelativeLayout
private val openGroupIconImageView: ImageView
private val nameTextView: TextView
private val urlTextView: TextView
private var url: String = ""
constructor(context: Context): this(context, null)
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
View.inflate(context, R.layout.open_group_invitation_view, this)
joinButton = findViewById(R.id.join_open_group_button)
openGroupIconContainer = findViewById(R.id.open_group_icon_image_view_container)
openGroupIconImageView = findViewById(R.id.open_group_icon_image_view)
nameTextView = findViewById(R.id.name_text_view)
urlTextView = findViewById(R.id.url_text_view)
joinButton.setOnClickListener { joinOpenGroup(url) }
}
fun setOpenGroup(name: String, url: String, isOutgoing: Boolean = false) {
nameTextView.text = name
urlTextView.text = OpenGroupUrlParser.trimQueryParameter(url)
this.url = url
joinButton.visibility = if (isOutgoing) View.GONE else View.VISIBLE
openGroupIconContainer.visibility = if (isOutgoing) View.VISIBLE else View.GONE
}
private fun joinOpenGroup(url: String) {
val openGroup = OpenGroupUrlParser.parseUrl(url)
val builder = AlertDialog.Builder(context)
builder.setTitle(context.getString(R.string.ConversationActivity_join_open_group, nameTextView.text.toString()))
builder.setCancelable(true)
val message: String =
context.getString(R.string.ConversationActivity_join_open_group_confirmation_message, nameTextView.text.toString())
builder.setMessage(message)
builder.setPositiveButton(R.string.yes) { dialog, _ ->
GlobalScope.launch(Dispatchers.IO) {
try {
dialog.dismiss()
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, context)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(context)
} catch (e: Exception) {
Log.e("Loki", "Failed to join open group.", e)
Toast.makeText(context, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
}
}
}
builder.setNegativeButton(R.string.no, null)
builder.show()
}
}

View File

@ -1,315 +0,0 @@
package org.thoughtcrime.securesms.loki.views
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import androidx.core.math.MathUtils
import network.loki.messenger.R
import org.session.libsession.utilities.byteToNormalizedFloat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class WaveformSeekBar : View {
companion object {
@JvmStatic
fun dp(context: Context, dp: Float): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.resources.displayMetrics
)
}
}
private val sampleDataHolder = SampleDataHolder(::invalidate)
/** An array of signed byte values representing the audio signal. */
var sampleData: ByteArray?
get() {
return sampleDataHolder.getSamples()
}
set(value) {
sampleDataHolder.setSamples(value)
invalidate()
}
/** Indicates whether the user is currently interacting with the view and performing a seeking gesture. */
private var userSeeking = false
private var _progress: Float = 0f
/** In [0..1] range. */
var progress: Float
set(value) {
// Do not let to modify the progress value from the outside
// when the user is currently interacting with the view.
if (userSeeking) return
_progress = value
invalidate()
progressChangeListener?.onProgressChanged(this, _progress, false)
}
get() {
return _progress
}
var barBackgroundColor: Int = Color.LTGRAY
set(value) {
field = value
invalidate()
}
var barProgressColor: Int = Color.WHITE
set(value) {
field = value
invalidate()
}
var barGap: Float = dp(context, 2f)
set(value) {
field = value
invalidate()
}
var barWidth: Float = dp(context, 5f)
set(value) {
field = value
invalidate()
}
var barMinHeight: Float = barWidth
set(value) {
field = value
invalidate()
}
var barCornerRadius: Float = dp(context, 2.5f)
set(value) {
field = value
invalidate()
}
var barGravity: WaveGravity = WaveGravity.CENTER
set(value) {
field = value
invalidate()
}
var progressChangeListener: ProgressChangeListener? = null
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val barRect = RectF()
private var canvasWidth = 0
private var canvasHeight = 0
private var touchDownX = 0f
private var touchDownProgress: Float = 0f
private var scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: super(context, attrs, defStyleAttr) {
val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar)
barWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_width, barWidth)
barGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_gap, barGap)
barCornerRadius = typedAttrs.getDimension(
R.styleable.WaveformSeekBar_bar_corner_radius,
barCornerRadius)
barMinHeight =
typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight)
barBackgroundColor = typedAttrs.getColor(
R.styleable.WaveformSeekBar_bar_background_color,
barBackgroundColor)
barProgressColor =
typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor)
progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress)
barGravity = WaveGravity.fromString(
typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity))
typedAttrs.recycle()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
canvasWidth = w
canvasHeight = h
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val totalWidth = getAvailableWidth()
val barAmount = (totalWidth / (barWidth + barGap)).toInt()
var lastBarRight = paddingLeft.toFloat()
(0 until barAmount).forEach { barIdx ->
// Convert a signed byte to a [0..1] float.
val barValue = byteToNormalizedFloat(sampleDataHolder.computeBarValue(barIdx, barAmount))
val barHeight = max(barMinHeight, getAvailableHeight() * barValue)
val top: Float = when (barGravity) {
WaveGravity.TOP -> paddingTop.toFloat()
WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f
WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight
}
barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight)
barPaint.color = if (barRect.right <= totalWidth * progress)
barProgressColor else barBackgroundColor
canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint)
lastBarRight = barRect.right + barGap
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
userSeeking = true
touchDownX = event.x
touchDownProgress = progress
updateProgress(event, false)
}
MotionEvent.ACTION_MOVE -> {
// Prevent any parent scrolling if the user scrolled more
// than scaledTouchSlop on horizontal axis.
if (abs(event.x - touchDownX) > scaledTouchSlop) {
parent.requestDisallowInterceptTouchEvent(true)
}
updateProgress(event, false)
}
MotionEvent.ACTION_UP -> {
userSeeking = false
updateProgress(event, true)
performClick()
}
MotionEvent.ACTION_CANCEL -> {
updateProgress(touchDownProgress, false)
userSeeking = false
}
}
return true
}
private fun updateProgress(event: MotionEvent, notify: Boolean) {
updateProgress(event.x / getAvailableWidth(), notify)
}
private fun updateProgress(progress: Float, notify: Boolean) {
_progress = MathUtils.clamp(progress, 0f, 1f)
invalidate()
if (notify) {
progressChangeListener?.onProgressChanged(this, _progress, true)
}
}
override fun performClick(): Boolean {
super.performClick()
return true
}
private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight
private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom
private class SampleDataHolder(private val invalidateDelegate: () -> Any) {
private var sampleDataFrom: ByteArray? = null
private var sampleDataTo: ByteArray? = null
private var progress = 1f // Mix between from and to values.
private var animation: ValueAnimator? = null
fun computeBarValue(barIdx: Int, barAmount: Int): Byte {
/** @return The array's value at the interpolated index. */
fun getSampleValue(sampleData: ByteArray?): Byte {
if (sampleData == null || sampleData.isEmpty())
return Byte.MIN_VALUE
else {
val sampleIdx = (barIdx * (sampleData.size / barAmount.toFloat())).toInt()
return sampleData[sampleIdx]
}
}
if (progress == 1f) {
return getSampleValue(sampleDataTo)
}
val fromValue = getSampleValue(sampleDataFrom)
val toValue = getSampleValue(sampleDataTo)
val rawResultValue = fromValue * (1f - progress) + toValue * progress
return rawResultValue.roundToInt().toByte()
}
fun setSamples(sampleData: ByteArray?) {
/** @return a mix between [sampleDataFrom] and [sampleDataTo] arrays according to the current [progress] value. */
fun computeNewDataFromArray(): ByteArray? {
if (sampleDataTo == null) return null
if (sampleDataFrom == null) return sampleDataTo
val sampleSize = min(sampleDataFrom!!.size, sampleDataTo!!.size)
return ByteArray(sampleSize) { i -> computeBarValue(i, sampleSize) }
}
sampleDataFrom = computeNewDataFromArray()
sampleDataTo = sampleData
progress = 0f
animation?.cancel()
animation = ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener { animation ->
progress = animation.animatedValue as Float
invalidateDelegate()
}
interpolator = DecelerateInterpolator(3f)
duration = 500
start()
}
}
fun getSamples(): ByteArray? {
return sampleDataTo
}
}
enum class WaveGravity {
TOP,
CENTER,
BOTTOM,
;
companion object {
@JvmStatic
fun fromString(gravity: String?): WaveGravity = when (gravity) {
"1" -> TOP
"2" -> CENTER
else -> BOTTOM
}
}
}
interface ProgressChangeListener {
fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean)
}
}

View File

@ -15,7 +15,7 @@ import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import network.loki.messenger.R;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.api
package org.thoughtcrime.securesms.notifications
import android.content.BroadcastReceiver
import android.content.Context

View File

@ -57,9 +57,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.loki.api.OpenGroupManager;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.SpanUtil;

View File

@ -1,5 +1,5 @@
@file:JvmName("FcmUtils")
package org.thoughtcrime.securesms.loki.utilities
package org.thoughtcrime.securesms.notifications
import com.google.android.gms.tasks.Task
import com.google.firebase.iid.FirebaseInstanceId

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.loki.api
package org.thoughtcrime.securesms.notifications
import android.content.Context
import nl.komponents.kovenant.functional.map

Some files were not shown because too many files have changed in this diff Show More