mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-12 00:13:38 +00:00
Merge branch 'refactor' of https://github.com/RyanRory/loki-messenger-android into refactor
This commit is contained in:
commit
80fa37e1a7
54
TRANSLATION.md
Normal file
54
TRANSLATION.md
Normal file
@ -0,0 +1,54 @@
|
||||
## How to translate Session into new languages quickly and easily
|
||||
|
||||
There are people all around the world who can benefit from access to Session — and that means we need to make Session available in as many languages as possible. We’ve had a number of requests from community members wanting to help out by translating Session into currently unsupported (or only partially supported) languages. If you speak multiple languages and you want to help us make Session available in another language you speak, this guide will show you how to translate Session’s iOS and Android apps quickly and easily.
|
||||
|
||||
### Translating Session for iOS:
|
||||
|
||||
Translating Session iOS into a new language is easy! You’ll need a web browser, a plaintext editor (TextEdit on macOS or Notepad on Windows will do), and a little bit of time and patience.
|
||||
|
||||
#### Step 1: Retrieving English strings
|
||||
- Go to [Session’s iOS GitHub page](https://github.com/loki-project/session-ios)
|
||||
- In the list of folders and files, click **Session**
|
||||
- Click **Meta**
|
||||
- Click **Translations**
|
||||
- Click **en.lproj**
|
||||
- Click **localizable.strings**
|
||||
- Scroll down to Line #2557 (using the line numbers on the left-hand side of the text). This line should contain the text `// MARK: - Session` (note: if this line does not contain that text, use Ctrl+F, Cmd+F, or your browser’s Find on Page function to find the line which contains that text)
|
||||
- Select and copy all text from that line down to the end of the file
|
||||
- Open a plaintext editor (TextEdit on macOS, Notepad on Windows, or your preferred plaintext editor) and create a new file
|
||||
- Paste the text you copied earlier into this blank file
|
||||
|
||||
#### Step 2: Translate!
|
||||
This file will now have a large number of lines of code. Each line contains 2 sets of information, both in quotation marks, like this:
|
||||
|
||||
`"continue_2" = "Continue";`
|
||||
|
||||
For each line of code, translate the word or sentence to the **right** of the equals sign into the language you are translating into. Do **not** edit the text to the **left** of the equals sign.
|
||||
|
||||
Once you are finished translating, save the file with a filename that specifies which language you have translated the app into, and email the file to us at support@getsession.org along with information about the language you have translated the app into. Be sure to specify in your email that you have translated the iOS app (for information on translating the Android app, see below). Our developers will then take your translation and apply it to the Session iOS app. Thank you for helping make Session more accessible for everyone!
|
||||
|
||||
|
||||
### Translating for Android:
|
||||
|
||||
It’s just as easy to add new translations on Session Android! Once again, you’ll need a web browser, a plaintext editor (TextEdit on macOS or Notepad on Windows will do), and a little bit of time and patience.
|
||||
|
||||
#### Step 1: Retrieving English strings
|
||||
- Go to [Session’s Android GitHub page](https://github.com/loki-project/session-android)
|
||||
- In the list of files and folders, click **res**
|
||||
- Click **values** (you will need to scroll down to find this folder; make sure you click the folder named **values** and not any of the folders named **values-xx** or with other suffixes)
|
||||
- Click **strings.xml**
|
||||
- Scroll down to Line #1657 (using the line numbers on the left-hand side of the text). This line should contain the text `<!-- Session -->` (note: if this line does not contain that text, use Ctrl+F, Cmd+F, or your browser’s Find on Page function to find the line which contains that text)
|
||||
- Select and copy all text from that line down to the end of the file
|
||||
- Open a plaintext editor (TextEdit on macOS, Notepad on Windows, or your preferred plaintext editor) and create a new file
|
||||
- Paste the text you copied earlier into this blank file
|
||||
|
||||
#### Step 2: Translate!
|
||||
This file will now have a large number of lines of code. Each line will contain `<string name=”xxx”>` and `</string>` tags. To translate Session Android, translate the text between these tags. For example:
|
||||
|
||||
`<string name="continue_2">Continue</string>`
|
||||
|
||||
In this line, translate the word **Continue** into the language you are translating into. Do not translate any other text. Do **not** translate the text inside either pair of angled brackets <>.
|
||||
|
||||
Translate the word or sentence between each pair of `<string></string>` tags, on each line.
|
||||
|
||||
Once you are finished translating, save the file with a filename that specifies which language you have translated the app into, and email the file to us at support@getsession.org along with information about the language you have translated the app into. Be sure to specify in your email that you have translated the Android app (for information on translating the iOS app, see above). Our developers will then take your translation and apply it to the Session Android app. Thank you for helping make Session more accessible for everyone!
|
@ -278,11 +278,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.ExperienceUpgradeActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.PassphraseCreateActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@ -312,13 +307,6 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.TextSecure.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.PassphraseChangeActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:label="@string/AndroidManifest__change_passphrase" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.VerifyIdentityActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.stickers.StickerManagementActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@ -483,11 +471,6 @@
|
||||
<action android:name="network.loki.securesms.RESTART" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.RotateSenderCertificateListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
@ -38,7 +38,6 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
@ -53,7 +52,6 @@ import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@ -84,7 +82,6 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -112,7 +109,6 @@ import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeys
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementationDelegate;
|
||||
import org.session.libsignal.service.loki.protocol.mentions.MentionsManager;
|
||||
import org.session.libsignal.service.loki.protocol.meta.SessionMetaProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
|
||||
@ -227,7 +223,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
initializePeriodicTasks();
|
||||
initializeWebRtc();
|
||||
initializePendingMessages();
|
||||
initializeUnidentifiedDeliveryAbilityRefresh();
|
||||
initializeBlobProvider();
|
||||
}
|
||||
|
||||
@ -376,7 +371,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
LocalBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
BackgroundPollWorker.schedulePeriodic(this); // Loki
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
@ -427,12 +421,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeUnidentifiedDeliveryAbilityRefresh() {
|
||||
if (TextSecurePreferences.isMultiDevice(this) && !TextSecurePreferences.isUnidentifiedDeliveryEnabled(this)) {
|
||||
jobManager.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBlobProvider() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
@ -574,7 +562,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
});
|
||||
}
|
||||
|
||||
public void clearData() {
|
||||
//FIXME AC: Using this method to cleanup app data is unsafe due to potential concurrent
|
||||
// activity that still might be using the data that is being deleted here.
|
||||
// The most reliable and safe way to do this is to use official API call:
|
||||
// https://developer.android.com/reference/android/app/ActivityManager.html#clearApplicationUserData()
|
||||
// The downside is it kills the app in the process and there's no any conventional way to start
|
||||
// another activity when the task is done.
|
||||
// Dev community is in demand for such a feature, so check on it some time in the feature
|
||||
// and replace our implementation with the API call when it's safe to do so.
|
||||
// Here's a feature request related https://issuetracker.google.com/issues/174903931
|
||||
public void clearAllData() {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
LokiPushNotificationManager.unregister(token, this);
|
||||
@ -582,7 +579,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
boolean wasUnlinked = TextSecurePreferences.getWasUnlinked(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
TextSecurePreferences.setWasUnlinked(this, wasUnlinked);
|
||||
MasterSecretUtil.clear(this);
|
||||
// MasterSecretUtil.clear(this);
|
||||
if (!deleteDatabase("signal.db")) {
|
||||
Log.d("Loki", "Failed to delete database.");
|
||||
}
|
||||
|
@ -1,201 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.session.libsignal.libsignal.SignalProtocolAddress;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
|
||||
|
||||
private OnClickListener callback;
|
||||
|
||||
public ConfirmIdentityDialog(Context context,
|
||||
MessageRecord messageRecord,
|
||||
IdentityKeyMismatch mismatch)
|
||||
{
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.from(context, mismatch.getAddress(), false);
|
||||
String name = recipient.toShortString();
|
||||
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(context, mismatch),
|
||||
introduction.length()+1, spannableString.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
setTitle(name);
|
||||
setMessage(spannableString);
|
||||
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getAddress()));
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new CancelListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
((TextView)this.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public void setCallback(OnClickListener callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
private class AcceptListener implements OnClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
private final Address address;
|
||||
|
||||
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, Address address) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.mismatch = mismatch;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(address.toPhoneString(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
|
||||
|
||||
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
|
||||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void processMessageRecord(MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
|
||||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
|
||||
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
|
||||
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor);
|
||||
MessageRecord record;
|
||||
|
||||
try {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.equals(recordMismatch)) {
|
||||
processMessageRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroupRecipient()) {
|
||||
MessageSender.resendGroupMessage(getContext(), messageRecord, mismatch.getAddress());
|
||||
} else {
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
boolean legacy = !messageRecord.isContentBundleKeyExchange();
|
||||
|
||||
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
|
||||
messageRecord.getIndividualRecipient().getAddress().toPhoneString(),
|
||||
messageRecord.getRecipientDeviceId(),
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
!legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
0, null);
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new PushDecryptJob(getContext(), pushId, messageRecord.getId()));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -23,15 +23,10 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
@ -40,17 +35,10 @@ import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
@ -60,72 +48,72 @@ import network.loki.messenger.R;
|
||||
public class DatabaseUpgradeActivity extends BaseActivity {
|
||||
private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName();
|
||||
|
||||
public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 0; // 46
|
||||
public static final int MMS_BODY_VERSION = 0; // 46
|
||||
public static final int TOFU_IDENTITIES_VERSION = 1; // 50
|
||||
public static final int CURVE25519_VERSION = 2; // 63
|
||||
public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 3; // 73
|
||||
public static final int NO_V1_VERSION = 4; // 83
|
||||
public static final int SIGNED_PREKEY_VERSION = 4; // 83
|
||||
public static final int NO_DECRYPT_QUEUE_VERSION = 5; // 113
|
||||
public static final int PUSH_DECRYPT_SERIAL_ID_VERSION = 6; // 131
|
||||
public static final int MIGRATE_SESSION_PLAINTEXT = 7; // 136
|
||||
public static final int CONTACTS_ACCOUNT_VERSION = 7; // 136
|
||||
public static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 8; // 151
|
||||
public static final int REDPHONE_SUPPORT_VERSION = 9; // 157
|
||||
public static final int NO_MORE_CANONICAL_DB_VERSION = 10; // 276
|
||||
public static final int PROFILES = 11; // 289
|
||||
public static final int SCREENSHOTS = 12; // 300
|
||||
public static final int PERSISTENT_BLOBS = 13; // 317
|
||||
public static final int INTERNALIZE_CONTACTS = 13; // 317
|
||||
public static final int SQLCIPHER = 14; // 334
|
||||
public static final int SQLCIPHER_COMPLETE = 15; // 352
|
||||
public static final int REMOVE_JOURNAL = 16; // 353
|
||||
public static final int REMOVE_CACHE = 17; // 354
|
||||
public static final int FULL_TEXT_SEARCH = 18; // 358
|
||||
public static final int BAD_IMPORT_CLEANUP = 19; // 373
|
||||
public static final int IMAGE_CACHE_CLEANUP = 20; // 406
|
||||
public static final int WORKMANAGER_MIGRATION = 21; // 408
|
||||
public static final int COLOR_MIGRATION = 22; // 412
|
||||
public static final int UNIDENTIFIED_DELIVERY = 23; // 422
|
||||
public static final int SIGNALING_KEY_DEPRECATION = 24; // 447
|
||||
public static final int CONVERSATION_SEARCH = 25; // 455
|
||||
// public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 0; // 46
|
||||
// public static final int MMS_BODY_VERSION = 0; // 46
|
||||
// public static final int TOFU_IDENTITIES_VERSION = 1; // 50
|
||||
// public static final int CURVE25519_VERSION = 2; // 63
|
||||
// public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 3; // 73
|
||||
// public static final int NO_V1_VERSION = 4; // 83
|
||||
// public static final int SIGNED_PREKEY_VERSION = 4; // 83
|
||||
// public static final int NO_DECRYPT_QUEUE_VERSION = 5; // 113
|
||||
// public static final int PUSH_DECRYPT_SERIAL_ID_VERSION = 6; // 131
|
||||
// public static final int MIGRATE_SESSION_PLAINTEXT = 7; // 136
|
||||
// public static final int CONTACTS_ACCOUNT_VERSION = 7; // 136
|
||||
// public static final int MEDIA_DOWNLOAD_CONTROLS_VERSION = 8; // 151
|
||||
// public static final int REDPHONE_SUPPORT_VERSION = 9; // 157
|
||||
// public static final int NO_MORE_CANONICAL_DB_VERSION = 10; // 276
|
||||
// public static final int PROFILES = 11; // 289
|
||||
// public static final int SCREENSHOTS = 12; // 300
|
||||
// public static final int PERSISTENT_BLOBS = 13; // 317
|
||||
// public static final int INTERNALIZE_CONTACTS = 13; // 317
|
||||
// public static final int SQLCIPHER = 14; // 334
|
||||
// public static final int SQLCIPHER_COMPLETE = 15; // 352
|
||||
// public static final int REMOVE_JOURNAL = 16; // 353
|
||||
// public static final int REMOVE_CACHE = 17; // 354
|
||||
// public static final int FULL_TEXT_SEARCH = 18; // 358
|
||||
// public static final int BAD_IMPORT_CLEANUP = 19; // 373
|
||||
// public static final int IMAGE_CACHE_CLEANUP = 20; // 406
|
||||
// public static final int WORKMANAGER_MIGRATION = 21; // 408
|
||||
// public static final int COLOR_MIGRATION = 22; // 412
|
||||
// public static final int UNIDENTIFIED_DELIVERY = 23; // 422
|
||||
// public static final int SIGNALING_KEY_DEPRECATION = 24; // 447
|
||||
// public static final int CONVERSATION_SEARCH = 25; // 455
|
||||
|
||||
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
|
||||
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
|
||||
add(TOFU_IDENTITIES_VERSION);
|
||||
add(CURVE25519_VERSION);
|
||||
add(ASYMMETRIC_MASTER_SECRET_FIX_VERSION);
|
||||
add(NO_V1_VERSION);
|
||||
add(SIGNED_PREKEY_VERSION);
|
||||
add(NO_DECRYPT_QUEUE_VERSION);
|
||||
add(PUSH_DECRYPT_SERIAL_ID_VERSION);
|
||||
add(MIGRATE_SESSION_PLAINTEXT);
|
||||
add(MEDIA_DOWNLOAD_CONTROLS_VERSION);
|
||||
add(REDPHONE_SUPPORT_VERSION);
|
||||
add(NO_MORE_CANONICAL_DB_VERSION);
|
||||
add(SCREENSHOTS);
|
||||
add(INTERNALIZE_CONTACTS);
|
||||
add(PERSISTENT_BLOBS);
|
||||
add(SQLCIPHER);
|
||||
add(SQLCIPHER_COMPLETE);
|
||||
add(REMOVE_CACHE);
|
||||
add(FULL_TEXT_SEARCH);
|
||||
add(BAD_IMPORT_CLEANUP);
|
||||
add(IMAGE_CACHE_CLEANUP);
|
||||
add(WORKMANAGER_MIGRATION);
|
||||
add(COLOR_MIGRATION);
|
||||
add(UNIDENTIFIED_DELIVERY);
|
||||
add(SIGNALING_KEY_DEPRECATION);
|
||||
add(CONVERSATION_SEARCH);
|
||||
// add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
|
||||
// add(TOFU_IDENTITIES_VERSION);
|
||||
// add(CURVE25519_VERSION);
|
||||
// add(ASYMMETRIC_MASTER_SECRET_FIX_VERSION);
|
||||
// add(NO_V1_VERSION);
|
||||
// add(SIGNED_PREKEY_VERSION);
|
||||
// add(NO_DECRYPT_QUEUE_VERSION);
|
||||
// add(PUSH_DECRYPT_SERIAL_ID_VERSION);
|
||||
// add(MIGRATE_SESSION_PLAINTEXT);
|
||||
// add(MEDIA_DOWNLOAD_CONTROLS_VERSION);
|
||||
// add(REDPHONE_SUPPORT_VERSION);
|
||||
// add(NO_MORE_CANONICAL_DB_VERSION);
|
||||
// add(SCREENSHOTS);
|
||||
// add(INTERNALIZE_CONTACTS);
|
||||
// add(PERSISTENT_BLOBS);
|
||||
// add(SQLCIPHER);
|
||||
// add(SQLCIPHER_COMPLETE);
|
||||
// add(REMOVE_CACHE);
|
||||
// add(FULL_TEXT_SEARCH);
|
||||
// add(BAD_IMPORT_CLEANUP);
|
||||
// add(IMAGE_CACHE_CLEANUP);
|
||||
// add(WORKMANAGER_MIGRATION);
|
||||
// add(COLOR_MIGRATION);
|
||||
// add(UNIDENTIFIED_DELIVERY);
|
||||
// add(SIGNALING_KEY_DEPRECATION);
|
||||
// add(CONVERSATION_SEARCH);
|
||||
}};
|
||||
|
||||
private MasterSecret masterSecret;
|
||||
// private MasterSecret masterSecret;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
this.masterSecret = KeyCachingService.getMasterSecret(this);
|
||||
// this.masterSecret = KeyCachingService.getMasterSecret(this);
|
||||
|
||||
if (needsUpgradeTask()) {
|
||||
Log.i("DatabaseUpgradeActivity", "Upgrading...");
|
||||
@ -202,160 +190,160 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||
Context context = DatabaseUpgradeActivity.this.getApplicationContext();
|
||||
|
||||
Log.i("DatabaseUpgradeActivity", "Running background upgrade..");
|
||||
DatabaseFactory.getInstance(DatabaseUpgradeActivity.this)
|
||||
.onApplicationLevelUpgrade(context, masterSecret, params[0], this);
|
||||
// DatabaseFactory.getInstance(DatabaseUpgradeActivity.this)
|
||||
// .onApplicationLevelUpgrade(context, params[0], this);
|
||||
|
||||
if (params[0] < CURVE25519_VERSION) {
|
||||
IdentityKeyUtil.migrateIdentityKeys(context, masterSecret);
|
||||
}
|
||||
|
||||
if (params[0] < NO_V1_VERSION) {
|
||||
File v1sessions = new File(context.getFilesDir(), "sessions");
|
||||
|
||||
if (v1sessions.exists() && v1sessions.isDirectory()) {
|
||||
File[] contents = v1sessions.listFiles();
|
||||
|
||||
if (contents != null) {
|
||||
for (File session : contents) {
|
||||
session.delete();
|
||||
}
|
||||
}
|
||||
|
||||
v1sessions.delete();
|
||||
}
|
||||
}
|
||||
|
||||
if (params[0] < SIGNED_PREKEY_VERSION) {
|
||||
// ApplicationContext.getInstance(getApplicationContext())
|
||||
// .getJobManager()
|
||||
// .add(new CreateSignedPreKeyJob(context));
|
||||
}
|
||||
|
||||
if (params[0] < NO_DECRYPT_QUEUE_VERSION) {
|
||||
scheduleMessagesInPushDatabase(context);
|
||||
}
|
||||
|
||||
if (params[0] < PUSH_DECRYPT_SERIAL_ID_VERSION) {
|
||||
scheduleMessagesInPushDatabase(context);
|
||||
}
|
||||
|
||||
if (params[0] < MIGRATE_SESSION_PLAINTEXT) {
|
||||
// new TextSecureSessionStore(context, masterSecret).migrateSessions();
|
||||
// new TextSecurePreKeyStore(context, masterSecret).migrateRecords();
|
||||
|
||||
IdentityKeyUtil.migrateIdentityKeys(context, masterSecret);
|
||||
scheduleMessagesInPushDatabase(context);;
|
||||
}
|
||||
|
||||
if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
|
||||
schedulePendingIncomingParts(context);
|
||||
}
|
||||
|
||||
if (params[0] < REDPHONE_SUPPORT_VERSION) {
|
||||
ApplicationContext.getInstance(getApplicationContext())
|
||||
.getJobManager()
|
||||
.add(new RefreshAttributesJob());
|
||||
}
|
||||
|
||||
if (params[0] < SCREENSHOTS) {
|
||||
boolean screenSecurity = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, true);
|
||||
TextSecurePreferences.setScreenSecurityEnabled(getApplicationContext(), screenSecurity);
|
||||
}
|
||||
|
||||
if (params[0] < PERSISTENT_BLOBS) {
|
||||
File externalDir = context.getExternalFilesDir(null);
|
||||
|
||||
if (externalDir != null && externalDir.isDirectory() && externalDir.exists()) {
|
||||
for (File blob : externalDir.listFiles()) {
|
||||
if (blob.exists() && blob.isFile()) blob.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params[0] < INTERNALIZE_CONTACTS) {
|
||||
if (TextSecurePreferences.isPushRegistered(getApplicationContext())) {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(getApplicationContext(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if (params[0] < SQLCIPHER) {
|
||||
scheduleMessagesInPushDatabase(context);
|
||||
}
|
||||
|
||||
if (params[0] < SQLCIPHER_COMPLETE) {
|
||||
File file = context.getDatabasePath("messages.db");
|
||||
if (file != null && file.exists()) file.delete();
|
||||
}
|
||||
|
||||
if (params[0] < REMOVE_JOURNAL) {
|
||||
File file = context.getDatabasePath("messages.db-journal");
|
||||
if (file != null && file.exists()) file.delete();
|
||||
}
|
||||
|
||||
if (params[0] < REMOVE_CACHE) {
|
||||
try {
|
||||
FileUtils.deleteDirectoryContents(context.getCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (params[0] < IMAGE_CACHE_CLEANUP) {
|
||||
try {
|
||||
FileUtils.deleteDirectoryContents(context.getExternalCacheDir());
|
||||
GlideApp.get(context).clearDiskCache();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
// This migration became unnecessary after switching away from WorkManager
|
||||
// if (params[0] < WORKMANAGER_MIGRATION) {
|
||||
// Log.i(TAG, "Beginning migration of existing jobs to WorkManager");
|
||||
// if (params[0] < CURVE25519_VERSION) {
|
||||
// IdentityKeyUtil.migrateIdentityKeys(context, masterSecret);
|
||||
// }
|
||||
//
|
||||
// JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager();
|
||||
// PersistentStorage storage = new PersistentStorage(getApplicationContext(), "TextSecureJobs", new JavaJobSerializer());
|
||||
// if (params[0] < NO_V1_VERSION) {
|
||||
// File v1sessions = new File(context.getFilesDir(), "sessions");
|
||||
//
|
||||
// for (Job job : storage.getAllUnencrypted()) {
|
||||
// jobManager.add(job);
|
||||
// Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
|
||||
// if (v1sessions.exists() && v1sessions.isDirectory()) {
|
||||
// File[] contents = v1sessions.listFiles();
|
||||
//
|
||||
// if (contents != null) {
|
||||
// for (File session : contents) {
|
||||
// session.delete();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// v1sessions.delete();
|
||||
// }
|
||||
// }
|
||||
|
||||
if (params[0] < COLOR_MIGRATION) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors((name, color) -> {
|
||||
if (color != null) {
|
||||
try {
|
||||
return MaterialColor.fromSerialized(color);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
Log.w(TAG, "Encountered an unknown color during legacy color migration.", e);
|
||||
return ContactColorsLegacy.generateFor(name);
|
||||
}
|
||||
}
|
||||
return ContactColorsLegacy.generateFor(name);
|
||||
});
|
||||
Log.i(TAG, "Color migration took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
if (params[0] < UNIDENTIFIED_DELIVERY) {
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
Log.i(TAG, "MultiDevice: Disabling UD (will be re-enabled if possible after pending refresh).");
|
||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling UD attributes refresh.");
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RefreshAttributesJob());
|
||||
}
|
||||
|
||||
if (params[0] < SIGNALING_KEY_DEPRECATION) {
|
||||
Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RefreshAttributesJob());
|
||||
}
|
||||
//
|
||||
// if (params[0] < SIGNED_PREKEY_VERSION) {
|
||||
//// ApplicationContext.getInstance(getApplicationContext())
|
||||
//// .getJobManager()
|
||||
//// .add(new CreateSignedPreKeyJob(context));
|
||||
// }
|
||||
//
|
||||
// if (params[0] < NO_DECRYPT_QUEUE_VERSION) {
|
||||
// scheduleMessagesInPushDatabase(context);
|
||||
// }
|
||||
//
|
||||
// if (params[0] < PUSH_DECRYPT_SERIAL_ID_VERSION) {
|
||||
// scheduleMessagesInPushDatabase(context);
|
||||
// }
|
||||
//
|
||||
// if (params[0] < MIGRATE_SESSION_PLAINTEXT) {
|
||||
//// new TextSecureSessionStore(context, masterSecret).migrateSessions();
|
||||
//// new TextSecurePreKeyStore(context, masterSecret).migrateRecords();
|
||||
//
|
||||
// IdentityKeyUtil.migrateIdentityKeys(context, masterSecret);
|
||||
// scheduleMessagesInPushDatabase(context);;
|
||||
// }
|
||||
//
|
||||
// if (params[0] < MEDIA_DOWNLOAD_CONTROLS_VERSION) {
|
||||
// schedulePendingIncomingParts(context);
|
||||
// }
|
||||
//
|
||||
// if (params[0] < REDPHONE_SUPPORT_VERSION) {
|
||||
// ApplicationContext.getInstance(getApplicationContext())
|
||||
// .getJobManager()
|
||||
// .add(new RefreshAttributesJob());
|
||||
// }
|
||||
//
|
||||
// if (params[0] < SCREENSHOTS) {
|
||||
// boolean screenSecurity = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, true);
|
||||
// TextSecurePreferences.setScreenSecurityEnabled(getApplicationContext(), screenSecurity);
|
||||
// }
|
||||
//
|
||||
// if (params[0] < PERSISTENT_BLOBS) {
|
||||
// File externalDir = context.getExternalFilesDir(null);
|
||||
//
|
||||
// if (externalDir != null && externalDir.isDirectory() && externalDir.exists()) {
|
||||
// for (File blob : externalDir.listFiles()) {
|
||||
// if (blob.exists() && blob.isFile()) blob.delete();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (params[0] < INTERNALIZE_CONTACTS) {
|
||||
// if (TextSecurePreferences.isPushRegistered(getApplicationContext())) {
|
||||
// TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(getApplicationContext(), true);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (params[0] < SQLCIPHER) {
|
||||
// scheduleMessagesInPushDatabase(context);
|
||||
// }
|
||||
//
|
||||
// if (params[0] < SQLCIPHER_COMPLETE) {
|
||||
// File file = context.getDatabasePath("messages.db");
|
||||
// if (file != null && file.exists()) file.delete();
|
||||
// }
|
||||
//
|
||||
// if (params[0] < REMOVE_JOURNAL) {
|
||||
// File file = context.getDatabasePath("messages.db-journal");
|
||||
// if (file != null && file.exists()) file.delete();
|
||||
// }
|
||||
//
|
||||
// if (params[0] < REMOVE_CACHE) {
|
||||
// try {
|
||||
// FileUtils.deleteDirectoryContents(context.getCacheDir());
|
||||
// } catch (IOException e) {
|
||||
// Log.w(TAG, e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (params[0] < IMAGE_CACHE_CLEANUP) {
|
||||
// try {
|
||||
// FileUtils.deleteDirectoryContents(context.getExternalCacheDir());
|
||||
// GlideApp.get(context).clearDiskCache();
|
||||
// } catch (IOException e) {
|
||||
// Log.w(TAG, e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // This migration became unnecessary after switching away from WorkManager
|
||||
//// if (params[0] < WORKMANAGER_MIGRATION) {
|
||||
//// Log.i(TAG, "Beginning migration of existing jobs to WorkManager");
|
||||
////
|
||||
//// JobManager jobManager = ApplicationContext.getInstance(getApplicationContext()).getJobManager();
|
||||
//// PersistentStorage storage = new PersistentStorage(getApplicationContext(), "TextSecureJobs", new JavaJobSerializer());
|
||||
////
|
||||
//// for (Job job : storage.getAllUnencrypted()) {
|
||||
//// jobManager.add(job);
|
||||
//// Log.i(TAG, "Migrated job with class '" + job.getClass().getSimpleName() + "' to run on new JobManager.");
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// if (params[0] < COLOR_MIGRATION) {
|
||||
// long startTime = System.currentTimeMillis();
|
||||
// DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors((name, color) -> {
|
||||
// if (color != null) {
|
||||
// try {
|
||||
// return MaterialColor.fromSerialized(color);
|
||||
// } catch (MaterialColor.UnknownColorException e) {
|
||||
// Log.w(TAG, "Encountered an unknown color during legacy color migration.", e);
|
||||
// return ContactColorsLegacy.generateFor(name);
|
||||
// }
|
||||
// }
|
||||
// return ContactColorsLegacy.generateFor(name);
|
||||
// });
|
||||
// Log.i(TAG, "Color migration took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
// }
|
||||
//
|
||||
// if (params[0] < UNIDENTIFIED_DELIVERY) {
|
||||
// if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
// Log.i(TAG, "MultiDevice: Disabling UD (will be re-enabled if possible after pending refresh).");
|
||||
// TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
||||
// }
|
||||
//
|
||||
// Log.i(TAG, "Scheduling UD attributes refresh.");
|
||||
// ApplicationContext.getInstance(context)
|
||||
// .getJobManager()
|
||||
// .add(new RefreshAttributesJob());
|
||||
// }
|
||||
//
|
||||
// if (params[0] < SIGNALING_KEY_DEPRECATION) {
|
||||
// Log.i(TAG, "Scheduling a RefreshAttributesJob to remove the signaling key remotely.");
|
||||
// ApplicationContext.getInstance(context)
|
||||
// .getJobManager()
|
||||
// .add(new RefreshAttributesJob());
|
||||
// }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,282 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.view.View;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
import com.nineoldandroids.animation.ArgbEvaluator;
|
||||
|
||||
import org.thoughtcrime.securesms.IntroPagerAdapter.IntroPage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity implements TypingIndicatorIntroFragment.Controller, LinkPreviewsIntroFragment.Controller {
|
||||
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
|
||||
private static final String DISMISS_ACTION = "network.loki.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
|
||||
private static final int NOTIFICATION_ID = 1339;
|
||||
|
||||
private enum ExperienceUpgrade {
|
||||
TYPING_INDICATORS(432,
|
||||
new IntroPage(0xFF2090EA,
|
||||
TypingIndicatorIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_typing_indicators,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
null,
|
||||
true),
|
||||
LINK_PREVIEWS(449,
|
||||
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_link_previews,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
null,
|
||||
true);
|
||||
|
||||
private int version;
|
||||
private List<IntroPage> pages;
|
||||
private @StringRes int notificationTitle;
|
||||
private @StringRes int notificationText;
|
||||
private @StringRes int notificationBigText;
|
||||
private @Nullable Class nextIntent;
|
||||
private boolean handlesNavigation;
|
||||
|
||||
ExperienceUpgrade(int version,
|
||||
@NonNull List<IntroPage> pages,
|
||||
@StringRes int notificationTitle,
|
||||
@StringRes int notificationText,
|
||||
@StringRes int notificationBigText,
|
||||
@Nullable Class nextIntent,
|
||||
boolean handlesNavigation)
|
||||
{
|
||||
this.version = version;
|
||||
this.pages = pages;
|
||||
this.notificationTitle = notificationTitle;
|
||||
this.notificationText = notificationText;
|
||||
this.notificationBigText = notificationBigText;
|
||||
this.nextIntent = nextIntent;
|
||||
this.handlesNavigation = handlesNavigation;
|
||||
}
|
||||
|
||||
ExperienceUpgrade(int version,
|
||||
@NonNull IntroPage page,
|
||||
@StringRes int notificationTitle,
|
||||
@StringRes int notificationText,
|
||||
@StringRes int notificationBigText,
|
||||
@Nullable Class nextIntent,
|
||||
boolean handlesNavigation)
|
||||
{
|
||||
this(version, Collections.singletonList(page), notificationTitle, notificationText, notificationBigText, nextIntent, handlesNavigation);
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<IntroPage> getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public IntroPage getPage(int i) {
|
||||
return pages.get(i);
|
||||
}
|
||||
|
||||
public int getNotificationTitle() {
|
||||
return notificationTitle;
|
||||
}
|
||||
|
||||
public int getNotificationText() {
|
||||
return notificationText;
|
||||
}
|
||||
|
||||
public int getNotificationBigText() {
|
||||
return notificationBigText;
|
||||
}
|
||||
|
||||
public boolean handlesNavigation() {
|
||||
return handlesNavigation;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Optional<ExperienceUpgrade> upgrade = getExperienceUpgrade(this);
|
||||
if (!upgrade.isPresent()) {
|
||||
onContinue(upgrade);
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.experience_upgrade_activity);
|
||||
final ViewPager pager = ViewUtil.findById(this, R.id.pager);
|
||||
final FloatingActionButton fab = ViewUtil.findById(this, R.id.fab);
|
||||
|
||||
pager.setAdapter(new IntroPagerAdapter(getSupportFragmentManager(), upgrade.get().getPages()));
|
||||
|
||||
if (upgrade.get().handlesNavigation()) {
|
||||
fab.setVisibility(View.GONE);
|
||||
} else {
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
fab.setOnClickListener(v -> onContinue(upgrade));
|
||||
}
|
||||
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(upgrade.get().getPage(0).backgroundColor));
|
||||
ServiceUtil.getNotificationManager(this).cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
private void onContinue(Optional<ExperienceUpgrade> seenUpgrade) {
|
||||
ServiceUtil.getNotificationManager(this).cancel(NOTIFICATION_ID);
|
||||
int latestVersion = seenUpgrade.isPresent() ? seenUpgrade.get().getVersion()
|
||||
: Util.getCanonicalVersionCode();
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
|
||||
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
|
||||
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
|
||||
Intent nextIntent = new Intent(this, HomeActivity.class);
|
||||
intent.putExtra("next_intent", nextIntent);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
startActivity(getIntent().getParcelableExtra("next_intent"));
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
public static boolean isUpdate(Context context) {
|
||||
return getExperienceUpgrade(context).isPresent();
|
||||
}
|
||||
|
||||
public static Optional<ExperienceUpgrade> getExperienceUpgrade(Context context) {
|
||||
final int currentVersionCode = Util.getCanonicalVersionCode();
|
||||
final int lastSeenVersion = TextSecurePreferences.getLastExperienceVersionCode(context);
|
||||
Log.i(TAG, "getExperienceUpgrade(" + lastSeenVersion + ")");
|
||||
|
||||
if (lastSeenVersion >= currentVersionCode) {
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, currentVersionCode);
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
Optional<ExperienceUpgrade> eligibleUpgrade = Optional.absent();
|
||||
for (ExperienceUpgrade upgrade : ExperienceUpgrade.values()) {
|
||||
if (lastSeenVersion < upgrade.getVersion()) eligibleUpgrade = Optional.of(upgrade);
|
||||
}
|
||||
|
||||
return eligibleUpgrade;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTypingIndicatorsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkPreviewsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
|
||||
}
|
||||
|
||||
private final class OnPageChangeListener implements ViewPager.OnPageChangeListener {
|
||||
private final ArgbEvaluator evaluator = new ArgbEvaluator();
|
||||
private final ExperienceUpgrade upgrade;
|
||||
|
||||
public OnPageChangeListener(ExperienceUpgrade upgrade) {
|
||||
this.upgrade = upgrade;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
final int nextPosition = (position + 1) % upgrade.getPages().size();
|
||||
|
||||
final int color = (Integer)evaluator.evaluate(positionOffset,
|
||||
upgrade.getPage(position).backgroundColor,
|
||||
upgrade.getPage(nextPosition).backgroundColor);
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(color));
|
||||
}
|
||||
}
|
||||
|
||||
public static class AppUpgradeReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()) &&
|
||||
intent.getData().getSchemeSpecificPart().equals(context.getPackageName()))
|
||||
{
|
||||
if (TextSecurePreferences.getLastExperienceVersionCode(context) < 339 &&
|
||||
!TextSecurePreferences.isPasswordDisabled(context))
|
||||
{
|
||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
||||
.setContentTitle(context.getString(R.string.ExperienceUpgradeActivity_unlock_to_complete_update))
|
||||
.setContentText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update)))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
||||
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
Optional<ExperienceUpgrade> experienceUpgrade = getExperienceUpgrade(context);
|
||||
|
||||
if (!experienceUpgrade.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (experienceUpgrade.get().getVersion() == TextSecurePreferences.getExperienceDismissedVersionCode(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent targetIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||
Intent dismissIntent = new Intent(context, AppUpgradeReceiver.class);
|
||||
dismissIntent.setAction(DISMISS_ACTION);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(context.getResources().getColor(R.color.signal_primary))
|
||||
.setContentTitle(context.getString(experienceUpgrade.get().getNotificationTitle()))
|
||||
.setContentText(context.getString(experienceUpgrade.get().getNotificationText()))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(experienceUpgrade.get().getNotificationBigText())))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0,
|
||||
targetIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
|
||||
dismissIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build();
|
||||
ServiceUtil.getNotificationManager(context).notify(NOTIFICATION_ID, notification);
|
||||
} else if (DISMISS_ACTION.equals(intent.getAction())) {
|
||||
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCanonicalVersionCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IntroPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
public static class IntroPage {
|
||||
final int backgroundColor;
|
||||
final Fragment fragment;
|
||||
|
||||
public IntroPage(int backgroundColor, Fragment fragment) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.fragment = fragment;
|
||||
}
|
||||
}
|
||||
|
||||
private List<IntroPage> pages;
|
||||
|
||||
public IntroPagerAdapter(FragmentManager fm, List<IntroPage> pages) {
|
||||
super(fm);
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
IntroPage page = pages.get(i);
|
||||
return page.fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
}
|
@ -62,7 +62,6 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -86,18 +85,11 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
public static final String ADDRESS_EXTRA = "address";
|
||||
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private Recipient recipient;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.media_overview_activity);
|
||||
@ -109,12 +101,6 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
this.viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
@ -172,7 +158,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(MediaOverviewGalleryFragment.ADDRESS_EXTRA, recipient.getAddress().serialize());
|
||||
args.putSerializable(MediaOverviewGalleryFragment.LOCALE_EXTRA, dynamicLanguage.getCurrentLocale());
|
||||
args.putSerializable(MediaOverviewGalleryFragment.LOCALE_EXTRA, Locale.getDefault());
|
||||
|
||||
fragment.setArguments(args);
|
||||
|
||||
|
@ -68,12 +68,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
@ -95,8 +95,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
public static final String OUTGOING_EXTRA = "outgoing";
|
||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
private TextView caption;
|
||||
@ -120,8 +118,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
dynamicLanguage.onCreate(this);
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
setContentView(R.layout.media_preview_activity);
|
||||
@ -172,7 +168,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
CharSequence relativeTimeSpan;
|
||||
|
||||
if (mediaItem.date > 0) {
|
||||
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this,dynamicLanguage.getCurrentLocale(), mediaItem.date);
|
||||
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
} else {
|
||||
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
|
||||
}
|
||||
@ -189,7 +185,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
dynamicLanguage.onResume(this);
|
||||
initializeMedia();
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
@ -102,15 +101,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
private ListView recipientsList;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean running;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
super.onCreate(bundle, ready);
|
||||
@ -125,7 +117,6 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setTitle("Message Details");
|
||||
@ -211,7 +202,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
sentDate.setText("-");
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
Locale dateLocale = dynamicLanguage.getCurrentLocale();
|
||||
Locale dateLocale = Locale.getDefault();
|
||||
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
|
||||
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
|
||||
sentDate.setOnLongClickListener(v -> {
|
||||
@ -271,7 +262,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
toFrom.setVisibility(View.GONE);
|
||||
separator.setVisibility(View.GONE);
|
||||
}
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), recipient, null, false);
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
||||
}
|
||||
|
||||
|
@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
|
||||
|
||||
/**
|
||||
* Base Activity for changing/prompting local encryption passphrase.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public abstract class PassphraseActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = PassphraseActivity.class.getSimpleName();
|
||||
|
||||
private KeyCachingService keyCachingService;
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
protected void setMasterSecret(MasterSecret masterSecret) {
|
||||
this.masterSecret = masterSecret;
|
||||
Intent bindIntent = new Intent(this, KeyCachingService.class);
|
||||
startService(bindIntent);
|
||||
bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
protected abstract void cleanup();
|
||||
|
||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
keyCachingService = ((KeyCachingService.KeySetBinder)service).getService();
|
||||
keyCachingService.setMasterSecret(masterSecret);
|
||||
|
||||
PassphraseActivity.this.unbindService(PassphraseActivity.this.serviceConnection);
|
||||
|
||||
masterSecret = null;
|
||||
cleanup();
|
||||
|
||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||
if (nextIntent != null) {
|
||||
try {
|
||||
startActivity(nextIntent);
|
||||
} catch (java.lang.SecurityException e) {
|
||||
Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
|
||||
}
|
||||
}
|
||||
finish();
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
keyCachingService = null;
|
||||
}
|
||||
};
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Activity for changing a user's local encryption passphrase.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class PassphraseChangeActivity extends PassphraseActivity {
|
||||
|
||||
private DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private EditText originalPassphrase;
|
||||
private EditText newPassphrase;
|
||||
private EditText repeatPassphrase;
|
||||
private Button okButton;
|
||||
private Button cancelButton;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.change_passphrase_activity);
|
||||
|
||||
initializeResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
this.originalPassphrase = (EditText) findViewById(R.id.old_passphrase );
|
||||
this.newPassphrase = (EditText) findViewById(R.id.new_passphrase );
|
||||
this.repeatPassphrase = (EditText) findViewById(R.id.repeat_passphrase );
|
||||
|
||||
this.okButton = (Button ) findViewById(R.id.ok_button );
|
||||
this.cancelButton = (Button ) findViewById(R.id.cancel_button );
|
||||
|
||||
this.okButton.setOnClickListener(new OkButtonClickListener());
|
||||
this.cancelButton.setOnClickListener(new CancelButtonClickListener());
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(this)) {
|
||||
this.originalPassphrase.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.originalPassphrase.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyAndSavePassphrases() {
|
||||
Editable originalText = this.originalPassphrase.getText();
|
||||
Editable newText = this.newPassphrase.getText();
|
||||
Editable repeatText = this.repeatPassphrase.getText();
|
||||
|
||||
String original = (originalText == null ? "" : originalText.toString());
|
||||
String passphrase = (newText == null ? "" : newText.toString());
|
||||
String passphraseRepeat = (repeatText == null ? "" : repeatText.toString());
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(this)) {
|
||||
original = MasterSecretUtil.UNENCRYPTED_PASSPHRASE;
|
||||
}
|
||||
|
||||
if (!passphrase.equals(passphraseRepeat)) {
|
||||
this.newPassphrase.setText("");
|
||||
this.repeatPassphrase.setText("");
|
||||
this.newPassphrase.setError(getString(R.string.PassphraseChangeActivity_passphrases_dont_match_exclamation));
|
||||
this.newPassphrase.requestFocus();
|
||||
} else if (passphrase.equals("")) {
|
||||
this.newPassphrase.setError(getString(R.string.PassphraseChangeActivity_enter_new_passphrase_exclamation));
|
||||
this.newPassphrase.requestFocus();
|
||||
} else {
|
||||
new ChangePassphraseTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, original, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelButtonClickListener implements OnClickListener {
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private class OkButtonClickListener implements OnClickListener {
|
||||
public void onClick(View v) {
|
||||
verifyAndSavePassphrases();
|
||||
}
|
||||
}
|
||||
|
||||
private class ChangePassphraseTask extends AsyncTask<String, Void, MasterSecret> {
|
||||
private final Context context;
|
||||
|
||||
public ChangePassphraseTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
okButton.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MasterSecret doInBackground(String... params) {
|
||||
try {
|
||||
MasterSecret masterSecret = MasterSecretUtil.changeMasterSecretPassphrase(context, params[0], params[1]);
|
||||
TextSecurePreferences.setPasswordDisabled(context, false);
|
||||
|
||||
return masterSecret;
|
||||
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w(PassphraseChangeActivity.class.getSimpleName(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MasterSecret masterSecret) {
|
||||
okButton.setEnabled(true);
|
||||
|
||||
if (masterSecret != null) {
|
||||
setMasterSecret(masterSecret);
|
||||
} else {
|
||||
originalPassphrase.setText("");
|
||||
originalPassphrase.setError(getString(R.string.PassphraseChangeActivity_incorrect_old_passphrase_exclamation));
|
||||
originalPassphrase.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup() {
|
||||
this.originalPassphrase = null;
|
||||
this.newPassphrase = null;
|
||||
this.repeatPassphrase = null;
|
||||
|
||||
System.gc();
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Activity for creating a user's local encryption passphrase.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
|
||||
public PassphraseCreateActivity() { }
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.create_passphrase_activity);
|
||||
|
||||
initializeResources();
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
new SecretGenerator().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
}
|
||||
|
||||
private class SecretGenerator extends AsyncTask<String, Void, Void> {
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(String... params) {
|
||||
String passphrase = params[0];
|
||||
masterSecret = MasterSecretUtil.generateMasterSecret(PassphraseCreateActivity.this,
|
||||
passphrase);
|
||||
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
|
||||
IdentityKeyUtil.generateIdentityKeyPair(PassphraseCreateActivity.this);
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void param) {
|
||||
setMasterSecret(masterSecret);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup() {
|
||||
System.gc();
|
||||
}
|
||||
}
|
@ -18,66 +18,43 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.os.IBinder;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.BounceInterpolator;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.os.CancellationSignal;
|
||||
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Activity that prompts for a user's passphrase.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
//TODO Rename to ScreenLockActivity and refactor to Kotlin.
|
||||
public class PassphrasePromptActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = PassphrasePromptActivity.class.getSimpleName();
|
||||
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private View passphraseAuthContainer;
|
||||
private ImageView fingerprintPrompt;
|
||||
private Button lockScreenButton;
|
||||
|
||||
private EditText passphraseText;
|
||||
private ImageButton showButton;
|
||||
private ImageButton hideButton;
|
||||
private AnimatingToggle visibilityToggle;
|
||||
|
||||
private FingerprintManagerCompat fingerprintManager;
|
||||
@ -87,20 +64,34 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
private boolean authenticated;
|
||||
private boolean failure;
|
||||
|
||||
private KeyCachingService keyCachingService;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate()");
|
||||
dynamicLanguage.onCreate(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.prompt_passphrase_activity);
|
||||
initializeResources();
|
||||
|
||||
// Start and bind to the KeyCachingService instance.
|
||||
Intent bindIntent = new Intent(this, KeyCachingService.class);
|
||||
startService(bindIntent);
|
||||
bindService(bindIntent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
keyCachingService = ((KeyCachingService.KeySetBinder)service).getService();
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
keyCachingService = null;
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
setLockTypeVisibility();
|
||||
|
||||
@ -126,26 +117,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
// inflater.inflate(R.menu.log_submit, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_submit_debug_logs: handleLogSubmit(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultcode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultcode, data);
|
||||
@ -159,57 +130,26 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLogSubmit() {
|
||||
Intent intent = new Intent(this, LogSubmitActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void handlePassphrase() {
|
||||
try {
|
||||
Editable text = passphraseText.getText();
|
||||
String passphrase = (text == null ? "" : text.toString());
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, passphrase);
|
||||
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException ipe) {
|
||||
passphraseText.setText("");
|
||||
passphraseText.setError(
|
||||
getString(R.string.PassphrasePromptActivity_invalid_passphrase_exclamation));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthenticated() {
|
||||
try {
|
||||
authenticated = true;
|
||||
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
authenticated = true;
|
||||
//TODO Replace with a proper call.
|
||||
keyCachingService.setMasterSecret(new Object());
|
||||
|
||||
private void setPassphraseVisibility(boolean visibility) {
|
||||
int cursorPosition = passphraseText.getSelectionStart();
|
||||
if (visibility) {
|
||||
passphraseText.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
|
||||
} else {
|
||||
passphraseText.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
// Finish and proceed with the next intent.
|
||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||
if (nextIntent != null) {
|
||||
startActivity(nextIntent);
|
||||
// try {
|
||||
// startActivity(nextIntent);
|
||||
// } catch (java.lang.SecurityException e) {
|
||||
// Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
|
||||
// }
|
||||
}
|
||||
passphraseText.setSelection(cursorPosition);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
|
||||
ImageButton okButton = findViewById(R.id.ok_button);
|
||||
|
||||
showButton = findViewById(R.id.passphrase_visibility);
|
||||
hideButton = findViewById(R.id.passphrase_visibility_off);
|
||||
visibilityToggle = findViewById(R.id.button_toggle);
|
||||
passphraseText = findViewById(R.id.passphrase_edit);
|
||||
passphraseAuthContainer = findViewById(R.id.password_auth_container);
|
||||
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
|
||||
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
|
||||
fingerprintManager = FingerprintManagerCompat.from(this);
|
||||
@ -220,13 +160,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
hint.setSpan(new RelativeSizeSpan(0.9f), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
hint.setSpan(new TypefaceSpan("sans-serif"), 0, hint.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
|
||||
passphraseText.setHint(hint);
|
||||
okButton.setOnClickListener(new OkButtonClickListener());
|
||||
showButton.setOnClickListener(new ShowButtonOnClickListener());
|
||||
hideButton.setOnClickListener(new HideButtonOnClickListener());
|
||||
passphraseText.setOnEditorActionListener(new PassphraseActionListener());
|
||||
passphraseText.setImeActionLabel(getString(R.string.prompt_passphrase_activity__unlock), EditorInfo.IME_ACTION_DONE);
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
|
||||
|
||||
@ -235,8 +168,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
private void setLockTypeVisibility() {
|
||||
if (TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
passphraseAuthContainer.setVisibility(View.GONE);
|
||||
|
||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
fingerprintPrompt.setVisibility(View.VISIBLE);
|
||||
lockScreenButton.setVisibility(View.GONE);
|
||||
@ -245,7 +176,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
lockScreenButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
passphraseAuthContainer.setVisibility(View.VISIBLE);
|
||||
fingerprintPrompt.setVisibility(View.GONE);
|
||||
lockScreenButton.setVisibility(View.GONE);
|
||||
}
|
||||
@ -266,13 +196,10 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
Log.i(TAG, "Listening for fingerprints...");
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null);
|
||||
} else if (Build.VERSION.SDK_INT >= 21){
|
||||
} else {
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Session", "");
|
||||
startActivityForResult(intent, 1);
|
||||
} else {
|
||||
Log.w(TAG, "Not compatible...");
|
||||
handleAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,54 +209,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private class PassphraseActionListener implements TextView.OnEditorActionListener {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {
|
||||
if ((keyEvent == null && actionId == EditorInfo.IME_ACTION_DONE) ||
|
||||
(keyEvent != null && keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||
(actionId == EditorInfo.IME_NULL)))
|
||||
{
|
||||
handlePassphrase();
|
||||
return true;
|
||||
} else if (keyEvent != null && keyEvent.getAction() == KeyEvent.ACTION_UP &&
|
||||
actionId == EditorInfo.IME_NULL)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class OkButtonClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handlePassphrase();
|
||||
}
|
||||
}
|
||||
|
||||
private class ShowButtonOnClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
visibilityToggle.display(hideButton);
|
||||
setPassphraseVisibility(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class HideButtonOnClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
visibilityToggle.display(showButton);
|
||||
setPassphraseVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup() {
|
||||
this.passphraseText.setText("");
|
||||
System.gc();
|
||||
}
|
||||
|
||||
private class FingerprintListener extends FingerprintManagerCompat.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
@ -356,7 +235,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
Log.w(TAG, "onAuthenticatoinFailed()");
|
||||
FingerprintManagerCompat.AuthenticationCallback callback = this;
|
||||
|
||||
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
|
||||
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
|
||||
@ -380,6 +258,5 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
fingerprintPrompt.startAnimation(shake);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -5,34 +5,30 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.activities.LandingActivity;
|
||||
import org.thoughtcrime.securesms.loki.activities.SeedActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
//TODO AC: Rename to ScreenLockActionBarActivity.
|
||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity {
|
||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
||||
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_CREATE_PASSPHRASE = 1;
|
||||
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
||||
private static final int STATE_UPGRADE_DATABASE = 3;
|
||||
private static final int STATE_PROMPT_PUSH_REGISTRATION = 4;
|
||||
private static final int STATE_EXPERIENCE_UPGRADE = 5;
|
||||
private static final int STATE_WELCOME_SCREEN = 6;
|
||||
private static final int STATE_PROMPT_PASSPHRASE = 1; //TODO AC: Rename to STATE_SCREEN_LOCKED
|
||||
private static final int STATE_UPGRADE_DATABASE = 2; //TODO AC: Rename to STATE_MIGRATE_DATA
|
||||
private static final int STATE_WELCOME_SCREEN = 3;
|
||||
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
|
||||
@ -41,7 +37,9 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
Log.i(TAG, "onCreate(" + savedInstanceState + ")");
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
final boolean locked = KeyCachingService.isLocked(this) &&
|
||||
TextSecurePreferences.isScreenLockEnabled(this) &&
|
||||
TextSecurePreferences.getLocalNumber(this) != null;
|
||||
routeApplicationState(locked);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -118,61 +116,39 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
Log.i(TAG, "routeApplicationState(), state: " + state);
|
||||
|
||||
switch (state) {
|
||||
case STATE_CREATE_PASSPHRASE: return getCreatePassphraseIntent();
|
||||
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
||||
case STATE_UPGRADE_DATABASE: return getUpgradeDatabaseIntent();
|
||||
case STATE_WELCOME_SCREEN: return getWelcomeIntent();
|
||||
case STATE_PROMPT_PUSH_REGISTRATION: return getPushRegistrationIntent();
|
||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int getApplicationState(boolean locked) {
|
||||
if (!MasterSecretUtil.isPassphraseInitialized(this)) {
|
||||
return STATE_CREATE_PASSPHRASE;
|
||||
} else if (locked) {
|
||||
if (locked) {
|
||||
return STATE_PROMPT_PASSPHRASE;
|
||||
} else if (DatabaseUpgradeActivity.isUpdate(this)) {
|
||||
return STATE_UPGRADE_DATABASE;
|
||||
} else if (!TextSecurePreferences.hasSeenWelcomeScreen(this)) {
|
||||
return STATE_WELCOME_SCREEN;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_PROMPT_PUSH_REGISTRATION;
|
||||
}
|
||||
// else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
||||
// return STATE_EXPERIENCE_UPGRADE;
|
||||
// }
|
||||
else {
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getUpgradeDatabaseIntent() {
|
||||
return getRoutedIntent(DatabaseUpgradeActivity.class,
|
||||
TextSecurePreferences.hasPromptedPushRegistration(this)
|
||||
? getConversationListIntent()
|
||||
: getPushRegistrationIntent());
|
||||
}
|
||||
|
||||
private Intent getExperienceUpgradeIntent() {
|
||||
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
|
||||
return getRoutedIntent(DatabaseUpgradeActivity.class, getConversationListIntent());
|
||||
}
|
||||
|
||||
private Intent getWelcomeIntent() {
|
||||
return getRoutedIntent(LandingActivity.class, getPushRegistrationIntent());
|
||||
return getRoutedIntent(LandingActivity.class, getConversationListIntent());
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return getRoutedIntent(SeedActivity.class, getConversationListIntent());
|
||||
private Intent getConversationListIntent() {
|
||||
return new Intent(this, HomeActivity.class);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
@ -181,10 +157,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getConversationListIntent() {
|
||||
return new Intent(this, HomeActivity.class);
|
||||
}
|
||||
|
||||
private void initializeClearKeyReceiver() {
|
||||
Log.i(TAG, "initializeClearKeyReceiver()");
|
||||
this.clearKeyReceiver = new BroadcastReceiver() {
|
||||
|
@ -48,7 +48,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
@ -1,660 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.TypeEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.fingerprint.Fingerprint;
|
||||
import org.session.libsignal.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.session.libsignal.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.session.libsignal.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
/**
|
||||
* Activity for verifying identity keys.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, ScanListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
|
||||
|
||||
public static final String ADDRESS_EXTRA = "address";
|
||||
public static final String IDENTITY_EXTRA = "recipient_identity";
|
||||
public static final String VERIFIED_EXTRA = "verified_state";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
||||
private VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
Recipient recipient = Recipient.from(this, (Address)getIntent().getParcelableExtra(ADDRESS_EXTRA), true);
|
||||
recipient.addListener(this);
|
||||
|
||||
setActionBarNotificationBarColor(recipient.getColor());
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_ADDRESS, getIntent().getParcelableExtra(ADDRESS_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
extras.putString(VerifyDisplayFragment.REMOTE_NUMBER, recipient.getAddress().toPhoneString());
|
||||
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
|
||||
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
|
||||
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
|
||||
|
||||
scanFragment.setScanListener(this);
|
||||
displayFragment.setClickListener(this);
|
||||
|
||||
initFragment(android.R.id.content, displayFragment, dynamicLanguage.getCurrentLocale(), extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(() -> setActionBarNotificationBarColor(recipient.getColor()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQrDataFound(final String data) {
|
||||
Util.runOnMain(() -> {
|
||||
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
|
||||
getSupportFragmentManager().popBackStack();
|
||||
displayFragment.setScannedFingerprint(data);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
|
||||
.onAllGranted(() -> {
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
|
||||
R.anim.slide_from_bottom, R.anim.slide_to_top);
|
||||
|
||||
transaction.replace(android.R.id.content, scanFragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
||||
}
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements RecipientModifiedListener, CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
public static final String REMOTE_ADDRESS = "remote_address";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
public static final String REMOTE_IDENTITY = "remote_identity";
|
||||
public static final String LOCAL_IDENTITY = "local_identity";
|
||||
public static final String LOCAL_NUMBER = "local_number";
|
||||
public static final String VERIFIED_STATE = "verified_state";
|
||||
|
||||
private Recipient recipient;
|
||||
private String localNumber;
|
||||
private String remoteNumber;
|
||||
|
||||
private IdentityKey localIdentity;
|
||||
private IdentityKey remoteIdentity;
|
||||
|
||||
private Fingerprint fingerprint;
|
||||
|
||||
private View container;
|
||||
private View numbersContainer;
|
||||
private ImageView qrCode;
|
||||
private ImageView qrVerified;
|
||||
private TextView tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private SwitchCompat verified;
|
||||
|
||||
private TextView[] codes = new TextView[12];
|
||||
private boolean animateSuccessOnDraw = false;
|
||||
private boolean animateFailureOnDraw = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
|
||||
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
|
||||
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
|
||||
this.verified = ViewUtil.findById(container, R.id.verified_switch);
|
||||
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
|
||||
this.description = ViewUtil.findById(container, R.id.description);
|
||||
this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
|
||||
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
|
||||
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
|
||||
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
|
||||
this.codes[3] = ViewUtil.findById(container, R.id.code_fourth);
|
||||
this.codes[4] = ViewUtil.findById(container, R.id.code_fifth);
|
||||
this.codes[5] = ViewUtil.findById(container, R.id.code_sixth);
|
||||
this.codes[6] = ViewUtil.findById(container, R.id.code_seventh);
|
||||
this.codes[7] = ViewUtil.findById(container, R.id.code_eighth);
|
||||
this.codes[8] = ViewUtil.findById(container, R.id.code_ninth);
|
||||
this.codes[9] = ViewUtil.findById(container, R.id.code_tenth);
|
||||
this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh);
|
||||
this.codes[11] = ViewUtil.findById(container, R.id.code_twelth);
|
||||
|
||||
this.qrCode.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
|
||||
this.verified.setOnCheckedChangeListener(this);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Address address = getArguments().getParcelable(REMOTE_ADDRESS);
|
||||
IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY);
|
||||
IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY);
|
||||
|
||||
if (address == null) throw new AssertionError("Address required");
|
||||
if (localIdentityParcelable == null) throw new AssertionError("local identity required");
|
||||
if (remoteIdentityParcelable == null) throw new AssertionError("remote identity required");
|
||||
|
||||
this.localNumber = getArguments().getString(LOCAL_NUMBER);
|
||||
this.localIdentity = localIdentityParcelable.get();
|
||||
this.remoteNumber = getArguments().getString(REMOTE_NUMBER);
|
||||
this.recipient = Recipient.from(getActivity(), address, true);
|
||||
this.remoteIdentity = remoteIdentityParcelable.get();
|
||||
|
||||
this.recipient.addListener(this);
|
||||
|
||||
new AsyncTask<Void, Void, Fingerprint>() {
|
||||
@Override
|
||||
protected Fingerprint doInBackground(Void... params) {
|
||||
return new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
|
||||
remoteNumber, remoteIdentity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(() -> setRecipientText(recipient));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
setRecipientText(recipient);
|
||||
|
||||
if (fingerprint != null) {
|
||||
setFingerprintViews(fingerprint, false);
|
||||
}
|
||||
|
||||
if (animateSuccessOnDraw) {
|
||||
animateSuccessOnDraw = false;
|
||||
animateVerifiedSuccess();
|
||||
} else if (animateFailureOnDraw) {
|
||||
animateFailureOnDraw = false;
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
recipient.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view,
|
||||
ContextMenuInfo menuInfo)
|
||||
{
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
|
||||
if (fingerprint != null) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (fingerprint == null) return super.onContextItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_copy: handleCopyToClipboard(fingerprint, codes.length); return true;
|
||||
case R.id.menu_compare: handleCompareWithClipboard(fingerprint); return true;
|
||||
default: return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
if (fingerprint != null) {
|
||||
inflater.inflate(R.menu.verify_identity, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.verify_identity__share: handleShare(fingerprint, codes.length); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setScannedFingerprint(String scanned) {
|
||||
try {
|
||||
if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) {
|
||||
this.animateSuccessOnDraw = true;
|
||||
} else {
|
||||
this.animateFailureOnDraw = true;
|
||||
}
|
||||
} catch (FingerprintVersionMismatchException e) {
|
||||
Log.w(TAG, e);
|
||||
if (e.getOurVersion() < e.getTheirVersion()) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} catch (FingerprintParsingException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setClickListener(View.OnClickListener listener) {
|
||||
this.clickListener = listener;
|
||||
}
|
||||
|
||||
private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) {
|
||||
String[] segments = getSegments(fingerprint, segmentCount);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
result.append(segments[i]);
|
||||
|
||||
if (i != segments.length - 1) {
|
||||
if (((i+1) % 4) == 0) result.append('\n');
|
||||
else result.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
|
||||
Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount));
|
||||
}
|
||||
|
||||
private void handleCompareWithClipboard(Fingerprint fingerprint) {
|
||||
String clipboardData = Util.readTextFromClipboard(getActivity());
|
||||
|
||||
if (clipboardData == null) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String numericClipboardData = clipboardData.replaceAll("\\D", "");
|
||||
|
||||
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) {
|
||||
animateVerifiedSuccess();
|
||||
} else {
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) {
|
||||
String shareString =
|
||||
getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" +
|
||||
getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n";
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, shareString);
|
||||
intent.setType("text/plain");
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString())));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
|
||||
String[] segments = getSegments(fingerprint, codes.length);
|
||||
|
||||
for (int i=0;i<codes.length;i++) {
|
||||
if (animate) setCodeSegment(codes[i], segments[i]);
|
||||
else codes[i].setText(segments[i]);
|
||||
}
|
||||
|
||||
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
|
||||
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
|
||||
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
|
||||
|
||||
qrCode.setImageBitmap(qrCodeBitmap);
|
||||
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000);
|
||||
ViewUtil.fadeIn(tapLabel, 1000);
|
||||
} else {
|
||||
qrCode.setVisibility(View.VISIBLE);
|
||||
tapLabel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCodeSegment(final TextView codeView, String segment) {
|
||||
ValueAnimator valueAnimator = new ValueAnimator();
|
||||
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
|
||||
|
||||
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
int value = (int) animation.getAnimatedValue();
|
||||
codeView.setText(String.format("%05d", value));
|
||||
}
|
||||
});
|
||||
|
||||
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
|
||||
return Math.round(startValue + (endValue - startValue) * fraction);
|
||||
}
|
||||
});
|
||||
|
||||
valueAnimator.setDuration(1000);
|
||||
valueAnimator.start();
|
||||
}
|
||||
|
||||
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
|
||||
String[] segments = new String[segmentCount];
|
||||
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
|
||||
int partSize = digits.length() / segmentCount;
|
||||
|
||||
for (int i=0;i<segmentCount;i++) {
|
||||
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Bitmap check = BitmapFactory.decodeResource(getResources(), id);
|
||||
float offset = (width - check.getWidth()) / 2;
|
||||
|
||||
canvas.drawBitmap(check, offset, offset, null);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void animateVerifiedSuccess() {
|
||||
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
|
||||
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp);
|
||||
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
private void animateVerifiedFailure() {
|
||||
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
|
||||
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp);
|
||||
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
private void animateVerified() {
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new OvershootInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
qrVerified.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0, 1, 0,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
|
||||
scaleAnimation.setInterpolator(new AnticipateInterpolator());
|
||||
scaleAnimation.setDuration(500);
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
new AsyncTask<Recipient, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Recipient... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + params[0].getAddress());
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.saveIdentity(params[0].getAddress(),
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED, false,
|
||||
System.currentTimeMillis(), true);
|
||||
} else {
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.setVerified(params[0].getAddress(),
|
||||
remoteIdentity,
|
||||
VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static class VerifyScanFragment extends Fragment {
|
||||
|
||||
private View container;
|
||||
private CameraView cameraView;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = ViewUtil.findById(container, R.id.scanner);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
this.scanningThread = new ScanningThread();
|
||||
this.scanningThread.setScanListener(scanListener);
|
||||
this.scanningThread.setCharacterSet("ISO-8859-1");
|
||||
this.cameraView.onResume();
|
||||
this.cameraView.setPreviewCallback(scanningThread);
|
||||
this.scanningThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
this.cameraView.onPause();
|
||||
this.scanningThread.stopScanning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
this.cameraView.onPause();
|
||||
this.cameraView.onResume();
|
||||
this.cameraView.setPreviewCallback(scanningThread);
|
||||
}
|
||||
|
||||
public void setScanListener(ScanListener listener) {
|
||||
if (this.scanningThread != null) scanningThread.setScanListener(listener);
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -2,49 +2,70 @@ package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.database.dto.DatabaseAttachmentDTO
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider {
|
||||
override fun getAttachment(uniqueID: String): DatabaseAttachmentDTO? {
|
||||
|
||||
override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val uniqueID = uniqueID.toLongOrNull() ?: return null
|
||||
val attachmentID = AttachmentId(0, uniqueID)
|
||||
val databaseAttachment = attachmentDatabase.getAttachment(attachmentID) ?: return null
|
||||
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
|
||||
return databaseAttachment.toAttachmentStream(context)
|
||||
}
|
||||
|
||||
return databaseAttachment.toDTO()
|
||||
override fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer? {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
|
||||
return databaseAttachment.toAttachmentPointer()
|
||||
}
|
||||
|
||||
override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun uploadAttachment(attachmentId: Long) {
|
||||
val attachmentUploadJob = AttachmentUploadJob(AttachmentId(attachmentId, 0), null)
|
||||
attachmentUploadJob.onRun()
|
||||
}
|
||||
|
||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
return smsDatabase.isOutgoingMessage(timestamp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Extension to DatabaseAttachment class
|
||||
fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
|
||||
return SessionServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.toDTO(): DatabaseAttachmentDTO {
|
||||
var databaseAttachmentDTO = DatabaseAttachmentDTO()
|
||||
databaseAttachmentDTO.contentType = this.contentType
|
||||
databaseAttachmentDTO.fileName = this.fileName
|
||||
databaseAttachmentDTO.caption = this.caption
|
||||
fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttachmentStream {
|
||||
val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!)
|
||||
var attachmentStream = SessionServiceAttachmentStream(stream, this.contentType, this.size, Optional.fromNullable(this.fileName), this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption), null)
|
||||
attachmentStream.attachmentId = this.attachmentId.rowId
|
||||
attachmentStream.isAudio = MediaUtil.isAudio(this)
|
||||
attachmentStream.isGif = MediaUtil.isGif(this)
|
||||
attachmentStream.isVideo = MediaUtil.isVideo(this)
|
||||
attachmentStream.isImage = MediaUtil.isImage(this)
|
||||
|
||||
databaseAttachmentDTO.size = this.size.toInt()
|
||||
databaseAttachmentDTO.key = ByteString.copyFrom(this.key?.toByteArray())
|
||||
databaseAttachmentDTO.digest = ByteString.copyFrom(this.digest)
|
||||
databaseAttachmentDTO.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
|
||||
attachmentStream.key = ByteString.copyFrom(this.key?.toByteArray())
|
||||
attachmentStream.digest = this.digest
|
||||
//attachmentStream.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
|
||||
|
||||
databaseAttachmentDTO.url = this.url
|
||||
attachmentStream.url = this.url
|
||||
|
||||
if (this.shouldHaveImageSize()) {
|
||||
databaseAttachmentDTO.shouldHaveImageSize = true
|
||||
databaseAttachmentDTO.width = this.width
|
||||
databaseAttachmentDTO.height = this.height
|
||||
}
|
||||
|
||||
return databaseAttachmentDTO
|
||||
return attachmentStream
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.shouldHaveImageSize(): Boolean {
|
||||
|
@ -1,66 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.identity;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class UntrustedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
|
||||
|
||||
private final List<IdentityRecord> untrustedRecords;
|
||||
private final ResendListener resendListener;
|
||||
|
||||
public UntrustedSendDialog(@NonNull Context context,
|
||||
@NonNull String message,
|
||||
@NonNull List<IdentityRecord> untrustedRecords,
|
||||
@NonNull ResendListener resendListener)
|
||||
{
|
||||
super(context);
|
||||
this.untrustedRecords = untrustedRecords;
|
||||
this.resendListener = resendListener;
|
||||
|
||||
setTitle(R.string.UntrustedSendDialog_send_message);
|
||||
setIconAttribute(R.attr.dialog_alert_icon);
|
||||
setMessage(message);
|
||||
setPositiveButton(R.string.UntrustedSendDialog_send, this);
|
||||
setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setApproval(identityRecord.getAddress(), true);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
resendListener.onResendMessage();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public interface ResendListener {
|
||||
public void onResendMessage();
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.identity;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.util.AttributeSet;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UnverifiedBannerView extends LinearLayout {
|
||||
|
||||
private static final String TAG = UnverifiedBannerView.class.getSimpleName();
|
||||
|
||||
private View container;
|
||||
private TextView text;
|
||||
private ImageView closeButton;
|
||||
|
||||
public UnverifiedBannerView(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
|
||||
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.unverified_banner_view, this, true);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.text = ViewUtil.findById(this, R.id.unverified_text);
|
||||
this.closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
}
|
||||
|
||||
public void display(@NonNull final String text,
|
||||
@NonNull final List<IdentityRecord> unverifiedIdentities,
|
||||
@NonNull final ClickListener clickListener,
|
||||
@NonNull final DismissListener dismissListener)
|
||||
{
|
||||
this.text.setText(text);
|
||||
setVisibility(View.VISIBLE);
|
||||
|
||||
this.container.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.i(TAG, "onClick()");
|
||||
clickListener.onClicked(unverifiedIdentities);
|
||||
}
|
||||
});
|
||||
|
||||
this.closeButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
hide();
|
||||
dismissListener.onDismissed(unverifiedIdentities);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public interface DismissListener {
|
||||
public void onDismissed(List<IdentityRecord> unverifiedIdentities);
|
||||
}
|
||||
|
||||
public interface ClickListener {
|
||||
public void onClicked(List<IdentityRecord> unverifiedIdentities);
|
||||
}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.identity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
|
||||
|
||||
private final List<IdentityRecord> untrustedRecords;
|
||||
private final ResendListener resendListener;
|
||||
|
||||
public UnverifiedSendDialog(@NonNull Context context,
|
||||
@NonNull String message,
|
||||
@NonNull List<IdentityRecord> untrustedRecords,
|
||||
@NonNull ResendListener resendListener)
|
||||
{
|
||||
super(context);
|
||||
this.untrustedRecords = untrustedRecords;
|
||||
this.resendListener = resendListener;
|
||||
|
||||
setTitle(R.string.UnverifiedSendDialog_send_message);
|
||||
setIconAttribute(R.attr.dialog_alert_icon);
|
||||
setMessage(message);
|
||||
setPositiveButton(R.string.UnverifiedSendDialog_send, this);
|
||||
setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (IdentityRecord identityRecord : untrustedRecords) {
|
||||
identityDatabase.setVerified(identityRecord.getAddress(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
resendListener.onResendMessage();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public interface ResendListener {
|
||||
public void onResendMessage();
|
||||
}
|
||||
}
|
@ -98,7 +98,6 @@ import org.thoughtcrime.securesms.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
@ -113,29 +112,21 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
@ -201,7 +192,6 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
@ -219,7 +209,6 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@ -230,7 +219,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
|
||||
/**
|
||||
@ -291,7 +279,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private Button unblockButton;
|
||||
private Button makeDefaultSmsButton;
|
||||
private InputAwareLayout container;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<GroupShareProfileView> groupShareProfileView;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private MentionTextWatcher mentionTextWatcher;
|
||||
@ -328,8 +315,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private int collapsedKeyboardHeight = Integer.MAX_VALUE;
|
||||
private int keyboardHeight = 0;
|
||||
|
||||
private final IdentityRecordList identityRecords = new IdentityRecordList();
|
||||
|
||||
// Message status bar
|
||||
private ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
|
||||
private String messageStatus = null;
|
||||
@ -506,7 +491,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
EventBus.getDefault().register(this);
|
||||
initializeEnabledCheck();
|
||||
initializeMmsEnabledCheck();
|
||||
initializeIdentityRecords();
|
||||
composeText.setTransport();
|
||||
|
||||
updateTitleTextView(recipient);
|
||||
@ -514,7 +498,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
updateSubtitleTextView();
|
||||
updateInputUI(recipient);
|
||||
setGroupShareProfileReminder(recipient);
|
||||
calculateCharactersRemaining();
|
||||
|
||||
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadId);
|
||||
markThreadAsRead();
|
||||
@ -734,9 +717,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
inflater.inflate(R.menu.conversation, menu);
|
||||
|
||||
if (isSingleConversation()) {
|
||||
inflater.inflate(R.menu.conversation_secure, menu);
|
||||
}
|
||||
// if (isSingleConversation()) {
|
||||
// inflater.inflate(R.menu.conversation_secure, menu);
|
||||
// }
|
||||
|
||||
if (recipient != null && recipient.isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
|
||||
else inflater.inflate(R.menu.conversation_unmuted, menu);
|
||||
@ -827,7 +810,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
case R.id.menu_add_shortcut: handleAddShortcut(); return true;
|
||||
case R.id.menu_search: handleSearch(); return true;
|
||||
// case R.id.menu_add_to_contacts: handleAddToContacts(); return true;
|
||||
case R.id.menu_reset_secure_session: handleResetSecureSession(); return true;
|
||||
// case R.id.menu_reset_secure_session: handleResetSecureSession(); return true;
|
||||
// case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
|
||||
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
||||
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
||||
@ -1179,52 +1162,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnverifiedRecipients() {
|
||||
List<Recipient> unverifiedRecipients = identityRecords.getUnverifiedRecipients(this);
|
||||
List<IdentityRecord> unverifiedRecords = identityRecords.getUnverifiedRecords();
|
||||
String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients);
|
||||
|
||||
if (message == null) return;
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
new UnverifiedSendDialog(this, message, unverifiedRecords, () -> {
|
||||
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
}).show();
|
||||
}
|
||||
|
||||
private void handleUntrustedRecipients() {
|
||||
List<Recipient> untrustedRecipients = identityRecords.getUntrustedRecipients(this);
|
||||
List<IdentityRecord> untrustedRecords = identityRecords.getUntrustedRecords();
|
||||
String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
|
||||
|
||||
if (untrustedMessage == null) return;
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, () -> {
|
||||
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
}).show();
|
||||
}
|
||||
|
||||
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
|
||||
Log.i(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")");
|
||||
if (isSecurityInitialized && isSecureText == true && isDefaultSms == this.isDefaultSms) {
|
||||
@ -1250,7 +1187,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
*/
|
||||
|
||||
calculateCharactersRemaining();
|
||||
supportInvalidateOptionsMenu();
|
||||
updateInputUI(recipient);
|
||||
}
|
||||
@ -1430,62 +1366,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> initializeIdentityRecords() {
|
||||
final SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Recipient, Void, Pair<IdentityRecordList, String>>() {
|
||||
@Override
|
||||
protected @NonNull Pair<IdentityRecordList, String> doInBackground(Recipient... params) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
|
||||
IdentityRecordList identityRecordList = new IdentityRecordList();
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
||||
if (params[0].isGroupRecipient()) {
|
||||
recipients.addAll(DatabaseFactory.getGroupDatabase(ConversationActivity.this)
|
||||
.getGroupMembers(params[0].getAddress().toGroupString(), false));
|
||||
} else {
|
||||
recipients.add(params[0]);
|
||||
}
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
Log.i(TAG, "Loading identity for: " + recipient.getAddress());
|
||||
identityRecordList.add(identityDatabase.getIdentity(recipient.getAddress()));
|
||||
}
|
||||
|
||||
String message = null;
|
||||
|
||||
if (identityRecordList.isUnverified()) {
|
||||
message = IdentityUtil.getUnverifiedBannerDescription(ConversationActivity.this, identityRecordList.getUnverifiedRecipients(ConversationActivity.this));
|
||||
}
|
||||
|
||||
return new Pair<>(identityRecordList, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@NonNull Pair<IdentityRecordList, String> result) {
|
||||
Log.i(TAG, "Got identity records: " + result.first.isUnverified());
|
||||
identityRecords.replaceWith(result.first);
|
||||
|
||||
if (result.second != null) {
|
||||
Log.d(TAG, "Replacing banner...");
|
||||
unverifiedBannerView.get().display(result.second, result.first.getUnverifiedRecords(),
|
||||
new UnverifiedClickedListener(),
|
||||
new UnverifiedDismissedListener());
|
||||
} else if (unverifiedBannerView.resolved()) {
|
||||
Log.d(TAG, "Clearing banner...");
|
||||
unverifiedBannerView.get().hide();
|
||||
}
|
||||
|
||||
// titleView.setVerified(isSecureText && identityRecords.isVerified());
|
||||
|
||||
future.set(true);
|
||||
}
|
||||
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
profilePictureView = findViewById(R.id.profilePictureView);
|
||||
titleTextView = findViewById(R.id.titleTextView);
|
||||
@ -1498,7 +1378,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
unblockButton = ViewUtil.findById(this, R.id.unblock_button);
|
||||
makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
|
||||
container = ViewUtil.findById(this, R.id.layout_container);
|
||||
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
|
||||
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
|
||||
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
|
||||
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
|
||||
@ -1700,11 +1579,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onIdentityRecordUpdate(final IdentityRecord event) {
|
||||
initializeIdentityRecords();
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
|
||||
public void onStickerPackInstalled(final StickerPackInstallEvent event) {
|
||||
if (!TextSecurePreferences.hasSeenStickerIntroTooltip(this)) return;
|
||||
@ -1731,7 +1605,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
initializeSecurity(true, isDefaultSms);
|
||||
calculateCharactersRemaining();
|
||||
}
|
||||
};
|
||||
|
||||
@ -1931,25 +1804,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCharactersRemaining() {
|
||||
/*
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
|
||||
"%d/%d (%d)",
|
||||
characterState.charactersRemaining,
|
||||
characterState.maxTotalMessageSize,
|
||||
characterState.messagesSpent));
|
||||
charactersLeft.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
charactersLeft.setVisibility(View.GONE);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard, boolean stickersAvailable) {
|
||||
boolean isSystemEmojiPreferred = TextSecurePreferences.isSystemEmojiPreferred(this);
|
||||
|
||||
@ -2127,9 +1981,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
||||
needsSplit;
|
||||
|
||||
if (identityRecords.isUnverified()) {
|
||||
handleUnverifiedRecipients();
|
||||
} else if (isMediaMessage) {
|
||||
if (isMediaMessage) {
|
||||
sendMediaMessage(expiresIn, subscriptionId, initiating);
|
||||
} else {
|
||||
sendTextMessage(expiresIn, subscriptionId, initiating);
|
||||
@ -2552,7 +2404,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
calculateCharactersRemaining();
|
||||
|
||||
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
|
||||
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
|
||||
@ -2719,67 +2570,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
updateLinkPreviewState();
|
||||
}
|
||||
|
||||
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
|
||||
@Override
|
||||
public void onDismissed(final List<IdentityRecord> unverifiedIdentities) {
|
||||
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (IdentityRecord identityRecord : unverifiedIdentities) {
|
||||
identityDatabase.setVerified(identityRecord.getAddress(),
|
||||
identityRecord.getIdentityKey(),
|
||||
VerifiedStatus.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
initializeIdentityRecords();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
private class UnverifiedClickedListener implements UnverifiedBannerView.ClickListener {
|
||||
@Override
|
||||
public void onClicked(final List<IdentityRecord> unverifiedIdentities) {
|
||||
Log.i(TAG, "onClicked: " + unverifiedIdentities.size());
|
||||
if (unverifiedIdentities.size() == 1) {
|
||||
Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class);
|
||||
intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, unverifiedIdentities.get(0).getAddress());
|
||||
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(0).getIdentityKey()));
|
||||
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
|
||||
|
||||
startActivity(intent);
|
||||
} else {
|
||||
String[] unverifiedNames = new String[unverifiedIdentities.size()];
|
||||
|
||||
for (int i=0;i<unverifiedIdentities.size();i++) {
|
||||
unverifiedNames[i] = Recipient.from(ConversationActivity.this, unverifiedIdentities.get(i).getAddress(), false).toShortString();
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ConversationActivity.this);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle("No longer verified");
|
||||
builder.setItems(unverifiedNames, (dialog, which) -> {
|
||||
Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class);
|
||||
intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, unverifiedIdentities.get(which).getAddress());
|
||||
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(which).getIdentityKey()));
|
||||
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
|
||||
|
||||
startActivity(intent);
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
|
||||
|
||||
private final String serialized;
|
||||
|
@ -48,8 +48,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
@ -66,8 +64,6 @@ import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
|
@ -53,14 +53,15 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemAlertView;
|
||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
@ -73,7 +74,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
@ -86,8 +86,8 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||
import org.thoughtcrime.securesms.loki.views.TapJackingProofLinearLayout;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
@ -108,9 +108,6 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@ -128,7 +125,7 @@ import network.loki.messenger.R;
|
||||
*
|
||||
*/
|
||||
|
||||
public class ConversationItem extends TapJackingProofLinearLayout
|
||||
public class ConversationItem extends LinearLayout
|
||||
implements RecipientModifiedListener, BindableConversationItem
|
||||
{
|
||||
private static final String TAG = ConversationItem.class.getSimpleName();
|
||||
@ -1036,16 +1033,6 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
|
||||
/// Event handlers
|
||||
|
||||
private void handleApproveIdentity() {
|
||||
List<IdentityKeyMismatch> mismatches = messageRecord.getIdentityKeyMismatches();
|
||||
|
||||
if (mismatches.size() != 1) {
|
||||
throw new AssertionError("Identity mismatch count: " + mismatches.size());
|
||||
}
|
||||
|
||||
new ConfirmIdentityDialog(context, messageRecord, mismatches.get(0)).show();
|
||||
}
|
||||
|
||||
private Spannable getLongMessageSpan(@NonNull MessageRecord messageRecord) {
|
||||
String message;
|
||||
Runnable action;
|
||||
@ -1238,7 +1225,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
intent.putExtra(MessageDetailsActivity.ADDRESS_EXTRA, conversationRecipient.getAddress());
|
||||
context.startActivity(intent);
|
||||
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
|
||||
handleApproveIdentity();
|
||||
// handleApproveIdentity();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
handleMessageApproval();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
@ -16,12 +15,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -29,17 +23,15 @@ import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
//TODO Remove this class.
|
||||
public class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientModifiedListener, BindableConversationItem
|
||||
{
|
||||
@ -256,24 +248,24 @@ public class ConversationUpdateItem extends LinearLayout
|
||||
|
||||
final Recipient sender = ConversationUpdateItem.this.sender;
|
||||
|
||||
IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
@Override
|
||||
public void onSuccess(Optional<IdentityRecord> result) {
|
||||
if (result.isPresent()) {
|
||||
Intent intent = new Intent(getContext(), VerifyIdentityActivity.class);
|
||||
intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, sender.getAddress());
|
||||
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey()));
|
||||
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
// IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
// @Override
|
||||
// public void onSuccess(Optional<IdentityRecord> result) {
|
||||
// if (result.isPresent()) {
|
||||
// Intent intent = new Intent(getContext(), VerifyIdentityActivity.class);
|
||||
// intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, sender.getAddress());
|
||||
// intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey()));
|
||||
// intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
//
|
||||
// getContext().startActivity(intent);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onFailure(ExecutionException e) {
|
||||
// Log.w(TAG, e);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* This class is used to asymmetricly encrypt local data. This is used in the case
|
||||
* where TextSecure receives an SMS, but the user's local encryption passphrase is
|
||||
* not cached (either because of a timeout, or because it hasn't yet been entered).
|
||||
*
|
||||
* In this case, we have access to the public key of a local keypair. We encrypt
|
||||
* the message with this, and put it into the DB. When the user enters their passphrase,
|
||||
* we can get access to the private key of the local keypair, decrypt the message, and
|
||||
* replace it into the DB with symmetric encryption.
|
||||
*
|
||||
* The encryption protocol is as follows:
|
||||
*
|
||||
* 1) Generate an ephemeral keypair.
|
||||
* 2) Do ECDH with the public key of the local durable keypair.
|
||||
* 3) Do KMF with the ECDH result to obtain a master secret.
|
||||
* 4) Encrypt the message with that master secret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class AsymmetricMasterCipher {
|
||||
|
||||
private final AsymmetricMasterSecret asymmetricMasterSecret;
|
||||
|
||||
public AsymmetricMasterCipher(AsymmetricMasterSecret asymmetricMasterSecret) {
|
||||
this.asymmetricMasterSecret = asymmetricMasterSecret;
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
|
||||
ECKeyPair ourKeyPair = Curve.generateKeyPair();
|
||||
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
|
||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body);
|
||||
|
||||
PublicKey ourPublicKey = new PublicKey(31337, ourKeyPair.getPublicKey());
|
||||
byte[] publicKeyBytes = ourPublicKey.serialize();
|
||||
|
||||
return Util.combine(publicKeyBytes, encryptedBodyBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(byte[] combined) throws IOException, InvalidMessageException {
|
||||
try {
|
||||
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
|
||||
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
|
||||
|
||||
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey();
|
||||
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
|
||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||
|
||||
return masterCipher.decryptBytes(parts[1]);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws IOException, InvalidMessageException {
|
||||
byte[] combined = Base64.decode(body);
|
||||
return new String(decryptBytes(combined));
|
||||
}
|
||||
|
||||
public String encryptBody(String body) {
|
||||
return Base64.encodeBytes(encryptBytes(body.getBytes()));
|
||||
}
|
||||
|
||||
private MasterCipher getMasterCipherForSecret(byte[] secretBytes) {
|
||||
SecretKeySpec cipherKey = deriveCipherKey(secretBytes);
|
||||
SecretKeySpec macKey = deriveMacKey(secretBytes);
|
||||
MasterSecret masterSecret = new MasterSecret(cipherKey, macKey);
|
||||
|
||||
return new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacKey(byte[] secretBytes) {
|
||||
byte[] digestedBytes = getDigestedBytes(secretBytes, 1);
|
||||
byte[] macKeyBytes = new byte[20];
|
||||
|
||||
System.arraycopy(digestedBytes, 0, macKeyBytes, 0, macKeyBytes.length);
|
||||
return new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherKey(byte[] secretBytes) {
|
||||
byte[] digestedBytes = getDigestedBytes(secretBytes, 0);
|
||||
byte[] cipherKeyBytes = new byte[16];
|
||||
|
||||
System.arraycopy(digestedBytes, 0, cipherKeyBytes, 0, cipherKeyBytes.length);
|
||||
return new SecretKeySpec(cipherKeyBytes, "AES");
|
||||
}
|
||||
|
||||
private byte[] getDigestedBytes(byte[] secretBytes, int iteration) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secretBytes, "HmacSHA256"));
|
||||
return mac.doFinal(Conversions.intToByteArray(iteration));
|
||||
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
@ -40,15 +41,13 @@ import java.util.List;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class IdentityKeyUtil {
|
||||
|
||||
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||
|
||||
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
|
||||
private static final String IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_private_curve25519";
|
||||
|
||||
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";
|
||||
@ -56,7 +55,7 @@ public class IdentityKeyUtil {
|
||||
public static final String LOKI_SEED = "loki_seed";
|
||||
|
||||
public static boolean hasIdentityKey(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
|
||||
return
|
||||
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
||||
@ -87,63 +86,46 @@ public class IdentityKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void generateIdentityKeyPair(Context context) {
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();;
|
||||
IdentityKey publicKey = new IdentityKey(keyPair.getPublicKey());
|
||||
public static void generateIdentityKeyPair(@NonNull Context context) {
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
ECPublicKey publicKey = keyPair.getPublicKey();
|
||||
ECPrivateKey privateKey = keyPair.getPrivateKey();
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
|
||||
}
|
||||
|
||||
public static void migrateIdentityKeys(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret)
|
||||
{
|
||||
if (!hasIdentityKey(context)) {
|
||||
if (hasLegacyIdentityKeys(context)) {
|
||||
IdentityKeyPair legacyPair = getLegacyIdentityKeyPair(context, masterSecret);
|
||||
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(legacyPair.getPublicKey().serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(legacyPair.getPrivateKey().serialize()));
|
||||
|
||||
delete(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF);
|
||||
delete(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF);
|
||||
} else {
|
||||
generateIdentityKeyPair(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BackupProtos.SharedPreference> getBackupRecords(@NonNull Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
final String prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME;
|
||||
SharedPreferences preferences = context.getSharedPreferences(prefName, 0);
|
||||
|
||||
LinkedList<BackupProtos.SharedPreference> prefList = new LinkedList<>();
|
||||
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setFile(prefName)
|
||||
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build());
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setFile(prefName)
|
||||
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build());
|
||||
if (preferences.contains(ED25519_PUBLIC_KEY)) {
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setFile(prefName)
|
||||
.setKey(ED25519_PUBLIC_KEY)
|
||||
.setValue(preferences.getString(ED25519_PUBLIC_KEY, null))
|
||||
.build());
|
||||
}
|
||||
if (preferences.contains(ED25519_SECRET_KEY)) {
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setFile(prefName)
|
||||
.setKey(ED25519_SECRET_KEY)
|
||||
.setValue(preferences.getString(ED25519_SECRET_KEY, null))
|
||||
.build());
|
||||
}
|
||||
prefList.add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setFile(prefName)
|
||||
.setKey(LOKI_SEED)
|
||||
.setValue(preferences.getString(LOKI_SEED, null))
|
||||
.build());
|
||||
@ -151,34 +133,13 @@ public class IdentityKeyUtil {
|
||||
return prefList;
|
||||
}
|
||||
|
||||
private static boolean hasLegacyIdentityKeys(Context context) {
|
||||
return
|
||||
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
|
||||
retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF) != null;
|
||||
}
|
||||
|
||||
private static IdentityKeyPair getLegacyIdentityKeyPair(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF));
|
||||
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
|
||||
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF)));
|
||||
|
||||
return new IdentityKeyPair(identityKey, privateKey);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String retrieve(Context context, String key) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
return preferences.getString(key, null);
|
||||
}
|
||||
|
||||
public static void save(Context context, String key, String value) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
|
||||
Editor preferencesEditor = preferences.edit();
|
||||
|
||||
preferencesEditor.putString(key, value);
|
||||
@ -186,6 +147,6 @@ public class IdentityKeyUtil {
|
||||
}
|
||||
|
||||
public static void delete(Context context, String key) {
|
||||
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
}
|
||||
}
|
||||
|
@ -1,225 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
* The protocol format is roughly:
|
||||
*
|
||||
* 1) 16 byte random IV.
|
||||
* 2) AES-CBC(plaintext)
|
||||
* 3) HMAC-SHA1 of 1 and 2
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterCipher {
|
||||
|
||||
private static final String TAG = MasterCipher.class.getSimpleName();
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Cipher encryptingCipher;
|
||||
private final Cipher decryptingCipher;
|
||||
private final Mac hmac;
|
||||
|
||||
public MasterCipher(MasterSecret masterSecret) {
|
||||
try {
|
||||
this.masterSecret = masterSecret;
|
||||
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.hmac = Mac.getInstance("HmacSHA1");
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
public String encryptBody(@NonNull String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.session.libsignal.libsignal.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.session.libsignal.libsignal.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(@NonNull byte[] decodedBody) throws InvalidMessageException {
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
|
||||
|
||||
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
|
||||
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
|
||||
|
||||
return encrypted;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
throw new InvalidMessageException(ge);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
|
||||
byte[] encryptedBody = getEncryptedBody(cipher, body);
|
||||
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
|
||||
|
||||
return encryptedAndMacBody;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
Log.w("bodycipher", ge);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean verifyMacFor(String content, byte[] theirMac) {
|
||||
byte[] ourMac = getMacFor(content);
|
||||
Log.i(TAG, "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.i(TAG, "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w(TAG, "Macing: " + content);
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
return mac.doFinal(content.getBytes());
|
||||
} catch (GeneralSecurityException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
|
||||
try {
|
||||
byte[] decodedBody = Base64.decode(body);
|
||||
return decryptBytes(decodedBody);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Bad Base64 Encoding...", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptAndEncodeBytes(@NonNull byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(@NonNull Mac hmac, @NonNull byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
if (encryptedAndMac.length < hmac.getMacLength()) {
|
||||
throw new InvalidMessageException("length(encrypted body + MAC) < length(MAC)");
|
||||
}
|
||||
|
||||
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
|
||||
|
||||
byte[] remoteMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encrypted);
|
||||
|
||||
if (!Arrays.equals(remoteMac, localMac))
|
||||
throw new InvalidMessageException("MAC doesen't match.");
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
|
||||
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] encrypted = cipher.doFinal(body);
|
||||
byte[] iv = cipher.getIV();
|
||||
|
||||
byte[] ivAndBody = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
|
||||
|
||||
return ivAndBody;
|
||||
}
|
||||
|
||||
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
|
||||
byte[] mac = hmac.doFinal(encryptedBody);
|
||||
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
|
||||
|
||||
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
|
||||
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
|
||||
|
||||
return encryptedAndMac;
|
||||
}
|
||||
|
||||
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
|
||||
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
return decryptingCipher;
|
||||
}
|
||||
|
||||
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
return encryptingCipher;
|
||||
}
|
||||
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents 1 and 2.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecret implements Parcelable {
|
||||
|
||||
private final SecretKeySpec encryptionKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
|
||||
@Override
|
||||
public MasterSecret createFromParcel(Parcel in) {
|
||||
return new MasterSecret(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MasterSecret[] newArray(int size) {
|
||||
return new MasterSecret[size];
|
||||
}
|
||||
};
|
||||
|
||||
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
private MasterSecret(Parcel in) {
|
||||
byte[] encryptionKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(encryptionKeyBytes);
|
||||
|
||||
byte[] macKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(macKeyBytes);
|
||||
|
||||
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
|
||||
public SecretKeySpec getEncryptionKey() {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(encryptionKey.getEncoded().length);
|
||||
out.writeByteArray(encryptionKey.getEncoded());
|
||||
out.writeInt(macKey.getEncoded().length);
|
||||
out.writeByteArray(macKey.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterSecret parcelClone() {
|
||||
Parcel thisParcel = Parcel.obtain();
|
||||
Parcel thatParcel = Parcel.obtain();
|
||||
byte[] bytes = null;
|
||||
|
||||
thisParcel.writeValue(this);
|
||||
bytes = thisParcel.marshall();
|
||||
|
||||
thatParcel.unmarshall(bytes, 0, bytes.length);
|
||||
thatParcel.setDataPosition(0);
|
||||
|
||||
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
|
||||
|
||||
thisParcel.recycle();
|
||||
thatParcel.recycle();
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
}
|
@ -1,374 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.PBEParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Helper class for generating and securely storing a MasterSecret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecretUtil {
|
||||
|
||||
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
MasterSecret masterSecret,
|
||||
String newPassphrase)
|
||||
{
|
||||
try {
|
||||
byte[] combinedSecrets = Util.combine(masterSecret.getEncryptionKey().getEncoded(),
|
||||
masterSecret.getMacKey().getEncoded());
|
||||
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(newPassphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return masterSecret;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new AssertionError(gse);
|
||||
}
|
||||
}
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
String originalPassphrase,
|
||||
String newPassphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
|
||||
changeMasterSecretPassphrase(context, masterSecret, newPassphrase);
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static MasterSecret getMasterSecret(Context context, String passphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
try {
|
||||
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
|
||||
byte[] macSalt = retrieve(context, "mac_salt");
|
||||
int iterations = retrieve(context, "passphrase_iterations", 100);
|
||||
byte[] encryptedMasterSecret = verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
|
||||
byte[] encryptionSalt = retrieve(context, "encryption_salt");
|
||||
byte[] combinedSecrets = decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
|
||||
byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
} catch (IOException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
|
||||
@Nullable MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
||||
|
||||
ECPublicKey djbPublicKey = null;
|
||||
ECPrivateKey djbPrivateKey = null;
|
||||
|
||||
if (djbPublicBytes != null) {
|
||||
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
||||
}
|
||||
|
||||
if (masterSecret != null) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
|
||||
if (djbPrivateBytes != null) {
|
||||
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
|
||||
} catch (InvalidKeyException | IOException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context,
|
||||
MasterSecret masterSecret)
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
|
||||
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
||||
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
||||
|
||||
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
|
||||
}
|
||||
|
||||
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
||||
try {
|
||||
byte[] encryptionSecret = generateEncryptionSecret();
|
||||
byte[] macSecret = generateMacSecret();
|
||||
byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(passphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasAsymmericMasterSecret(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
}
|
||||
|
||||
public static boolean isPassphraseInitialized(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return preferences.getBoolean("passphrase_initialized", false);
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, int value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putInt(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, byte[] value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putString(key, Base64.encodeBytes(value))
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, boolean value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putBoolean(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] retrieve(Context context, String key) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
String encodedValue = settings.getString(key, "");
|
||||
|
||||
if (TextUtils.isEmpty(encodedValue)) return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
}
|
||||
|
||||
private static int retrieve(Context context, String key, int defaultValue) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.getInt(key, defaultValue);
|
||||
}
|
||||
|
||||
private static byte[] generateEncryptionSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(128);
|
||||
|
||||
SecretKey key = generator.generateKey();
|
||||
return key.getEncoded();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.w("keyutil", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateMacSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
|
||||
return generator.generateKey().getEncoded();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
private static int generateIterationCount(String passphrase, byte[] salt) {
|
||||
int TARGET_ITERATION_TIME = 50; //ms
|
||||
int MINIMUM_ITERATION_COUNT = 100; //default for low-end devices
|
||||
int BENCHMARK_ITERATION_COUNT = 10000; //baseline starting iteration count
|
||||
|
||||
try {
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, BENCHMARK_ITERATION_COUNT);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
skf.generateSecret(keyspec);
|
||||
long finishTime = System.currentTimeMillis();
|
||||
|
||||
int scaledIterationTarget = (int) (((double)BENCHMARK_ITERATION_COUNT / (double)(finishTime - startTime)) * TARGET_ITERATION_TIME);
|
||||
|
||||
if (scaledIterationTarget < MINIMUM_ITERATION_COUNT) return MINIMUM_ITERATION_COUNT;
|
||||
else return scaledIterationTarget;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
return skf.generateSecret(keyspec);
|
||||
}
|
||||
|
||||
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int iterations, int opMode)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
|
||||
cipher.init(opMode, key, new PBEParameterSpec(salt, iterations));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static byte[] encryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static byte[] decryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException, IOException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static Mac getMacForPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
byte[] pbkdf2 = key.getEncoded();
|
||||
SecretKeySpec hmacKey = new SecretKeySpec(pbkdf2, "HmacSHA1");
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(hmacKey);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private static byte[] verifyMac(byte[] macSalt, int iterations, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
|
||||
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
|
||||
|
||||
byte[] givenMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encryptedData);
|
||||
|
||||
if (Arrays.equals(givenMac, localMac)) return encryptedData;
|
||||
else throw new InvalidPassphraseException("MAC Error");
|
||||
}
|
||||
|
||||
private static byte[] macWithPassphrase(byte[] macSalt, int iterations, byte[] data, String passphrase) throws GeneralSecurityException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
byte[] mac = hmac.doFinal(data);
|
||||
byte[] result = new byte[data.length + mac.length];
|
||||
|
||||
System.arraycopy(data, 0, result, 0, data.length);
|
||||
System.arraycopy(mac, 0, result, data.length, mac.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -14,70 +14,79 @@ import org.session.libsignal.libsignal.state.SessionStore;
|
||||
import org.session.libsignal.libsignal.state.SignalProtocolStore;
|
||||
import org.session.libsignal.libsignal.state.SignedPreKeyRecord;
|
||||
import org.session.libsignal.libsignal.state.SignedPreKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SignalProtocolStoreImpl implements SignalProtocolStore {
|
||||
|
||||
// private final PreKeyStore preKeyStore;
|
||||
// private final SignedPreKeyStore signedPreKeyStore;
|
||||
private final IdentityKeyStore identityKeyStore;
|
||||
private final Context context;
|
||||
// private final PreKeyStore preKeyStore;
|
||||
// private final SignedPreKeyStore signedPreKeyStore;
|
||||
// private final IdentityKeyStore identityKeyStore;
|
||||
private final SessionStore sessionStore;
|
||||
|
||||
public SignalProtocolStoreImpl(Context context) {
|
||||
// this.preKeyStore = new TextSecurePreKeyStore(context);
|
||||
// this.signedPreKeyStore = new TextSecurePreKeyStore(context);
|
||||
this.identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
// this.identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
this.sessionStore = new TextSecureSessionStore(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return identityKeyStore.getIdentityKeyPair();
|
||||
return IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
// return identityKeyStore.getIdentityKeyPair();
|
||||
// throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalRegistrationId() {
|
||||
return identityKeyStore.getLocalRegistrationId();
|
||||
// return identityKeyStore.getLocalRegistrationId();
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address, identityKey);
|
||||
// return identityKeyStore.saveIdentity(address, identityKey);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
||||
// return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
return identityKeyStore.getIdentity(address);
|
||||
// return identityKeyStore.getIdentity(address);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
// return preKeyStore.loadPreKey(preKeyId);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePreKey(int preKeyId, PreKeyRecord record) {
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
// preKeyStore.storePreKey(preKeyId, record);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPreKey(int preKeyId) {
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
// return preKeyStore.containsPreKey(preKeyId);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePreKey(int preKeyId) {
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
// preKeyStore.removePreKey(preKeyId);
|
||||
throw new UnsupportedOperationException("This method will be removed with refactor.");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,151 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.IdentityKeyPair;
|
||||
import org.session.libsignal.libsignal.SignalProtocolAddress;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
||||
|
||||
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
|
||||
|
||||
private static final String TAG = TextSecureIdentityKeyStore.class.getSimpleName();
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public TextSecureIdentityKeyStore(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalRegistrationId() {
|
||||
return TextSecurePreferences.getLocalRegistrationId(context);
|
||||
}
|
||||
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
Address signalAddress = Address.fromSerialized(address.getName());
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(signalAddress);
|
||||
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.i(TAG, "Saving new identity...");
|
||||
identityDatabase.saveIdentity(signalAddress, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
|
||||
Log.i(TAG, "Replacing existing identity...");
|
||||
VerifiedStatus verifiedStatus;
|
||||
|
||||
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED ||
|
||||
identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED)
|
||||
{
|
||||
verifiedStatus = VerifiedStatus.UNVERIFIED;
|
||||
} else {
|
||||
verifiedStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
||||
identityDatabase.saveIdentity(signalAddress, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
|
||||
IdentityUtil.markIdentityUpdate(context, Recipient.from(context, signalAddress, true));
|
||||
SessionUtil.archiveSiblingSessions(context, address);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.i(TAG, "Setting approval status...");
|
||||
identityDatabase.setApproval(signalAddress, nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return saveIdentity(address, identityKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
String ourNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
Address theirAddress = Address.fromSerialized(address.getName());
|
||||
|
||||
if (ourNumber.equals(address.getName()) || Address.fromSerialized(ourNumber).equals(theirAddress)) {
|
||||
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirAddress));
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
Optional<IdentityRecord> record = DatabaseFactory.getIdentityDatabase(context).getIdentity(Address.fromSerialized(address.getName()));
|
||||
|
||||
if (record.isPresent()) {
|
||||
return record.get().getIdentityKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.w(TAG, "Nothing here, returning true...");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
|
||||
Log.w(TAG, "Identity keys don't match...");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
Log.w(TAG, "Needs unverified approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.w(TAG, "Needs non-blocking approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isFirstUse() &&
|
||||
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
|
||||
!identityRecord.isApprovedNonBlocking();
|
||||
}
|
||||
}
|
@ -17,18 +17,15 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
|
||||
@ -36,7 +33,6 @@ 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.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class DatabaseFactory {
|
||||
|
||||
@ -51,7 +47,6 @@ public class DatabaseFactory {
|
||||
private final MediaDatabase media;
|
||||
private final ThreadDatabase thread;
|
||||
private final MmsSmsDatabase mmsSmsDatabase;
|
||||
private final IdentityDatabase identityDatabase;
|
||||
private final DraftDatabase draftDatabase;
|
||||
private final PushDatabase pushDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
@ -107,10 +102,6 @@ public class DatabaseFactory {
|
||||
return getInstance(context).media;
|
||||
}
|
||||
|
||||
public static IdentityDatabase getIdentityDatabase(Context context) {
|
||||
return getInstance(context).identityDatabase;
|
||||
}
|
||||
|
||||
public static DraftDatabase getDraftDatabase(Context context) {
|
||||
return getInstance(context).draftDatabase;
|
||||
}
|
||||
@ -211,7 +202,6 @@ public class DatabaseFactory {
|
||||
this.media = new MediaDatabase(context, databaseHelper);
|
||||
this.thread = new ThreadDatabase(context, databaseHelper);
|
||||
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
|
||||
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
|
||||
this.draftDatabase = new DraftDatabase(context, databaseHelper);
|
||||
this.pushDatabase = new PushDatabase(context, databaseHelper);
|
||||
this.groupDatabase = new GroupDatabase(context, databaseHelper);
|
||||
@ -233,28 +223,4 @@ public class DatabaseFactory {
|
||||
this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
int fromVersion, DatabaseUpgradeActivity.DatabaseUpgradeListener listener)
|
||||
{
|
||||
databaseHelper.getWritableDatabase();
|
||||
|
||||
ClassicOpenHelper legacyOpenHelper = null;
|
||||
|
||||
if (fromVersion < DatabaseUpgradeActivity.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) {
|
||||
legacyOpenHelper = new ClassicOpenHelper(context);
|
||||
legacyOpenHelper.onApplicationLevelUpgrade(context, masterSecret, fromVersion, listener);
|
||||
}
|
||||
|
||||
if (fromVersion < DatabaseUpgradeActivity.SQLCIPHER && TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
|
||||
if (legacyOpenHelper == null) {
|
||||
legacyOpenHelper = new ClassicOpenHelper(context);
|
||||
}
|
||||
|
||||
SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret,
|
||||
legacyOpenHelper.getWritableDatabase(),
|
||||
databaseHelper.getWritableDatabase(),
|
||||
listener);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,243 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* 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.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IdentityDatabase extends Database {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityDatabase.class.getSimpleName();
|
||||
|
||||
private static final String TABLE_NAME = "identities";
|
||||
private static final String ID = "_id";
|
||||
private static final String ADDRESS = "address";
|
||||
private static final String IDENTITY_KEY = "key";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
private static final String FIRST_USE = "first_use";
|
||||
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
|
||||
private static final String VERIFIED = "verified";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
ADDRESS + " TEXT UNIQUE, " +
|
||||
IDENTITY_KEY + " TEXT, " +
|
||||
FIRST_USE + " INTEGER DEFAULT 0, " +
|
||||
TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
VERIFIED + " INTEGER DEFAULT 0, " +
|
||||
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
|
||||
|
||||
public enum VerifiedStatus {
|
||||
DEFAULT, VERIFIED, UNVERIFIED;
|
||||
|
||||
public int toInt() {
|
||||
if (this == DEFAULT) return 0;
|
||||
else if (this == VERIFIED) return 1;
|
||||
else if (this == UNVERIFIED) return 2;
|
||||
else throw new AssertionError();
|
||||
}
|
||||
|
||||
public static VerifiedStatus forState(int state) {
|
||||
if (state == 0) return DEFAULT;
|
||||
else if (state == 1) return VERIFIED;
|
||||
else if (state == 2) return UNVERIFIED;
|
||||
else throw new AssertionError("No such state: " + state);
|
||||
}
|
||||
}
|
||||
|
||||
IdentityDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public Cursor getIdentities() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
return database.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public @Nullable IdentityReader readerFor(@Nullable Cursor cursor) {
|
||||
if (cursor == null) return null;
|
||||
return new IdentityReader(cursor);
|
||||
}
|
||||
|
||||
public Optional<IdentityRecord> getIdentity(Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, ADDRESS + " = ?",
|
||||
new String[] {address.serialize()}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return Optional.of(getIdentityRecord(cursor));
|
||||
}
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public void saveIdentity(Address address, IdentityKey identityKey, VerifiedStatus verifiedStatus,
|
||||
boolean firstUse, long timestamp, boolean nonBlockingApproval)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(ADDRESS, address.serialize());
|
||||
contentValues.put(IDENTITY_KEY, identityKeyString);
|
||||
contentValues.put(TIMESTAMP, timestamp);
|
||||
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
||||
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
|
||||
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
|
||||
|
||||
database.replace(TABLE_NAME, null, contentValues);
|
||||
|
||||
EventBus.getDefault().post(new IdentityRecord(address, identityKey, verifiedStatus,
|
||||
firstUse, timestamp, nonBlockingApproval));
|
||||
}
|
||||
|
||||
public void setApproval(Address address, boolean nonBlockingApproval) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
|
||||
|
||||
database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", new String[] {address.serialize()});
|
||||
}
|
||||
|
||||
public void setVerified(Address address, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
||||
|
||||
int updated = database.update(TABLE_NAME, contentValues, ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?",
|
||||
new String[] {address.serialize(), Base64.encodeBytes(identityKey.serialize())});
|
||||
|
||||
if (updated > 0) {
|
||||
Optional<IdentityRecord> record = getIdentity(address);
|
||||
if (record.isPresent()) EventBus.getDefault().post(record.get());
|
||||
}
|
||||
}
|
||||
|
||||
private IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
|
||||
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED));
|
||||
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
|
||||
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
|
||||
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
||||
|
||||
return new IdentityRecord(Address.fromSerialized(address), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
|
||||
}
|
||||
|
||||
public static class IdentityRecord {
|
||||
|
||||
private final Address address;
|
||||
private final IdentityKey identitykey;
|
||||
private final VerifiedStatus verifiedStatus;
|
||||
private final boolean firstUse;
|
||||
private final long timestamp;
|
||||
private final boolean nonblockingApproval;
|
||||
|
||||
private IdentityRecord(Address address,
|
||||
IdentityKey identitykey, VerifiedStatus verifiedStatus,
|
||||
boolean firstUse, long timestamp, boolean nonblockingApproval)
|
||||
{
|
||||
this.address = address;
|
||||
this.identitykey = identitykey;
|
||||
this.verifiedStatus = verifiedStatus;
|
||||
this.firstUse = firstUse;
|
||||
this.timestamp = timestamp;
|
||||
this.nonblockingApproval = nonblockingApproval;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identitykey;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public VerifiedStatus getVerifiedStatus() {
|
||||
return verifiedStatus;
|
||||
}
|
||||
|
||||
public boolean isApprovedNonBlocking() {
|
||||
return nonblockingApproval;
|
||||
}
|
||||
|
||||
public boolean isFirstUse() {
|
||||
return firstUse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "{address: " + address + ", identityKey: " + identitykey + ", verifiedStatus: " + verifiedStatus + ", firstUse: " + firstUse + "}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class IdentityReader {
|
||||
private final Cursor cursor;
|
||||
|
||||
IdentityReader(@NonNull Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public @Nullable IdentityRecord getNext() {
|
||||
if (cursor.moveToNext()) {
|
||||
try {
|
||||
return getIdentityRecord(cursor);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,254 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.function.BiFunction;
|
||||
|
||||
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SQLCipherMigrationHelper {
|
||||
|
||||
private static final String TAG = SQLCipherMigrationHelper.class.getSimpleName();
|
||||
|
||||
private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
|
||||
private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
|
||||
|
||||
static void migratePlaintext(@NonNull Context context,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
|
||||
{
|
||||
modernDb.beginTransaction();
|
||||
try {
|
||||
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
|
||||
copyTable("identities", legacyDb, modernDb, null);
|
||||
copyTable("push", legacyDb, modernDb, null);
|
||||
copyTable("groups", legacyDb, modernDb, null);
|
||||
copyTable("recipient_preferences", legacyDb, modernDb, null);
|
||||
copyTable("group_receipts", legacyDb, modernDb, null);
|
||||
modernDb.setTransactionSuccessful();
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
GenericForegroundService.stopForegroundTask(context);
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrateCiphertext(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
|
||||
@Nullable DatabaseUpgradeActivity.DatabaseUpgradeListener listener)
|
||||
{
|
||||
MasterCipher legacyCipher = new MasterCipher(masterSecret);
|
||||
AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
|
||||
|
||||
modernDb.beginTransaction();
|
||||
|
||||
try {
|
||||
GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database));
|
||||
int total = 5000;
|
||||
|
||||
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("type"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.second);
|
||||
row.put("type", plaintext.first);
|
||||
|
||||
if (listener != null && (progress.first % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(0, progress.first, progress.second), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("mms", legacyDb, modernDb, (row, progress) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("msg_box"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.second);
|
||||
row.put("msg_box", plaintext.first);
|
||||
|
||||
if (listener != null && (progress.first % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(1000, progress.first, progress.second), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("part", legacyDb, modernDb, (row, progress) -> {
|
||||
String fileName = row.getAsString("file_name");
|
||||
String mediaKey = row.getAsString("cd");
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(fileName)) {
|
||||
row.put("file_name", legacyCipher.decryptBody(fileName));
|
||||
}
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(mediaKey)) {
|
||||
byte[] plaintext;
|
||||
|
||||
if (mediaKey.startsWith("?ASYNC-")) {
|
||||
plaintext = legacyAsymmetricCipher.decryptBytes(Base64.decode(mediaKey.substring("?ASYNC-".length())));
|
||||
} else {
|
||||
plaintext = legacyCipher.decryptBytes(Base64.decode(mediaKey));
|
||||
}
|
||||
|
||||
row.put("cd", Base64.encodeBytes(plaintext));
|
||||
}
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
if (listener != null && (progress.first % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(2000, progress.first, progress.second), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("thread", legacyDb, modernDb, (row, progress) -> {
|
||||
Long snippetType = row.getAsLong("snippet_type");
|
||||
if (snippetType == null) snippetType = 0L;
|
||||
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
snippetType, row.getAsString("snippet"));
|
||||
|
||||
row.put("snippet", plaintext.second);
|
||||
row.put("snippet_type", plaintext.first);
|
||||
|
||||
if (listener != null && (progress.first % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(3000, progress.first, progress.second), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
|
||||
copyTable("drafts", legacyDb, modernDb, (row, progress) -> {
|
||||
String draftType = row.getAsString("type");
|
||||
String draft = row.getAsString("value");
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(draftType)) row.put("type", legacyCipher.decryptBody(draftType));
|
||||
if (!TextUtils.isEmpty(draft)) row.put("value", legacyCipher.decryptBody(draft));
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
if (listener != null && (progress.first % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(4000, progress.first, progress.second), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
|
||||
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
|
||||
modernDb.setTransactionSuccessful();
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
GenericForegroundService.stopForegroundTask(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyTable(@NonNull String tableName,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
|
||||
@Nullable BiFunction<ContentValues, Pair<Integer, Integer>, ContentValues> transformer)
|
||||
{
|
||||
Set<String> destinationColumns = getTableColumns(tableName, modernDb);
|
||||
|
||||
try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) {
|
||||
int count = (cursor != null) ? cursor.getCount() : 0;
|
||||
int progress = 1;
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
ContentValues row = new ContentValues();
|
||||
|
||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
||||
String columnName = cursor.getColumnName(i);
|
||||
|
||||
if (destinationColumns.contains(columnName)) {
|
||||
switch (cursor.getType(i)) {
|
||||
case Cursor.FIELD_TYPE_STRING: row.put(columnName, cursor.getString(i)); break;
|
||||
case Cursor.FIELD_TYPE_FLOAT: row.put(columnName, cursor.getFloat(i)); break;
|
||||
case Cursor.FIELD_TYPE_INTEGER: row.put(columnName, cursor.getLong(i)); break;
|
||||
case Cursor.FIELD_TYPE_BLOB: row.put(columnName, cursor.getBlob(i)); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transformer != null) {
|
||||
row = transformer.apply(row, new Pair<>(progress++, count));
|
||||
}
|
||||
|
||||
modernDb.insert(tableName, null, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<Long, String> getPlaintextBody(@NonNull MasterCipher legacyCipher,
|
||||
@NonNull AsymmetricMasterCipher legacyAsymmetricCipher,
|
||||
long type,
|
||||
@Nullable String body)
|
||||
{
|
||||
try {
|
||||
if (!TextUtils.isEmpty(body)) {
|
||||
if ((type & ENCRYPTION_SYMMETRIC_BIT) != 0) body = legacyCipher.decryptBody(body);
|
||||
else if ((type & ENCRYPTION_ASYMMETRIC_BIT) != 0) body = legacyAsymmetricCipher.decryptBody(body);
|
||||
}
|
||||
} catch (InvalidMessageException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
type &= ~(ENCRYPTION_SYMMETRIC_BIT);
|
||||
type &= ~(ENCRYPTION_ASYMMETRIC_BIT);
|
||||
|
||||
return new Pair<>(type, body);
|
||||
}
|
||||
|
||||
private static Set<String> getTableColumns(String tableName, net.sqlcipher.database.SQLiteDatabase database) {
|
||||
Set<String> results = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = database.rawQuery("PRAGMA table_info(" + tableName + ")", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(cursor.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static int getTotalProgress(int sectionOffset, int sectionProgress, int sectionTotal) {
|
||||
double percentOfSectionComplete = ((double)sectionProgress) / ((double)sectionTotal);
|
||||
return sectionOffset + (int)(((double)1000) * percentOfSectionComplete);
|
||||
}
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@ -14,15 +10,11 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
@ -41,44 +33,14 @@ 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.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = SQLCipherOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2;
|
||||
private static final int MIGRATE_PREKEYS_VERSION = 3;
|
||||
private static final int MIGRATE_SESSIONS_VERSION = 4;
|
||||
private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5;
|
||||
private static final int ATTACHMENT_DIMENSIONS = 6;
|
||||
private static final int QUOTED_REPLIES = 7;
|
||||
private static final int SHARED_CONTACTS = 8;
|
||||
private static final int FULL_TEXT_SEARCH = 9;
|
||||
private static final int BAD_IMPORT_CLEANUP = 10;
|
||||
private static final int QUOTE_MISSING = 11;
|
||||
private static final int NOTIFICATION_CHANNELS = 12;
|
||||
private static final int SECRET_SENDER = 13;
|
||||
private static final int ATTACHMENT_CAPTIONS = 14;
|
||||
private static final int ATTACHMENT_CAPTIONS_FIX = 15;
|
||||
private static final int PREVIEWS = 16;
|
||||
private static final int CONVERSATION_SEARCH = 17;
|
||||
private static final int SELF_ATTACHMENT_CLEANUP = 18;
|
||||
private static final int RECIPIENT_FORCE_SMS_SELECTION = 19;
|
||||
private static final int JOBMANAGER_STRIKES_BACK = 20;
|
||||
private static final int STICKERS = 21;
|
||||
private static final int lokiV1 = 22;
|
||||
private static final int lokiV2 = 23;
|
||||
private static final int lokiV3 = 24;
|
||||
private static final int lokiV4 = 25;
|
||||
private static final int lokiV5 = 26;
|
||||
private static final int lokiV6 = 27;
|
||||
// First public release (1.0.0) DB version was 27.
|
||||
// So we have to keep the migrations onwards.
|
||||
private static final int lokiV7 = 28;
|
||||
private static final int lokiV8 = 29;
|
||||
private static final int lokiV9 = 30;
|
||||
@ -91,9 +53,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV16 = 37;
|
||||
private static final int lokiV17 = 38;
|
||||
private static final int lokiV18_CLEAR_BG_POLL_JOBS = 39;
|
||||
private static final int lokiV19_OLD_CODE_CLEANUP = 40; //TODO Change back to 40 when the refactoring is over.
|
||||
//TODO Merge all "refactor" migrations to one before pushing to the main repo.
|
||||
private static final int lokiV19_REFACTOR0 = 40;
|
||||
private static final int lokiV19_REFACTOR1 = 41;
|
||||
private static final int lokiV19_REFACTOR2 = 42;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV19_OLD_CODE_CLEANUP; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV19_REFACTOR1;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -124,7 +90,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(MmsDatabase.CREATE_TABLE);
|
||||
db.execSQL(AttachmentDatabase.CREATE_TABLE);
|
||||
db.execSQL(ThreadDatabase.CREATE_TABLE);
|
||||
db.execSQL(IdentityDatabase.CREATE_TABLE);
|
||||
db.execSQL(DraftDatabase.CREATE_TABLE);
|
||||
db.execSQL(PushDatabase.CREATE_TABLE);
|
||||
db.execSQL(GroupDatabase.CREATE_TABLE);
|
||||
@ -176,20 +141,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
executeStatements(db, GroupDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||
executeStatements(db, StickerDatabase.CREATE_INDEXES);
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
|
||||
android.database.sqlite.SQLiteDatabase legacyDb = legacyHelper.getWritableDatabase();
|
||||
|
||||
SQLCipherMigrationHelper.migratePlaintext(context, legacyDb, db);
|
||||
|
||||
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
|
||||
|
||||
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null);
|
||||
else TextSecurePreferences.setNeedsSqlCipherMigration(context, true);
|
||||
|
||||
SessionStoreMigrationHelper.migrateSessions(context, db);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -209,379 +160,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
try {
|
||||
|
||||
if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) {
|
||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId());
|
||||
}
|
||||
|
||||
if (oldVersion < MIGRATE_PREKEYS_VERSION) {
|
||||
db.execSQL("CREATE TABLE signed_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL, signature TEXT NOT NULL, timestamp INTEGER DEFAULT 0)");
|
||||
db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)");
|
||||
}
|
||||
|
||||
if (oldVersion < MIGRATE_SESSIONS_VERSION) {
|
||||
db.execSQL("CREATE TABLE sessions (_id INTEGER PRIMARY KEY, address TEXT NOT NULL, device INTEGER NOT NULL, record BLOB NOT NULL, UNIQUE(address, device) ON CONFLICT REPLACE)");
|
||||
SessionStoreMigrationHelper.migrateSessions(context, db);
|
||||
}
|
||||
|
||||
if (oldVersion < NO_MORE_IMAGE_THUMBNAILS_VERSION) {
|
||||
ContentValues update = new ContentValues();
|
||||
update.put("thumbnail", (String)null);
|
||||
update.put("aspect_ratio", (String)null);
|
||||
update.put("thumbnail_random", (String)null);
|
||||
|
||||
try (Cursor cursor = db.query("part", new String[] {"_id", "ct", "thumbnail"}, "thumbnail IS NOT NULL", null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||
String contentType = cursor.getString(cursor.getColumnIndexOrThrow("ct"));
|
||||
|
||||
if (contentType != null && !contentType.startsWith("video")) {
|
||||
String thumbnailPath = cursor.getString(cursor.getColumnIndexOrThrow("thumbnail"));
|
||||
File thumbnailFile = new File(thumbnailPath);
|
||||
thumbnailFile.delete();
|
||||
|
||||
db.update("part", update, "_id = ?", new String[] {String.valueOf(id)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < ATTACHMENT_DIMENSIONS) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN width INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN height INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < QUOTED_REPLIES) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_id INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_author TEXT");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_body TEXT");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_attachment INTEGER DEFAULT -1");
|
||||
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < SHARED_CONTACTS) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN shared_contacts TEXT");
|
||||
}
|
||||
|
||||
if (oldVersion < FULL_TEXT_SEARCH) {
|
||||
db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, content=sms, content_rowid=_id)");
|
||||
db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(rowid, body) VALUES (new._id, new.body);\n" +
|
||||
"END;");
|
||||
db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
|
||||
"END;\n");
|
||||
db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(sms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
|
||||
" INSERT INTO sms_fts(rowid, body) VALUES(new._id, new.body);\n" +
|
||||
"END;");
|
||||
|
||||
db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, content=mms, content_rowid=_id)");
|
||||
db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(rowid, body) VALUES (new._id, new.body);\n" +
|
||||
"END;");
|
||||
db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
|
||||
"END;\n");
|
||||
db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(mms_fts, rowid, body) VALUES('delete', old._id, old.body);\n" +
|
||||
" INSERT INTO mms_fts(rowid, body) VALUES(new._id, new.body);\n" +
|
||||
"END;");
|
||||
|
||||
Log.i(TAG, "Beginning to build search index.");
|
||||
long start = SystemClock.elapsedRealtime();
|
||||
|
||||
db.execSQL("INSERT INTO sms_fts (rowid, body) SELECT _id, body FROM sms");
|
||||
|
||||
long smsFinished = SystemClock.elapsedRealtime();
|
||||
Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms");
|
||||
|
||||
db.execSQL("INSERT INTO mms_fts (rowid, body) SELECT _id, body FROM mms");
|
||||
|
||||
long mmsFinished = SystemClock.elapsedRealtime();
|
||||
Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms");
|
||||
Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms");
|
||||
}
|
||||
|
||||
if (oldVersion < BAD_IMPORT_CLEANUP) {
|
||||
String trimmedCondition = " NOT IN (SELECT _id FROM mms)";
|
||||
|
||||
db.delete("group_receipts", "mms_id" + trimmedCondition, null);
|
||||
|
||||
String[] columns = new String[] { "_id", "unique_id", "_data", "thumbnail"};
|
||||
|
||||
try (Cursor cursor = db.query("part", columns, "mid" + trimmedCondition, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
db.delete("part", "_id = ? AND unique_id = ?", new String[] { String.valueOf(cursor.getLong(0)), String.valueOf(cursor.getLong(1)) });
|
||||
|
||||
String data = cursor.getString(2);
|
||||
String thumbnail = cursor.getString(3);
|
||||
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
new File(data).delete();
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(thumbnail)) {
|
||||
new File(thumbnail).delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This column only being checked due to upgrade issues as described in #8184
|
||||
if (oldVersion < QUOTE_MISSING && !columnExists(db, "mms", "quote_missing")) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_missing INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
// Note: The column only being checked due to upgrade issues as described in #8184
|
||||
if (oldVersion < NOTIFICATION_CHANNELS && !columnExists(db, "recipient_preferences", "notification_channel")) {
|
||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN notification_channel TEXT DEFAULT NULL");
|
||||
NotificationChannels.create(context);
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT recipient_ids, system_display_name, signal_profile_name, notification, vibrate FROM recipient_preferences WHERE notification NOT NULL OR vibrate != 0", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String addressString = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
||||
Address address = Address.fromExternal(context, addressString);
|
||||
String systemName = cursor.getString(cursor.getColumnIndexOrThrow("system_display_name"));
|
||||
String profileName = cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_name"));
|
||||
String messageSound = cursor.getString(cursor.getColumnIndexOrThrow("notification"));
|
||||
Uri messageSoundUri = messageSound != null ? Uri.parse(messageSound) : null;
|
||||
int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow("vibrate"));
|
||||
String displayName = NotificationChannels.getChannelDisplayNameFor(context, systemName, profileName, address);
|
||||
boolean vibrateEnabled = vibrateState == 0 ? TextSecurePreferences.isNotificationVibrateEnabled(context) : vibrateState == 1;
|
||||
|
||||
if (address.isGroup()) {
|
||||
try(Cursor groupCursor = db.rawQuery("SELECT title FROM groups WHERE group_id = ?", new String[] { address.toGroupString() })) {
|
||||
if (groupCursor != null && groupCursor.moveToFirst()) {
|
||||
String title = groupCursor.getString(groupCursor.getColumnIndexOrThrow("title"));
|
||||
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
displayName = title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String channelId = NotificationChannels.createChannelFor(context, address, displayName, messageSoundUri, vibrateEnabled);
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put("notification_channel", channelId);
|
||||
db.update("recipient_preferences", values, "recipient_ids = ?", new String[] { addressString });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < SECRET_SENDER) {
|
||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN unidentified_access_mode INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE push ADD COLUMN server_timestamp INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE push ADD COLUMN server_guid TEXT DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE group_receipts ADD COLUMN unidentified INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN unidentified INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN unidentified INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < ATTACHMENT_CAPTIONS) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
// 4.30.8 included a migration, but not a correct CREATE_TABLE statement, so we need to add
|
||||
// this column if it isn't present.
|
||||
if (oldVersion < ATTACHMENT_CAPTIONS_FIX) {
|
||||
if (!columnExists(db, "part", "caption")) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL");
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < PREVIEWS) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN previews TEXT");
|
||||
}
|
||||
|
||||
if (oldVersion < CONVERSATION_SEARCH) {
|
||||
db.execSQL("DROP TABLE sms_fts");
|
||||
db.execSQL("DROP TABLE mms_fts");
|
||||
db.execSQL("DROP TRIGGER sms_ai");
|
||||
db.execSQL("DROP TRIGGER sms_au");
|
||||
db.execSQL("DROP TRIGGER sms_ad");
|
||||
db.execSQL("DROP TRIGGER mms_ai");
|
||||
db.execSQL("DROP TRIGGER mms_au");
|
||||
db.execSQL("DROP TRIGGER mms_ad");
|
||||
|
||||
db.execSQL("CREATE VIRTUAL TABLE sms_fts USING fts5(body, thread_id UNINDEXED, content=sms, content_rowid=_id)");
|
||||
db.execSQL("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" +
|
||||
"END;");
|
||||
db.execSQL("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
|
||||
"END;\n");
|
||||
db.execSQL("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN\n" +
|
||||
" INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
|
||||
" INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" +
|
||||
"END;");
|
||||
|
||||
db.execSQL("CREATE VIRTUAL TABLE mms_fts USING fts5(body, thread_id UNINDEXED, content=mms, content_rowid=_id)");
|
||||
db.execSQL("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n" +
|
||||
"END;");
|
||||
db.execSQL("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
|
||||
"END;\n");
|
||||
db.execSQL("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN\n" +
|
||||
" INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n" +
|
||||
" INSERT INTO mms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id);\n" +
|
||||
"END;");
|
||||
|
||||
Log.i(TAG, "Beginning to build search index.");
|
||||
long start = SystemClock.elapsedRealtime();
|
||||
|
||||
db.execSQL("INSERT INTO sms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM sms");
|
||||
|
||||
long smsFinished = SystemClock.elapsedRealtime();
|
||||
Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms");
|
||||
|
||||
db.execSQL("INSERT INTO mms_fts (rowid, body, thread_id) SELECT _id, body, thread_id FROM mms");
|
||||
|
||||
long mmsFinished = SystemClock.elapsedRealtime();
|
||||
Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms");
|
||||
Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms");
|
||||
}
|
||||
|
||||
if (oldVersion < SELF_ATTACHMENT_CLEANUP) {
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
|
||||
if (!TextUtils.isEmpty(localNumber)) {
|
||||
try (Cursor threadCursor = db.rawQuery("SELECT _id FROM thread WHERE recipient_ids = ?", new String[]{ localNumber })) {
|
||||
if (threadCursor != null && threadCursor.moveToFirst()) {
|
||||
long threadId = threadCursor.getLong(0);
|
||||
ContentValues updateValues = new ContentValues(1);
|
||||
|
||||
updateValues.put("pending_push", 0);
|
||||
|
||||
int count = db.update("part", updateValues, "mid IN (SELECT _id FROM mms WHERE thread_id = ?)", new String[]{ String.valueOf(threadId) });
|
||||
Log.i(TAG, "Updated " + count + " self-sent attachments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < RECIPIENT_FORCE_SMS_SELECTION) {
|
||||
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN force_sms_selection INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < JOBMANAGER_STRIKES_BACK) {
|
||||
db.execSQL("CREATE TABLE job_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"job_spec_id TEXT UNIQUE, " +
|
||||
"factory_key TEXT, " +
|
||||
"queue_key TEXT, " +
|
||||
"create_time INTEGER, " +
|
||||
"next_run_attempt_time INTEGER, " +
|
||||
"run_attempt INTEGER, " +
|
||||
"max_attempts INTEGER, " +
|
||||
"max_backoff INTEGER, " +
|
||||
"max_instances INTEGER, " +
|
||||
"lifespan INTEGER, " +
|
||||
"serialized_data TEXT, " +
|
||||
"is_running INTEGER)");
|
||||
|
||||
db.execSQL("CREATE TABLE constraint_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"job_spec_id TEXT, " +
|
||||
"factory_key TEXT, " +
|
||||
"UNIQUE(job_spec_id, factory_key))");
|
||||
|
||||
db.execSQL("CREATE TABLE dependency_spec(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"job_spec_id TEXT, " +
|
||||
"depends_on_job_spec_id TEXT, " +
|
||||
"UNIQUE(job_spec_id, depends_on_job_spec_id))");
|
||||
}
|
||||
|
||||
if (oldVersion < STICKERS) {
|
||||
db.execSQL("CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"pack_id TEXT NOT NULL, " +
|
||||
"pack_key TEXT NOT NULL, " +
|
||||
"pack_title TEXT NOT NULL, " +
|
||||
"pack_author TEXT NOT NULL, " +
|
||||
"sticker_id INTEGER, " +
|
||||
"cover INTEGER, " +
|
||||
"emoji TEXT NOT NULL, " +
|
||||
"last_used INTEGER, " +
|
||||
"installed INTEGER," +
|
||||
"file_path TEXT NOT NULL, " +
|
||||
"file_length INTEGER, " +
|
||||
"file_random BLOB, " +
|
||||
"UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)");
|
||||
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS sticker_pack_id_index ON sticker (pack_id);");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON sticker (sticker_id);");
|
||||
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN sticker_pack_id TEXT");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN sticker_pack_key TEXT");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN sticker_id INTEGER DEFAULT -1");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON part (sticker_pack_id)");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV1) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV2) {
|
||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV3) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand());
|
||||
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
|
||||
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN avatar_url TEXT");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN url TEXT");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV4) {
|
||||
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV5) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV6) {
|
||||
// Migrate public chats from __textsecure_group__ to __loki_public_chat_group__
|
||||
try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) {
|
||||
while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) {
|
||||
String chatString = lokiPublicChatCursor.getString(0);
|
||||
PublicChat publicChat = PublicChat.fromJSON(chatString);
|
||||
if (publicChat != null) {
|
||||
byte[] groupId = publicChat.getId().getBytes();
|
||||
String oldId = GroupUtil.getEncodedId(groupId, false);
|
||||
String newId = GroupUtil.getEncodedOpenGroupId(groupId);
|
||||
ContentValues threadUpdate = new ContentValues();
|
||||
threadUpdate.put("recipient_ids", newId);
|
||||
db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
|
||||
ContentValues groupUpdate = new ContentValues();
|
||||
groupUpdate.put("group_id", newId);
|
||||
db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate RSS feeds from __textsecure_group__ to __loki_rss_feed_group__
|
||||
String[] rssFeedIds = new String[] { "loki.network.feed", "loki.network.messenger-updates.feed" };
|
||||
for (String groupId : rssFeedIds) {
|
||||
String oldId = GroupUtil.getEncodedId(groupId.getBytes(), false);
|
||||
String newId = GroupUtil.getEncodedRSSFeedId(groupId.getBytes());
|
||||
ContentValues threadUpdate = new ContentValues();
|
||||
threadUpdate.put("recipient_ids", newId);
|
||||
db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
|
||||
ContentValues groupUpdate = new ContentValues();
|
||||
groupUpdate.put("group_id", newId);
|
||||
db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
|
||||
}
|
||||
|
||||
// Add admin field in groups
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV7) {
|
||||
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
||||
}
|
||||
@ -637,26 +215,28 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("DELETE FROM constraint_spec WHERE factory_key = 'BackgroundPollJob'");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV19_OLD_CODE_CLEANUP) {
|
||||
// Many classes were removed. We need to update D structure and data to match the code changes.
|
||||
|
||||
String[] deletedJobKeys = {
|
||||
"ServiceOutageDetectionJob",
|
||||
};
|
||||
for (String jobKey : deletedJobKeys) {
|
||||
db.execSQL("DELETE FROM job_spec WHERE factory_key = ?", new String[]{jobKey});
|
||||
db.execSQL("DELETE FROM constraint_spec WHERE factory_key = ?", new String[]{jobKey});
|
||||
}
|
||||
// Many classes were removed. We need to update DB structure and data to match the code changes.
|
||||
//TODO Merge "refactor" changes in one migration.
|
||||
if (oldVersion < lokiV19_REFACTOR0) {
|
||||
deleteJobRecords(db, "ServiceOutageDetectionJob");
|
||||
}
|
||||
if (oldVersion < lokiV19_REFACTOR1) {
|
||||
db.execSQL("DROP TABLE identities");
|
||||
deleteJobRecords(db, "RetrieveProfileJob");
|
||||
}
|
||||
if (oldVersion < lokiV19_REFACTOR2) {
|
||||
deleteJobRecords(db,
|
||||
"RefreshAttributesJob",
|
||||
"RotateProfileKeyJob",
|
||||
"RefreshUnidentifiedDeliveryAbilityJob",
|
||||
"RotateCertificateJob"
|
||||
);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
if (oldVersion < MIGRATE_PREKEYS_VERSION) {
|
||||
// PreKeyMigrationHelper.cleanUpPreKeys(context);
|
||||
}
|
||||
}
|
||||
|
||||
public SQLiteDatabase getReadableDatabase() {
|
||||
@ -691,4 +271,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all the records related to the job keys specified.
|
||||
* This method should be called once the Signal job class is deleted from the project.
|
||||
*/
|
||||
private static void deleteJobRecords(SQLiteDatabase db, String... jobKeys) {
|
||||
for (String jobKey : jobKeys) {
|
||||
db.execSQL("DELETE FROM job_spec WHERE factory_key = ?", new String[]{jobKey});
|
||||
db.execSQL("DELETE FROM constraint_spec WHERE factory_key = ?", new String[]{jobKey});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,109 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.session.libsignal.libsignal.state.SessionRecord;
|
||||
import org.session.libsignal.libsignal.state.SessionState;
|
||||
import org.session.libsignal.libsignal.state.StorageProtos.SessionStructure;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
class SessionStoreMigrationHelper {
|
||||
|
||||
private static final String TAG = SessionStoreMigrationHelper.class.getSimpleName();
|
||||
|
||||
private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private static final int SINGLE_STATE_VERSION = 1;
|
||||
private static final int ARCHIVE_STATES_VERSION = 2;
|
||||
private static final int PLAINTEXT_VERSION = 3;
|
||||
private static final int CURRENT_VERSION = 3;
|
||||
|
||||
static void migrateSessions(Context context, SQLiteDatabase database) {
|
||||
File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2);
|
||||
|
||||
if (directory.exists()) {
|
||||
File[] sessionFiles = directory.listFiles();
|
||||
|
||||
if (sessionFiles != null) {
|
||||
for (File sessionFile : sessionFiles) {
|
||||
try {
|
||||
String[] parts = sessionFile.getName().split("[.]");
|
||||
Address address = Address.fromSerialized(parts[0]);
|
||||
|
||||
int deviceId;
|
||||
|
||||
if (parts.length > 1) deviceId = Integer.parseInt(parts[1]);
|
||||
else deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
|
||||
FileInputStream in = new FileInputStream(sessionFile);
|
||||
int versionMarker = readInteger(in);
|
||||
|
||||
if (versionMarker > CURRENT_VERSION) {
|
||||
throw new AssertionError("Unknown version: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
byte[] serialized = readBlob(in);
|
||||
in.close();
|
||||
|
||||
if (versionMarker < PLAINTEXT_VERSION) {
|
||||
throw new AssertionError("Not plaintext: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
SessionRecord sessionRecord;
|
||||
|
||||
if (versionMarker == SINGLE_STATE_VERSION) {
|
||||
Log.i(TAG, "Migrating single state version: " + sessionFile.getAbsolutePath());
|
||||
SessionStructure sessionStructure = SessionStructure.parseFrom(serialized);
|
||||
SessionState sessionState = new SessionState(sessionStructure);
|
||||
|
||||
sessionRecord = new SessionRecord(sessionState);
|
||||
} else if (versionMarker >= ARCHIVE_STATES_VERSION) {
|
||||
Log.i(TAG, "Migrating session: " + sessionFile.getAbsolutePath());
|
||||
sessionRecord = new SessionRecord(serialized);
|
||||
} else {
|
||||
throw new AssertionError("Unknown version: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SessionDatabase.ADDRESS, address.serialize());
|
||||
contentValues.put(SessionDatabase.DEVICE, deviceId);
|
||||
contentValues.put(SessionDatabase.RECORD, sessionRecord.serialize());
|
||||
|
||||
database.insert(SessionDatabase.TABLE_NAME, null, contentValues);
|
||||
} catch (NumberFormatException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private static int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.identity;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class IdentityRecordList {
|
||||
|
||||
private static final String TAG = IdentityRecordList.class.getSimpleName();
|
||||
|
||||
private final List<IdentityRecord> identityRecords = new LinkedList<>();
|
||||
|
||||
public void add(Optional<IdentityRecord> identityRecord) {
|
||||
if (identityRecord.isPresent()) {
|
||||
identityRecords.add(identityRecord.get());
|
||||
}
|
||||
}
|
||||
|
||||
public void replaceWith(IdentityRecordList identityRecordList) {
|
||||
identityRecords.clear();
|
||||
identityRecords.addAll(identityRecordList.identityRecords);
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return identityRecords.size() > 0;
|
||||
}
|
||||
|
||||
public boolean isUnverified() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isUntrusted() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (isUntrusted(identityRecord)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<IdentityRecord> getUntrustedRecords() {
|
||||
List<IdentityRecord> results = new LinkedList<>();
|
||||
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (isUntrusted(identityRecord)) {
|
||||
results.add(identityRecord);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public List<Recipient> getUntrustedRecipients(Context context) {
|
||||
List<Recipient> untrusted = new LinkedList<>();
|
||||
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (isUntrusted(identityRecord)) {
|
||||
untrusted.add(Recipient.from(context, identityRecord.getAddress(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return untrusted;
|
||||
}
|
||||
|
||||
public List<IdentityRecord> getUnverifiedRecords() {
|
||||
List<IdentityRecord> results = new LinkedList<>();
|
||||
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
results.add(identityRecord);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public List<Recipient> getUnverifiedRecipients(Context context) {
|
||||
List<Recipient> unverified = new LinkedList<>();
|
||||
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
unverified.add(Recipient.from(context, identityRecord.getAddress(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return unverified;
|
||||
}
|
||||
|
||||
private boolean isUntrusted(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isApprovedNonBlocking() &&
|
||||
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(5);
|
||||
}
|
||||
|
||||
}
|
@ -22,13 +22,8 @@ import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerDownloadJob;
|
||||
@ -36,6 +31,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.push.MessageSenderEventListener;
|
||||
@ -56,18 +52,13 @@ import network.loki.messenger.BuildConfig;
|
||||
AttachmentDownloadJob.class,
|
||||
IncomingMessageObserver.class,
|
||||
PushNotificationReceiveJob.class,
|
||||
RefreshAttributesJob.class,
|
||||
RequestGroupInfoJob.class,
|
||||
PushGroupUpdateJob.class,
|
||||
AvatarDownloadJob.class,
|
||||
RetrieveProfileJob.class,
|
||||
RetrieveProfileAvatarJob.class,
|
||||
SendReadReceiptJob.class,
|
||||
AppProtectionPreferenceFragment.class,
|
||||
RotateCertificateJob.class,
|
||||
SendDeliveryReceiptJob.class,
|
||||
RotateProfileKeyJob.class,
|
||||
RefreshUnidentifiedDeliveryAbilityJob.class,
|
||||
TypingSendJob.class,
|
||||
AttachmentUploadJob.class,
|
||||
PushDecryptJob.class,
|
||||
@ -94,17 +85,6 @@ public class SignalCommunicationModule {
|
||||
|
||||
}
|
||||
|
||||
@Provides
|
||||
synchronized SignalServiceAccountManager provideSignalAccountManager() {
|
||||
if (this.accountManager == null) {
|
||||
this.accountManager = new SignalServiceAccountManager(networkAccess.getConfiguration(context),
|
||||
new DynamicCredentialsProvider(context),
|
||||
BuildConfig.USER_AGENT);
|
||||
}
|
||||
|
||||
return this.accountManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
|
||||
if (this.messageSender == null) {
|
||||
@ -121,8 +101,8 @@ public class SignalCommunicationModule {
|
||||
DatabaseFactory.getSSKDatabase(context),
|
||||
DatabaseFactory.getLokiThreadDatabase(context),
|
||||
DatabaseFactory.getLokiMessageDatabase(context),
|
||||
// DatabaseFactory.getLokiPreKeyBundleDatabase(context),
|
||||
null,
|
||||
null, // DatabaseFactory.getLokiPreKeyBundleDatabase(context)
|
||||
new SessionProtocolImpl(context),
|
||||
new SessionResetImplementation(context),
|
||||
DatabaseFactory.getLokiUserDatabase(context),
|
||||
DatabaseFactory.getGroupDatabase(context),
|
||||
|
@ -21,7 +21,6 @@ import com.google.android.material.tabs.TabLayout;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@ -42,19 +41,12 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
||||
public static final String EXTRA_WIDTH = "extra_width";
|
||||
public static final String EXTRA_HEIGHT = "extra_height";
|
||||
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private GiphyGifFragment gifFragment;
|
||||
private GiphyStickerFragment stickerFragment;
|
||||
private boolean forMms;
|
||||
|
||||
private GiphyAdapter.GiphyViewHolder finishingImage;
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.giphy_activity);
|
||||
|
@ -1,85 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
//TODO AC: Looks like we don't use it anymore. Make sure it's abandoned and delete.
|
||||
public class WorkManagerFactoryMappings {
|
||||
|
||||
private static final Map<String, String> FACTORY_MAP = new HashMap<String, String>() {{
|
||||
put(AttachmentDownloadJob.class.getName(), AttachmentDownloadJob.KEY);
|
||||
put(AttachmentUploadJob.class.getName(), AttachmentUploadJob.KEY);
|
||||
put(AvatarDownloadJob.class.getName(), AvatarDownloadJob.KEY);
|
||||
put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY);
|
||||
put(LocalBackupJob.class.getName(), LocalBackupJob.KEY);
|
||||
put(MmsDownloadJob.class.getName(), MmsDownloadJob.KEY);
|
||||
put(MmsReceiveJob.class.getName(), MmsReceiveJob.KEY);
|
||||
put(MmsSendJob.class.getName(), MmsSendJob.KEY);
|
||||
put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY);
|
||||
put(PushContentReceiveJob.class.getName(), PushContentReceiveJob.KEY);
|
||||
put(PushDecryptJob.class.getName(), PushDecryptJob.KEY);
|
||||
put(PushGroupSendJob.class.getName(), PushGroupSendJob.KEY);
|
||||
put(PushGroupUpdateJob.class.getName(), PushGroupUpdateJob.KEY);
|
||||
put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY);
|
||||
put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY);
|
||||
put(PushTextSendJob.class.getName(), PushTextSendJob.KEY);
|
||||
put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY);
|
||||
put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY);
|
||||
put(RequestGroupInfoJob.class.getName(), RequestGroupInfoJob.KEY);
|
||||
put(RetrieveProfileAvatarJob.class.getName(), RetrieveProfileAvatarJob.KEY);
|
||||
put(RetrieveProfileJob.class.getName(), RetrieveProfileJob.KEY);
|
||||
put(RotateCertificateJob.class.getName(), RotateCertificateJob.KEY);
|
||||
put(RotateProfileKeyJob.class.getName(), RotateProfileKeyJob.KEY);
|
||||
put(SendDeliveryReceiptJob.class.getName(), SendDeliveryReceiptJob.KEY);
|
||||
put(SendReadReceiptJob.class.getName(), SendReadReceiptJob.KEY);
|
||||
put(SmsReceiveJob.class.getName(), SmsReceiveJob.KEY);
|
||||
put(SmsSendJob.class.getName(), SmsSendJob.KEY);
|
||||
put(SmsSentJob.class.getName(), SmsSentJob.KEY);
|
||||
put(TrimThreadJob.class.getName(), TrimThreadJob.KEY);
|
||||
put(TypingSendJob.class.getName(), TypingSendJob.KEY);
|
||||
put(UpdateApkJob.class.getName(), UpdateApkJob.KEY);
|
||||
put(PrepareAttachmentAudioExtrasJob.class.getName(), PrepareAttachmentAudioExtrasJob.KEY);
|
||||
put(ResetThreadSessionJob.class.getName(), ResetThreadSessionJob.KEY);
|
||||
}};
|
||||
|
||||
public static @Nullable String getFactoryKey(@NonNull String workManagerClass) {
|
||||
return FACTORY_MAP.get(workManagerClass);
|
||||
}
|
||||
}
|
@ -6,6 +6,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
@ -23,19 +30,12 @@ import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "AttachmentDownloadJob";
|
||||
@ -48,8 +48,6 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
|
||||
private static final String KEY_MANUAL = "part_manual";
|
||||
|
||||
@Inject SignalServiceMessageReceiver messageReceiver;
|
||||
|
||||
private long messageId;
|
||||
private long partRowId;
|
||||
private long partUniqueId;
|
||||
@ -166,7 +164,21 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||
attachmentFile = createTempFile();
|
||||
|
||||
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
|
||||
InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)));
|
||||
// InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)));
|
||||
|
||||
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
||||
|
||||
DownloadUtilities.downloadFile(attachmentFile, pointer.getUrl(), MAX_ATTACHMENT_SIZE, (total, progress) -> {
|
||||
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
|
||||
});
|
||||
|
||||
final InputStream stream;
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (!pointer.getDigest().isPresent()) {
|
||||
stream = new FileInputStream(attachmentFile);
|
||||
} else {
|
||||
stream = AttachmentCipherInputStream.createForAttachment(attachmentFile, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
||||
}
|
||||
|
||||
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
|
||||
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
|
||||
|
@ -44,13 +44,8 @@ public final class JobManagerFactories {
|
||||
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
|
||||
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
||||
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||
put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory());
|
||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory(application));
|
||||
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||
put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory());
|
||||
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
|
||||
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory());
|
||||
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
|
||||
|
@ -5,7 +5,6 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -64,6 +63,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
@ -92,7 +92,6 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
@ -107,10 +106,7 @@ import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.ReadMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.RequestMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.session.libsignal.service.api.messages.multidevice.VerifiedMessage;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
@ -249,7 +245,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
||||
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
|
||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
|
||||
SignalServiceContent content = cipher.decrypt(envelope);
|
||||
|
||||
@ -479,10 +475,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
|
||||
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
|
||||
}
|
||||
|
||||
private void handleSynchronizeStickerPackOperation(@NonNull List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.service.api.SignalServiceAccountManager;
|
||||
import org.session.libsignal.service.api.push.exceptions.NetworkFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RefreshAttributesJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "RefreshAttributesJob";
|
||||
|
||||
private static final String TAG = RefreshAttributesJob.class.getSimpleName();
|
||||
|
||||
@Inject SignalServiceAccountManager signalAccountManager;
|
||||
|
||||
public RefreshAttributesJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("RefreshAttributesJob")
|
||||
.build());
|
||||
}
|
||||
|
||||
private RefreshAttributesJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
|
||||
signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages, "",
|
||||
unidentifiedAccessKey, universalUnidentifiedAccess);
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof NetworkFailureException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Failed to update account attributes!");
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<RefreshAttributesJob> {
|
||||
@Override
|
||||
public @NonNull RefreshAttributesJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
||||
return new RefreshAttributesJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessagePipe;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.crypto.ProfileCipher;
|
||||
import org.session.libsignal.service.api.profiles.SignalServiceProfile;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RefreshUnidentifiedDeliveryAbilityJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "RefreshUnidentifiedDeliveryAbilityJob";
|
||||
|
||||
private static final String TAG = RefreshUnidentifiedDeliveryAbilityJob.class.getSimpleName();
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
public RefreshUnidentifiedDeliveryAbilityJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
.build());
|
||||
}
|
||||
|
||||
private RefreshUnidentifiedDeliveryAbilityJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws Exception {
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
|
||||
SignalServiceProfile profile = retrieveProfile(TextSecurePreferences.getLocalNumber(context));
|
||||
|
||||
boolean enabled = profile.getUnidentifiedAccess() != null && isValidVerifier(profileKey, profile.getUnidentifiedAccess());
|
||||
|
||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, enabled);
|
||||
Log.i(TAG, "Set UD status to: " + enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return exception instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveProfile(@NonNull String number) throws IOException {
|
||||
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(new SignalServiceAddress(number), Optional.absent());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return receiver.retrieveProfile(new SignalServiceAddress(number), Optional.absent());
|
||||
}
|
||||
|
||||
private boolean isValidVerifier(@NonNull byte[] profileKey, @NonNull String verifier) {
|
||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||
try {
|
||||
return profileCipher.verifyUnidentifiedAccess(Base64.decode(verifier));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<RefreshUnidentifiedDeliveryAbilityJob> {
|
||||
@Override
|
||||
public @NonNull RefreshUnidentifiedDeliveryAbilityJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RefreshUnidentifiedDeliveryAbilityJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessagePipe;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.crypto.InvalidCiphertextException;
|
||||
import org.session.libsignal.service.api.crypto.ProfileCipher;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair;
|
||||
import org.session.libsignal.service.api.profiles.SignalServiceProfile;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.session.libsignal.service.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RetrieveProfileJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "RetrieveProfileJob";
|
||||
|
||||
private static final String TAG = RetrieveProfileJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
private Recipient recipient;
|
||||
|
||||
public RetrieveProfileJob(@NonNull Recipient recipient) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(3)
|
||||
.build(),
|
||||
recipient);
|
||||
}
|
||||
|
||||
private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient) {
|
||||
super(parameters);
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_ADDRESS, recipient.getAddress().serialize()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, InvalidKeyException {
|
||||
// Loki - Do nothing
|
||||
/*
|
||||
try {
|
||||
if (recipient.isGroupRecipient()) handleGroupRecipient(recipient);
|
||||
else handleIndividualRecipient(recipient);
|
||||
} catch (InvalidNumberException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {}
|
||||
|
||||
private void handleIndividualRecipient(Recipient recipient)
|
||||
throws IOException, InvalidKeyException, InvalidNumberException
|
||||
{
|
||||
String number = recipient.getAddress().toPhoneString();
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(recipient);
|
||||
|
||||
SignalServiceProfile profile;
|
||||
|
||||
try {
|
||||
profile = retrieveProfile(number, unidentifiedAccess);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
profile = retrieveProfile(number, Optional.absent());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
setIdentityKey(recipient, profile.getIdentityKey());
|
||||
setProfileName(recipient, profile.getName());
|
||||
setProfileAvatar(recipient, profile.getAvatar());
|
||||
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||
}
|
||||
|
||||
private void handleGroupRecipient(Recipient group)
|
||||
throws IOException, InvalidKeyException, InvalidNumberException
|
||||
{
|
||||
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.getAddress().toGroupString(), false);
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
handleIndividualRecipient(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveProfile(@NonNull String number, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
|
||||
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
|
||||
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe
|
||||
: authPipe;
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(new SignalServiceAddress(number), unidentifiedAccess);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return receiver.retrieveProfile(new SignalServiceAddress(number), unidentifiedAccess);
|
||||
}
|
||||
|
||||
private void setIdentityKey(Recipient recipient, String identityKeyValue) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(identityKeyValue)) {
|
||||
Log.w(TAG, "Identity key is missing on profile!");
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyValue), 0);
|
||||
|
||||
if (!DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipient.getAddress())
|
||||
.isPresent())
|
||||
{
|
||||
Log.w(TAG, "Still first use...");
|
||||
return;
|
||||
}
|
||||
|
||||
IdentityUtil.saveIdentity(context, recipient.getAddress().toPhoneString(), identityKey);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
|
||||
if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) {
|
||||
Log.i(TAG, "Marking recipient UD status as unrestricted.");
|
||||
recipientDatabase.setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED);
|
||||
} else if (profileKey == null || unidentifiedAccessVerifier == null) {
|
||||
Log.i(TAG, "Marking recipient UD status as disabled.");
|
||||
recipientDatabase.setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED);
|
||||
} else {
|
||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||
boolean verifiedUnidentifiedAccess;
|
||||
|
||||
try {
|
||||
verifiedUnidentifiedAccess = profileCipher.verifyUnidentifiedAccess(Base64.decode(unidentifiedAccessVerifier));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
verifiedUnidentifiedAccess = false;
|
||||
}
|
||||
|
||||
UnidentifiedAccessMode mode = verifiedUnidentifiedAccess ? UnidentifiedAccessMode.ENABLED : UnidentifiedAccessMode.DISABLED;
|
||||
Log.i(TAG, "Marking recipient UD status as " + mode.name() + " after verification.");
|
||||
recipientDatabase.setUnidentifiedAccessMode(recipient, mode);
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileName(Recipient recipient, String profileName) {
|
||||
try {
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
if (profileKey == null) return;
|
||||
|
||||
String plaintextProfileName = null;
|
||||
|
||||
if (profileName != null) {
|
||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||
plaintextProfileName = new String(profileCipher.decryptName(Base64.decode(profileName)));
|
||||
}
|
||||
|
||||
if (!Util.equals(plaintextProfileName, recipient.getProfileName())) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient, plaintextProfileName);
|
||||
}
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileAvatar(Recipient recipient, String profileAvatar) {
|
||||
if (recipient.getProfileKey() == null) return;
|
||||
|
||||
if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RetrieveProfileAvatarJob(recipient, profileAvatar));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Recipient recipient) {
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RetrieveProfileJob> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RetrieveProfileJob(parameters, Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.session.libsignal.service.api.SignalServiceAccountManager;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class RotateCertificateJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "RotateCertificateJob";
|
||||
|
||||
private static final String TAG = RotateCertificateJob.class.getSimpleName();
|
||||
|
||||
@Inject SignalServiceAccountManager accountManager;
|
||||
|
||||
public RotateCertificateJob(Context context) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("__ROTATE_SENDER_CERTIFICATE__")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
setContext(context);
|
||||
}
|
||||
|
||||
private RotateCertificateJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
// Loki - Do nothing
|
||||
/*
|
||||
synchronized (RotateCertificateJob.class) {
|
||||
byte[] certificate = accountManager.getSenderCertificate();
|
||||
TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Failed to rotate sender certificate!");
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RotateCertificateJob> {
|
||||
@Override
|
||||
public @NonNull RotateCertificateJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RotateCertificateJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.session.libsignal.service.api.SignalServiceAccountManager;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.service.api.util.StreamDetails;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RotateProfileKeyJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static String KEY = "RotateProfileKeyJob";
|
||||
|
||||
@Inject SignalServiceAccountManager accountManager;
|
||||
|
||||
public RotateProfileKeyJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("__ROTATE_PROFILE_KEY__")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(25)
|
||||
.setMaxInstances(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
private RotateProfileKeyJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws Exception {
|
||||
byte[] profileKey = ProfileKeyUtil.rotateProfileKey(context);
|
||||
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context));
|
||||
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RefreshAttributesJob());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return exception instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
private @Nullable StreamDetails getProfileAvatar() {
|
||||
try {
|
||||
Address localAddress = Address.fromSerialized(TextSecurePreferences.getLocalNumber(context));
|
||||
File avatarFile = AvatarHelper.getAvatarFile(context, localAddress);
|
||||
|
||||
if (avatarFile.exists()) {
|
||||
return new StreamDetails(new FileInputStream(avatarFile), "image/jpeg", avatarFile.length());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RotateProfileKeyJob> {
|
||||
@Override
|
||||
public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RotateProfileKeyJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -189,7 +189,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica
|
||||
TextSecurePreferences.setRestorationTime(context, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(context, true)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(context)
|
||||
TextSecurePreferences.setHasSeenLightThemeIntroSheet(context)
|
||||
val application = ApplicationContext.getInstance(context)
|
||||
|
@ -204,7 +204,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
// Clear all data if this is a secondary device
|
||||
if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) {
|
||||
TextSecurePreferences.setWasUnlinked(this, true)
|
||||
ApplicationContext.getInstance(this).clearData()
|
||||
ApplicationContext.getInstance(this).clearAllData()
|
||||
}
|
||||
|
||||
// Perform chat sessions reset if requested (usually happens after backup restoration).
|
||||
@ -220,36 +220,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
if (hasViewedSeed || !isMasterDevice) {
|
||||
seedReminderView.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Multi device removal sheet
|
||||
if (!TextSecurePreferences.getHasSeenMultiDeviceRemovalSheet(this)) {
|
||||
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(this)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getDeviceLinks(userPublicKey)
|
||||
if (deviceLinks.isNotEmpty()) {
|
||||
val bottomSheet = MultiDeviceRemovalBottomSheet()
|
||||
bottomSheet.onOKTapped = {
|
||||
bottomSheet.dismiss()
|
||||
}
|
||||
bottomSheet.onLinkTapped = {
|
||||
bottomSheet.dismiss()
|
||||
val url = "https://getsession.org/faq"
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
}
|
||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Light theme introduction sheet
|
||||
if (!TextSecurePreferences.hasSeenLightThemeIntroSheet(this) &&
|
||||
UiModeUtilities.isDayUiMode(this)) {
|
||||
TextSecurePreferences.setHasSeenLightThemeIntroSheet(this)
|
||||
val bottomSheet = LightThemeFeatureIntroBottomSheet()
|
||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
@ -73,6 +74,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
try {
|
||||
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
|
||||
} catch (e: Exception) {
|
||||
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoader()
|
||||
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
|
@ -2,15 +2,18 @@ package org.thoughtcrime.securesms.loki.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_landing.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.views.FakeChatView
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
class LandingActivity : BaseActionBarActivity() {
|
||||
|
||||
@ -18,13 +21,26 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_landing)
|
||||
setUpActionBarSessionLogo(true)
|
||||
fakeChatView.startAnimating()
|
||||
registerButton.setOnClickListener { register() }
|
||||
restoreButton.setOnClickListener { restoreFromRecoveryPhrase() }
|
||||
restoreBackupButton.setOnClickListener { restoreFromBackup() }
|
||||
findViewById<FakeChatView>(R.id.fakeChatView).startAnimating()
|
||||
|
||||
findViewById<View>(R.id.registerButton).setOnClickListener { register() }
|
||||
findViewById<View>(R.id.restoreButton).setOnClickListener { restoreFromRecoveryPhrase() }
|
||||
findViewById<View>(R.id.restoreBackupButton).setOnClickListener { restoreFromBackup() }
|
||||
|
||||
if (TextSecurePreferences.getWasUnlinked(this)) {
|
||||
Toast.makeText(this, R.string.activity_landing_device_unlinked_dialog_title, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
// Setup essentials for a new user.
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this)
|
||||
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode())
|
||||
TextSecurePreferences.setPasswordDisabled(this, true)
|
||||
TextSecurePreferences.setReadReceiptsEnabled(this, true)
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(this, true)
|
||||
|
||||
//AC: This is a temporary workaround to trick the old code that the screen is unlocked.
|
||||
KeyCachingService.setMasterSecret(applicationContext, Object())
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
@ -46,7 +62,6 @@ class LandingActivity : BaseActionBarActivity() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.LOKI_SEED)
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.stopPolling()
|
||||
}
|
||||
|
@ -151,7 +151,6 @@ class PNModeActivity : BaseActionBarActivity() {
|
||||
return
|
||||
}
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
||||
TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(this)
|
||||
TextSecurePreferences.setHasSeenLightThemeIntroSheet(this)
|
||||
|
@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
@ -72,9 +71,6 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
|
||||
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
|
||||
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
|
@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
@ -109,9 +108,6 @@ class RegisterActivity : BaseActionBarActivity() {
|
||||
val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
|
||||
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this, 0)
|
||||
TextSecurePreferences.setHasViewedSeed(this, false)
|
||||
|
@ -29,7 +29,7 @@ class ClosedGroupPoller private constructor(private val context: Context, privat
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollInterval: Long = 2 * 1000
|
||||
private val pollInterval: Long = 4 * 1000
|
||||
|
||||
public lateinit var shared: ClosedGroupPoller
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Box
|
||||
import com.goterl.lazycode.lazysodium.interfaces.Sign
|
||||
import org.session.libsignal.libsignal.util.Hex
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
|
||||
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't sign message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.SigningFailed
|
||||
}
|
||||
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
|
||||
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.EncryptionFailed
|
||||
}
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
override fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String> {
|
||||
val ciphertext = envelope.content ?: throw SessionProtocol.Exception.NoData
|
||||
val recipientX25519PrivateKey: ByteArray
|
||||
val recipientX25519PublicKey: ByteArray
|
||||
when (envelope.type) {
|
||||
UNIDENTIFIED_SENDER_VALUE -> {
|
||||
recipientX25519PrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
recipientX25519PublicKey = Hex.fromStringCondensed(TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded())
|
||||
}
|
||||
CLOSED_GROUP_CIPHERTEXT_VALUE -> {
|
||||
val hexEncodedGroupPublicKey = envelope.source
|
||||
val sskDB = DatabaseFactory.getSSKDatabase(context)
|
||||
if (!sskDB.isSSKBasedClosedGroup(hexEncodedGroupPublicKey)) { throw SessionProtocol.Exception.InvalidGroupPublicKey }
|
||||
val hexEncodedGroupPrivateKey = sskDB.getClosedGroupPrivateKey(hexEncodedGroupPublicKey) ?: throw SessionProtocol.Exception.NoGroupPrivateKey
|
||||
recipientX25519PrivateKey = Hex.fromStringCondensed(hexEncodedGroupPrivateKey)
|
||||
recipientX25519PublicKey = Hex.fromStringCondensed(hexEncodedGroupPublicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
val signatureSize = Sign.BYTES
|
||||
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES
|
||||
|
||||
// 1. ) Decrypt the message
|
||||
val plaintextWithMetadata = ByteArray(ciphertext.size - Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't decrypt message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.DecryptionFailed
|
||||
}
|
||||
if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw SessionProtocol.Exception.DecryptionFailed }
|
||||
// 2. ) Get the message parts
|
||||
val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size)
|
||||
val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize)
|
||||
val plaintext = plaintextWithMetadata.sliceArray(0 until plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize))
|
||||
// 3. ) Verify the signature
|
||||
val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey)
|
||||
try {
|
||||
val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey)
|
||||
if (!isValid) { throw SessionProtocol.Exception.InvalidSignature }
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't verify message signature due to error: $exception.")
|
||||
throw SessionProtocol.Exception.InvalidSignature
|
||||
}
|
||||
// 4. ) Get the sender's X25519 public key
|
||||
val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
|
||||
sodium.convertPublicKeyEd25519ToCurve25519(senderX25519PublicKey, senderED25519PublicKey)
|
||||
|
||||
return Pair(plaintext, "05" + senderX25519PublicKey.toHexString())
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.*
|
||||
|
||||
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
|
||||
|
||||
@ -375,6 +377,16 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
return database.delete(openGroupProfilePictureTable, "$publicChatID = ?", arrayOf(index)) > 0
|
||||
}
|
||||
|
||||
override fun getLastSnodePoolRefreshDate(): Date? {
|
||||
val time = TextSecurePreferences.getLastSnodePoolRefreshDate(context)
|
||||
if (time <= 0) { return null }
|
||||
return Date(time)
|
||||
}
|
||||
|
||||
override fun setLastSnodePoolRefreshDate(date: Date) {
|
||||
TextSecurePreferences.setLastSnodePoolRefreshDate(context, date)
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
|
||||
return setOf()
|
||||
|
@ -10,12 +10,13 @@ import android.view.LayoutInflater
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
val contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_clear_all_data, null)
|
||||
val builder = AlertDialog.Builder(requireContext())
|
||||
val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
builder.setView(contentView)
|
||||
@ -25,6 +26,19 @@ class ClearAllDataDialog : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun clearAllData() {
|
||||
ApplicationContext.getInstance(context).clearData()
|
||||
if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
|
||||
ApplicationContext.getInstance(context).clearAllData()
|
||||
} else {
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
val message = "We’ve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID."
|
||||
dialog.setMessage(message)
|
||||
dialog.setPositiveButton("Yes") { _, _ ->
|
||||
ApplicationContext.getInstance(context).clearAllData()
|
||||
}
|
||||
dialog.setNegativeButton("Cancel") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
dialog.create().show()
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.utilities
|
||||
import android.content.Context
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.utils.Key
|
||||
import com.goterl.lazycode.lazysodium.utils.KeyPair
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
@ -15,12 +16,6 @@ object KeyPairUtilities {
|
||||
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
data class KeyPairGenerationResult(
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
)
|
||||
|
||||
fun generate(): KeyPairGenerationResult {
|
||||
val seed = sodium.randomBytesBuf(16)
|
||||
try {
|
||||
@ -45,4 +40,22 @@ object KeyPairUtilities {
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
|
||||
}
|
||||
|
||||
fun hasV2KeyPair(context: Context): Boolean {
|
||||
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
|
||||
}
|
||||
|
||||
fun getUserED25519KeyPair(context: Context): KeyPair? {
|
||||
val hexEncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
|
||||
val hexEncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
|
||||
val ed25519PublicKey = Key.fromBase64String(hexEncodedED25519PublicKey)
|
||||
val ed25519SecretKey = Key.fromBase64String(hexEncodedED25519SecretKey)
|
||||
return KeyPair(ed25519PublicKey, ed25519SecretKey)
|
||||
}
|
||||
|
||||
data class KeyPairGenerationResult(
|
||||
val seed: ByteArray,
|
||||
val ed25519KeyPair: KeyPair,
|
||||
val x25519KeyPair: ECKeyPair
|
||||
)
|
||||
}
|
@ -5,17 +5,13 @@ import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseChangeActivity;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@ -27,8 +23,6 @@ import network.loki.messenger.R;
|
||||
|
||||
public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment implements InjectableType {
|
||||
|
||||
private CheckBoxPreference disablePassphrase;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
@ -39,17 +33,12 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
|
||||
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
|
||||
|
||||
this.findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF).setOnPreferenceClickListener(new ChangePassphraseClickListener());
|
||||
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF).setOnPreferenceClickListener(new PassphraseIntervalClickListener());
|
||||
this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener());
|
||||
this.findPreference(TextSecurePreferences.TYPING_INDICATORS).setOnPreferenceChangeListener(new TypingIndicatorsToggleListener());
|
||||
this.findPreference(TextSecurePreferences.LINK_PREVIEWS).setOnPreferenceChangeListener(new LinkPreviewToggleListener());
|
||||
disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener());
|
||||
|
||||
initializeVisibility();
|
||||
}
|
||||
@ -62,16 +51,9 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!TextSecurePreferences.isPasswordDisabled(getContext())) initializePassphraseTimeoutSummary();
|
||||
else initializeScreenLockTimeoutSummary();
|
||||
|
||||
disablePassphrase.setChecked(!TextSecurePreferences.isPasswordDisabled(getActivity()));
|
||||
}
|
||||
|
||||
private void initializePassphraseTimeoutSummary() {
|
||||
int timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(getActivity());
|
||||
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF)
|
||||
.setSummary(getResources().getQuantityString(R.plurals.AppProtectionPreferenceFragment_minutes, timeoutMinutes, timeoutMinutes));
|
||||
if (TextSecurePreferences.isPasswordDisabled(getContext())) {
|
||||
initializeScreenLockTimeoutSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeScreenLockTimeoutSummary() {
|
||||
@ -87,11 +69,6 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
|
||||
private void initializeVisibility() {
|
||||
if (TextSecurePreferences.isPasswordDisabled(getContext())) {
|
||||
findPreference("pref_enable_passphrase_temporary").setVisible(false);
|
||||
findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF).setVisible(false);
|
||||
findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF).setVisible(false);
|
||||
findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF).setVisible(false);
|
||||
|
||||
KeyguardManager keyguardManager = (KeyguardManager)getContext().getSystemService(Context.KEYGUARD_SERVICE);
|
||||
if (!keyguardManager.isKeyguardSecure()) {
|
||||
((SwitchPreferenceCompat)findPreference(TextSecurePreferences.SCREEN_LOCK)).setChecked(false);
|
||||
@ -178,90 +155,4 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
|
||||
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
|
||||
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
|
||||
// if (TextSecurePreferences.isRegistrationtLockEnabled(context)) {
|
||||
// return context.getString(privacySummaryResId, offRes, onRes);
|
||||
// } else {
|
||||
return context.getString(privacySummaryResId, offRes, offRes);
|
||||
// }
|
||||
} else {
|
||||
// if (TextSecurePreferences.isRegistrationtLockEnabled(context)) {
|
||||
// return context.getString(privacySummaryResId, onRes, onRes);
|
||||
// } else {
|
||||
return context.getString(privacySummaryResId, onRes, offRes);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (MasterSecretUtil.isPassphraseInitialized(getActivity())) {
|
||||
startActivity(new Intent(getActivity(), PassphraseChangeActivity.class));
|
||||
} else {
|
||||
Toast.makeText(getActivity(),
|
||||
R.string.ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class PassphraseIntervalClickListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
new TimeDurationPickerDialog(getContext(), (view, duration) -> {
|
||||
int timeoutMinutes = Math.max((int)TimeUnit.MILLISECONDS.toMinutes(duration), 1);
|
||||
|
||||
TextSecurePreferences.setPassphraseTimeoutInterval(getActivity(), timeoutMinutes);
|
||||
|
||||
initializePassphraseTimeoutSummary();
|
||||
|
||||
}, 0).show();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class DisablePassphraseClickListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(final Preference preference, Object newValue) {
|
||||
if (((CheckBoxPreference)preference).isChecked()) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase);
|
||||
builder.setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setPositiveButton(R.string.ApplicationPreferencesActivity_disable, (dialog, which) -> {
|
||||
MasterSecretUtil.changeMasterSecretPassphrase(getActivity(),
|
||||
KeyCachingService.getMasterSecret(getContext()),
|
||||
MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
|
||||
TextSecurePreferences.setPasswordDisabled(getActivity(), true);
|
||||
((CheckBoxPreference)preference).setChecked(false);
|
||||
|
||||
Intent intent = new Intent(getActivity(), KeyCachingService.class);
|
||||
intent.setAction(KeyCachingService.DISABLE_ACTION);
|
||||
getActivity().startService(intent);
|
||||
|
||||
initializeVisibility();
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
} else {
|
||||
Intent intent = new Intent(getActivity(), PassphraseChangeActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,10 @@ import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
@ -34,13 +36,9 @@ import androidx.core.app.NotificationCompat;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
|
||||
import org.thoughtcrime.securesms.DummyActivity;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@ -53,7 +51,8 @@ import network.loki.messenger.R;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
//TODO AC: This service does only serve one purpose now - to track the screen lock state and handle the timer.
|
||||
// We need to refactor it and cleanup from all the old Signal code.
|
||||
public class KeyCachingService extends Service {
|
||||
|
||||
private static final String TAG = KeyCachingService.class.getSimpleName();
|
||||
@ -61,19 +60,41 @@ public class KeyCachingService extends Service {
|
||||
public static final int SERVICE_RUNNING_ID = 4141;
|
||||
|
||||
public static final String KEY_PERMISSION = "network.loki.messenger.ACCESS_SESSION_SECRETS";
|
||||
public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT";
|
||||
// public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT";
|
||||
public static final String CLEAR_KEY_EVENT = "org.thoughtcrime.securesms.service.action.CLEAR_KEY_EVENT";
|
||||
public static final String LOCK_TOGGLED_EVENT = "org.thoughtcrime.securesms.service.action.LOCK_ENABLED_EVENT";
|
||||
private static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT";
|
||||
public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY";
|
||||
public static final String DISABLE_ACTION = "org.thoughtcrime.securesms.service.action.DISABLE";
|
||||
public static final String LOCALE_CHANGE_EVENT = "org.thoughtcrime.securesms.service.action.LOCALE_CHANGE_EVENT";
|
||||
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
// public static final String DISABLE_ACTION = "org.thoughtcrime.securesms.service.action.DISABLE";
|
||||
|
||||
private final IBinder binder = new KeySetBinder();
|
||||
|
||||
private static MasterSecret masterSecret;
|
||||
// AC: This is a temporal drop off replacement for the refactoring time being.
|
||||
// This field only indicates if the app was unlocked or not (null means locked).
|
||||
private static Object masterSecret;
|
||||
|
||||
/**
|
||||
* A temporal utility method to quickly call {@link KeyCachingService#setMasterSecret(Object)}
|
||||
* without explicitly binding to the service.
|
||||
*/
|
||||
public static void setMasterSecret(Context context, Object masterSecret) {
|
||||
// Start and bind to the KeyCachingService instance.
|
||||
Intent bindIntent = new Intent(context, KeyCachingService.class);
|
||||
context.startService(bindIntent);
|
||||
context.bindService(bindIntent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||
KeyCachingService service = ((KeySetBinder) binder).getService();
|
||||
service.setMasterSecret(masterSecret);
|
||||
context.unbindService(this);
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
|
||||
}
|
||||
|
||||
public KeyCachingService() {}
|
||||
|
||||
@ -81,18 +102,6 @@ public class KeyCachingService extends Service {
|
||||
return getMasterSecret(context) == null;
|
||||
}
|
||||
|
||||
public static synchronized @Nullable MasterSecret getMasterSecret(Context context) {
|
||||
if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
|
||||
try {
|
||||
return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w("KeyCachingService", e);
|
||||
}
|
||||
}
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static void onAppForegrounded(@NonNull Context context) {
|
||||
ServiceUtil.getAlarmManager(context).cancel(buildExpirationPendingIntent(context));
|
||||
}
|
||||
@ -101,13 +110,25 @@ public class KeyCachingService extends Service {
|
||||
startTimeoutIfAppropriate(context);
|
||||
}
|
||||
|
||||
public static synchronized @Nullable Object getMasterSecret(Context context) {
|
||||
// if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
|
||||
// try {
|
||||
// return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
// } catch (InvalidPassphraseException e) {
|
||||
// Log.w("KeyCachingService", e);
|
||||
// }
|
||||
// }
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public void setMasterSecret(final MasterSecret masterSecret) {
|
||||
public void setMasterSecret(final Object masterSecret) {
|
||||
synchronized (KeyCachingService.class) {
|
||||
KeyCachingService.masterSecret = masterSecret;
|
||||
|
||||
foregroundService();
|
||||
broadcastNewSecret();
|
||||
// broadcastNewSecret();
|
||||
startTimeoutIfAppropriate(this);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@ -131,8 +152,7 @@ public class KeyCachingService extends Service {
|
||||
switch (intent.getAction()) {
|
||||
case CLEAR_KEY_ACTION: handleClearKey(); break;
|
||||
case PASSPHRASE_EXPIRED_EVENT: handleClearKey(); break;
|
||||
case DISABLE_ACTION: handleDisableService(); break;
|
||||
case LOCALE_CHANGE_EVENT: handleLocaleChanged(); break;
|
||||
// case DISABLE_ACTION: handleDisableService(); break;
|
||||
case LOCK_TOGGLED_EVENT: handleLockToggled(); break;
|
||||
}
|
||||
}
|
||||
@ -146,12 +166,12 @@ public class KeyCachingService extends Service {
|
||||
super.onCreate();
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
try {
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w("KeyCachingService", e);
|
||||
}
|
||||
// try {
|
||||
// MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(new Object());
|
||||
// } catch (InvalidPassphraseException e) {
|
||||
// Log.w("KeyCachingService", e);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,12 +216,12 @@ public class KeyCachingService extends Service {
|
||||
private void handleLockToggled() {
|
||||
stopForeground(true);
|
||||
|
||||
try {
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
// try {
|
||||
// MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
// } catch (InvalidPassphraseException e) {
|
||||
// Log.w(TAG, e);
|
||||
// }
|
||||
}
|
||||
|
||||
private void handleDisableService() {
|
||||
@ -212,11 +232,6 @@ public class KeyCachingService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLocaleChanged() {
|
||||
dynamicLanguage.updateServiceLocale(this);
|
||||
foregroundService();
|
||||
}
|
||||
|
||||
private static void startTimeoutIfAppropriate(@NonNull Context context) {
|
||||
boolean appVisible = ApplicationContext.getInstance(context).isAppVisible();
|
||||
boolean secretSet = KeyCachingService.masterSecret != null;
|
||||
@ -268,14 +283,14 @@ public class KeyCachingService extends Service {
|
||||
startForeground(SERVICE_RUNNING_ID, builder.build());
|
||||
}
|
||||
|
||||
private void broadcastNewSecret() {
|
||||
Log.i(TAG, "Broadcasting new secret...");
|
||||
|
||||
Intent intent = new Intent(NEW_KEY_EVENT);
|
||||
intent.setPackage(getApplicationContext().getPackageName());
|
||||
|
||||
sendBroadcast(intent, KEY_PERMISSION);
|
||||
}
|
||||
// private void broadcastNewSecret() {
|
||||
// Log.i(TAG, "Broadcasting new secret...");
|
||||
//
|
||||
// Intent intent = new Intent(NEW_KEY_EVENT);
|
||||
// intent.setPackage(getApplicationContext().getPackageName());
|
||||
//
|
||||
// sendBroadcast(intent, KEY_PERMISSION);
|
||||
// }
|
||||
|
||||
private PendingIntent buildLockIntent() {
|
||||
Intent intent = new Intent(this, KeyCachingService.class);
|
||||
|
@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RotateSenderCertificateListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUnidentifiedAccessCertificateRotationTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RotateCertificateJob(context));
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new RotateSenderCertificateListener().onReceive(context, new Intent());
|
||||
}
|
||||
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.IncomingIdentityDefaultMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingIdentityVerifiedMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingIdentityDefaultMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingIdentityVerifiedMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
import org.session.libsignal.libsignal.SignalProtocolAddress;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.session.libsignal.libsignal.state.SessionRecord;
|
||||
import org.session.libsignal.libsignal.state.SessionStore;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.multidevice.VerifiedMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.session.libsignal.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class IdentityUtil {
|
||||
|
||||
private static final String TAG = IdentityUtil.class.getSimpleName();
|
||||
|
||||
public static ListenableFuture<Optional<IdentityRecord>> getRemoteIdentityKey(final Context context, final Recipient recipient) {
|
||||
final SettableFuture<Optional<IdentityRecord>> future = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Recipient, Void, Optional<IdentityRecord>>() {
|
||||
@Override
|
||||
protected Optional<IdentityRecord> doInBackground(Recipient... recipient) {
|
||||
return DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipient[0].getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<IdentityRecord> result) {
|
||||
future.set(result);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public static void markIdentityVerified(Context context, Recipient recipient, boolean verified, boolean remote)
|
||||
{
|
||||
long time = System.currentTimeMillis();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupDatabase.Reader reader = groupDatabase.getGroups();
|
||||
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
if (groupRecord.isRSSFeed() || groupRecord.isOpenGroup()) { continue; }
|
||||
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive() && !groupRecord.isMms()) {
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
|
||||
|
||||
if (remote) {
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
|
||||
|
||||
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
|
||||
else incoming = new IncomingIdentityDefaultMessage(incoming);
|
||||
|
||||
smsDatabase.insertMessageInbox(incoming);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(group.getGroupId(), false)), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
|
||||
OutgoingTextMessage outgoing ;
|
||||
|
||||
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient);
|
||||
else outgoing = new OutgoingIdentityDefaultMessage(recipient);
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoing, false, time, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, false);
|
||||
|
||||
if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
|
||||
else incoming = new IncomingIdentityDefaultMessage(incoming);
|
||||
|
||||
smsDatabase.insertMessageInbox(incoming);
|
||||
} else {
|
||||
OutgoingTextMessage outgoing;
|
||||
|
||||
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient);
|
||||
else outgoing = new OutgoingIdentityDefaultMessage(recipient);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
|
||||
Log.i(TAG, "Inserting verified outbox...");
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoing, false, time, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void markIdentityUpdate(Context context, Recipient recipient) {
|
||||
long time = System.currentTimeMillis();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupDatabase.Reader reader = groupDatabase.getGroups();
|
||||
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
if (groupRecord.isRSSFeed() || groupRecord.isOpenGroup()) { continue; }
|
||||
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) {
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
|
||||
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
|
||||
|
||||
smsDatabase.insertMessageInbox(groupUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.absent(), 0, false);
|
||||
IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming);
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(individualUpdate);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveIdentity(Context context, String number, IdentityKey identityKey) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(number, 1);
|
||||
|
||||
if (identityKeyStore.saveIdentity(address, identityKey)) {
|
||||
if (sessionStore.containsSession(address)) {
|
||||
SessionRecord sessionRecord = sessionStore.loadSession(address);
|
||||
sessionRecord.archiveCurrentState();
|
||||
|
||||
sessionStore.storeSession(address, sessionRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void processVerifiedMessage(Context context, VerifiedMessage verifiedMessage) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
Recipient recipient = Recipient.from(context, Address.fromExternal(context, verifiedMessage.getDestination()), true);
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipient.getAddress());
|
||||
|
||||
if (!identityRecord.isPresent() && verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT) {
|
||||
Log.w(TAG, "No existing record for default status");
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT &&
|
||||
identityRecord.isPresent() &&
|
||||
identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey()) &&
|
||||
identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.DEFAULT)
|
||||
{
|
||||
identityDatabase.setVerified(recipient.getAddress(), identityRecord.get().getIdentityKey(), IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
markIdentityVerified(context, recipient, false, true);
|
||||
}
|
||||
|
||||
if (verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.VERIFIED &&
|
||||
(!identityRecord.isPresent() ||
|
||||
(identityRecord.isPresent() && !identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey())) ||
|
||||
(identityRecord.isPresent() && identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED)))
|
||||
{
|
||||
saveIdentity(context, verifiedMessage.getDestination(), verifiedMessage.getIdentityKey());
|
||||
identityDatabase.setVerified(recipient.getAddress(), verifiedMessage.getIdentityKey(), IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
markIdentityVerified(context, recipient, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static @Nullable String getUnverifiedBannerDescription(@NonNull Context context,
|
||||
@NonNull List<Recipient> unverified)
|
||||
{
|
||||
return getPluralizedIdentityDescription(context, unverified,
|
||||
R.string.IdentityUtil_unverified_banner_one,
|
||||
R.string.IdentityUtil_unverified_banner_two,
|
||||
R.string.IdentityUtil_unverified_banner_many);
|
||||
}
|
||||
|
||||
public static @Nullable String getUnverifiedSendDialogDescription(@NonNull Context context,
|
||||
@NonNull List<Recipient> unverified)
|
||||
{
|
||||
return getPluralizedIdentityDescription(context, unverified,
|
||||
R.string.IdentityUtil_unverified_dialog_one,
|
||||
R.string.IdentityUtil_unverified_dialog_two,
|
||||
R.string.IdentityUtil_unverified_dialog_many);
|
||||
}
|
||||
|
||||
public static @Nullable String getUntrustedSendDialogDescription(@NonNull Context context,
|
||||
@NonNull List<Recipient> untrusted)
|
||||
{
|
||||
return getPluralizedIdentityDescription(context, untrusted,
|
||||
R.string.IdentityUtil_untrusted_dialog_one,
|
||||
R.string.IdentityUtil_untrusted_dialog_two,
|
||||
R.string.IdentityUtil_untrusted_dialog_many);
|
||||
}
|
||||
|
||||
private static @Nullable String getPluralizedIdentityDescription(@NonNull Context context,
|
||||
@NonNull List<Recipient> recipients,
|
||||
@StringRes int resourceOne,
|
||||
@StringRes int resourceTwo,
|
||||
@StringRes int resourceMany)
|
||||
{
|
||||
if (recipients.isEmpty()) return null;
|
||||
|
||||
if (recipients.size() == 1) {
|
||||
String name = recipients.get(0).toShortString();
|
||||
return context.getString(resourceOne, name);
|
||||
} else {
|
||||
String firstName = recipients.get(0).toShortString();
|
||||
String secondName = recipients.get(1).toShortString();
|
||||
|
||||
if (recipients.size() == 2) {
|
||||
return context.getString(resourceTwo, firstName, secondName);
|
||||
} else {
|
||||
int othersCount = recipients.size() - 2;
|
||||
String nMore = context.getResources().getQuantityString(R.plurals.identity_others, othersCount, othersCount);
|
||||
|
||||
return context.getString(resourceMany, firstName, secondName, nMore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -81,7 +82,6 @@ public class TextSecurePreferences {
|
||||
public static final String REGISTERED_GCM_PREF = "pref_gcm_registered";
|
||||
private static final String GCM_PASSWORD_PREF = "pref_gcm_password";
|
||||
private static final String SEEN_WELCOME_SCREEN_PREF = "pref_seen_welcome_screen";
|
||||
private static final String PROMPTED_PUSH_REGISTRATION_PREF = "pref_prompted_push_registration";
|
||||
private static final String PROMPTED_DEFAULT_SMS_PREF = "pref_prompted_default_sms";
|
||||
private static final String PROMPTED_OPTIMIZE_DOZE_PREF = "pref_prompted_optimize_doze";
|
||||
private static final String PROMPTED_SHARE_PREF = "pref_prompted_share";
|
||||
@ -136,7 +136,7 @@ public class TextSecurePreferences {
|
||||
private static final String DATABASE_UNENCRYPTED_SECRET = "pref_database_unencrypted_secret";
|
||||
private static final String ATTACHMENT_ENCRYPTED_SECRET = "pref_attachment_encrypted_secret";
|
||||
private static final String ATTACHMENT_UNENCRYPTED_SECRET = "pref_attachment_unencrypted_secret";
|
||||
private static final String NEEDS_SQLCIPHER_MIGRATION = "pref_needs_sql_cipher_migration";
|
||||
private static final String NEEDS_SQLCIPHER_MIGRATION = "pref_needs_sql_cipher_migration"; //TODO AC: Delete
|
||||
|
||||
private static final String NEXT_PRE_KEY_ID = "pref_next_pre_key_id";
|
||||
private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id";
|
||||
@ -879,21 +879,13 @@ public class TextSecurePreferences {
|
||||
}
|
||||
|
||||
public static boolean hasSeenWelcomeScreen(Context context) {
|
||||
return getBooleanPreference(context, SEEN_WELCOME_SCREEN_PREF, true);
|
||||
return getBooleanPreference(context, SEEN_WELCOME_SCREEN_PREF, false);
|
||||
}
|
||||
|
||||
public static void setHasSeenWelcomeScreen(Context context, boolean value) {
|
||||
setBooleanPreference(context, SEEN_WELCOME_SCREEN_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean hasPromptedPushRegistration(Context context) {
|
||||
return getBooleanPreference(context, PROMPTED_PUSH_REGISTRATION_PREF, false);
|
||||
}
|
||||
|
||||
public static void setPromptedPushRegistration(Context context, boolean value) {
|
||||
setBooleanPreference(context, PROMPTED_PUSH_REGISTRATION_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean hasPromptedDefaultSmsProvider(Context context) {
|
||||
return getBooleanPreference(context, PROMPTED_DEFAULT_SMS_PREF, false);
|
||||
}
|
||||
@ -1246,6 +1238,14 @@ public class TextSecurePreferences {
|
||||
public static void setHasSeenLightThemeIntroSheet(Context context) {
|
||||
setBooleanPreference(context, "has_seen_light_theme_intro_sheet", true);
|
||||
}
|
||||
|
||||
public static long getLastSnodePoolRefreshDate(Context context) {
|
||||
return getLongPreference(context, "last_snode_pool_refresh_date", 0);
|
||||
}
|
||||
|
||||
public static void setLastSnodePoolRefreshDate(Context context, Date date) {
|
||||
setLongPreference(context, "last_snode_pool_refresh_date", date.getTime());
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Backup related
|
||||
|
@ -1,41 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
|
||||
public class VerifySpan extends ClickableSpan {
|
||||
|
||||
private final Context context;
|
||||
private final Address address;
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
public VerifySpan(@NonNull Context context, @NonNull IdentityKeyMismatch mismatch) {
|
||||
this.context = context;
|
||||
this.address = mismatch.getAddress();
|
||||
this.identityKey = mismatch.getIdentityKey();
|
||||
}
|
||||
|
||||
public VerifySpan(@NonNull Context context, @NonNull Address address, @NonNull IdentityKey identityKey) {
|
||||
this.context = context;
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
Intent intent = new Intent(context, VerifyIdentityActivity.class);
|
||||
intent.putExtra(VerifyIdentityActivity.ADDRESS_EXTRA, address);
|
||||
intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
|
||||
intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class VersionTracker {
|
||||
|
||||
public static int getLastSeenVersion(@NonNull Context context) {
|
||||
return TextSecurePreferences.getLastVersionCode(context);
|
||||
}
|
||||
|
||||
public static void updateLastSeenVersion(@NonNull Context context) {
|
||||
try {
|
||||
int currentVersionCode = Util.getCanonicalVersionCode();
|
||||
TextSecurePreferences.setLastVersionCode(context, currentVersionCode);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
|
||||
object VersionTracker {
|
||||
|
||||
@JvmStatic
|
||||
fun getLastSeenVersion(context: Context): Int {
|
||||
var version = TextSecurePreferences.getLastVersionCode(context)
|
||||
// Zero means the app is freshly installed = user is actually on the current version.
|
||||
if (version == 0) {
|
||||
version = updateLastSeenVersion(context)
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun updateLastSeenVersion(context: Context): Int {
|
||||
return try {
|
||||
val currentVersionCode = Util.getCanonicalVersionCode()
|
||||
TextSecurePreferences.setLastVersionCode(context, currentVersionCode)
|
||||
currentVersionCode
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException("Failed to update the last seen app version.", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Message Notifications" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="There are two ways Session can notify you of new messages." />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/fcmOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Message Notifications" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Fast Mode" />
|
||||
android:text="There are two ways Session can notify you of new messages." />
|
||||
|
||||
<TextView
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/fcmOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
|
||||
<TextView
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Fast Mode" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/accent"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/activity_pn_mode_recommended_option_tag" />
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/backgroundPollingOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/accent"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/activity_pn_mode_recommended_option_tag" />
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Slow Mode" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/backgroundPollingOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
|
||||
<TextView
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Slow Mode" />
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/onboarding_button_bottom_offset"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/onboarding_button_bottom_offset"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -30,7 +30,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_register_explanation" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
@ -54,7 +54,7 @@
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -36,7 +36,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_seed_explanation" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/seedTextView"
|
||||
android:layout_width="match_parent"
|
||||
@ -49,7 +49,7 @@
|
||||
android:textAlignment="center"
|
||||
android:text="nautical novelty populate onion awkward bent etiquette plant submarine itches vipers september axis maximum populate" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
android:id="@+id/revealButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -65,7 +65,7 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="196dp"
|
||||
|
@ -50,7 +50,7 @@
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -71,7 +71,7 @@
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="0dp"
|
||||
@ -79,7 +79,7 @@
|
||||
android:layout_weight="1"
|
||||
android:text="@string/copy" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="0dp"
|
||||
|
@ -1,116 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Message Notifications" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="There are two ways Session can notify you of new messages." />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/fcmOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/large_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Message Notifications" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Fast Mode" />
|
||||
android:text="There are two ways Session can notify you of new messages." />
|
||||
|
||||
<TextView
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/fcmOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
|
||||
<TextView
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Fast Mode" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/accent"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/activity_pn_mode_recommended_option_tag" />
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/backgroundPollingOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/accent"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/activity_pn_mode_recommended_option_tag" />
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Slow Mode" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.PNModeView
|
||||
android:id="@+id/backgroundPollingOptionView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/very_large_spacing"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginRight="@dimen/very_large_spacing"
|
||||
android:padding="12dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/pn_option_background">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
|
||||
<TextView
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:text="Slow Mode" />
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginTop="@dimen/very_large_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/onboarding_button_bottom_offset"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="Session will occasionally check for new messages in the background. Full metadata protection is guaranteed, but message notifications will be unreliable." />
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.loki.views.PNModeView>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentFilled"
|
||||
android:id="@+id/registerButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginLeft="@dimen/massive_spacing"
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:layout_marginBottom="@dimen/onboarding_button_bottom_offset"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -30,7 +30,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_register_explanation" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
@ -55,7 +55,7 @@
|
||||
android:layout_marginRight="@dimen/massive_spacing"
|
||||
android:text="@string/continue_2" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -36,7 +36,7 @@
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_seed_explanation" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/seedTextView"
|
||||
android:layout_width="match_parent"
|
||||
@ -49,7 +49,7 @@
|
||||
android:textAlignment="center"
|
||||
android:text="nautical novelty populate onion awkward bent etiquette plant submarine itches vipers september axis maximum populate" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
android:id="@+id/revealButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -66,7 +66,7 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="196dp"
|
||||
|
@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<FrameLayout android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center">
|
||||
|
||||
<LinearLayout android:paddingEnd="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingTop="10dip"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText android:id="@+id/old_passphrase"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:layout_marginBottom="10dip"
|
||||
android:hint="@string/change_passphrase_activity__old_passphrase"
|
||||
android:singleLine="true"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText android:id="@+id/new_passphrase"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/change_passphrase_activity__new_passphrase"
|
||||
android:singleLine="true"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText android:id="@+id/repeat_passphrase"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:hint="@string/change_passphrase_activity__repeat_new_passphrase"
|
||||
android:singleLine="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout android:orientation="horizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp">
|
||||
|
||||
<Button android:id="@+id/cancel_button"
|
||||
android:text="@android:string/cancel"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
|
||||
<Button android:id="@+id/ok_button"
|
||||
android:text="@android:string/ok"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
</ScrollView>
|
@ -146,13 +146,6 @@
|
||||
android:inflatedId="@+id/group_share_profile_view"
|
||||
android:layout="@layout/conversation_activity_group_share_profile_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/unverified_banner_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/unverified_banner"
|
||||
android:layout="@layout/conversation_activity_unverified_banner_stub" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_content"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.identity.UnverifiedBannerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/unverified_banner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/prompt_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/passphrase_edit"
|
||||
style="?android:attr/progressBarStyleLargeInverse"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:padding="10dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/watermark"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="128dp"
|
||||
android:layout_above="@id/passphrase_edit"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/PassphrasePromptActivity_watermark_content_description"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
|
||||
</RelativeLayout>
|
@ -20,7 +20,7 @@
|
||||
android:textAlignment="center"
|
||||
android:textSize="@dimen/medium_font_size" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
style="@style/SessionIDTextView"
|
||||
android:id="@+id/seedTextView"
|
||||
android:layout_width="wrap_content"
|
||||
@ -56,7 +56,7 @@
|
||||
android:layout_weight="1"
|
||||
android:text="@string/cancel" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Dialog.Unimportant"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="0dp"
|
||||
|
@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:fab="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<Button android:id="@+id/continue_button"
|
||||
android:layout_width="140sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/experience_upgrade_activity__continue"
|
||||
android:visibility="invisible"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true" />
|
||||
|
||||
<com.melnykov.fab.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="25dp"
|
||||
android:src="@drawable/ic_arrow_forward_white_24dp"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
|
||||
fab:fab_shadow="false"
|
||||
fab:fab_colorNormal="#33000000"
|
||||
fab:fab_colorPressed="#66000000"
|
||||
fab:fab_colorRipple="#66000000" />
|
||||
</FrameLayout>
|
@ -49,7 +49,7 @@
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofTextView
|
||||
<TextView
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -70,7 +70,7 @@
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="0dp"
|
||||
@ -78,7 +78,7 @@
|
||||
android:layout_weight="1"
|
||||
android:text="@string/copy" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.views.TapJackingProofButton
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="0dp"
|
||||
|
@ -73,73 +73,7 @@
|
||||
android:id="@+id/lock_screen_auth_container"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:text="Tap to Unlock" />
|
||||
|
||||
<RelativeLayout android:id="@+id/password_auth_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="60dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<EditText android:id="@+id/passphrase_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="45sp"
|
||||
android:inputType="textPassword"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginEnd="50dp"
|
||||
android:singleLine="true"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="40dp"
|
||||
tools:text="password"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/button_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignRight="@+id/passphrase_edit"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageButton android:id="@+id/passphrase_visibility"
|
||||
android:src="?ic_visibility_on"
|
||||
android:background="@drawable/touch_highlight_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
<ImageButton android:id="@+id/passphrase_visibility_off"
|
||||
android:src="?ic_visibility_off"
|
||||
android:background="@drawable/touch_highlight_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
<ImageButton android:id="@+id/ok_button"
|
||||
android:src="?ic_arrow_forward"
|
||||
android:contentDescription="@string/PassphrasePromptActivity_ok_button_content_description"
|
||||
android:background="@null"
|
||||
android:text="@string/prompt_passphrase_activity__unlock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingBottom="5dp"/>
|
||||
</RelativeLayout>
|
||||
android:text="Tap to Unlock"/>
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
@ -1,188 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:background="?verification_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout android:layout_width="250dp"
|
||||
android:layout_height="250dp">
|
||||
|
||||
<TextView android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/verify_display_fragment__loading"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.SquareImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="20dp"
|
||||
android:padding="20dp"
|
||||
android:background="@drawable/qr_code_background"
|
||||
android:visibility="invisible"
|
||||
tools:src="@drawable/splash_logo"
|
||||
tools:visibility="invisible"/>
|
||||
|
||||
<TextView android:id="@+id/tap_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="35dp"
|
||||
android:textColor="@color/gray50"
|
||||
android:textSize="11sp"
|
||||
android:visibility="invisible"
|
||||
android:text="@string/verify_display_fragment__tap_to_scan"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.SquareImageView
|
||||
android:id="@+id/qr_verified"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="20dp"
|
||||
android:padding="20dp"
|
||||
android:src="@drawable/ic_check_white_48dp"
|
||||
android:background="@drawable/qr_code_background"
|
||||
android:backgroundTint="@color/green_500"
|
||||
android:visibility="gone"/>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<TableLayout android:id="@+id/number_table"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TableRow android:gravity="center_horizontal"
|
||||
android:clickable="false"
|
||||
android:focusable="false">
|
||||
|
||||
<TextView android:id="@+id/code_first"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="22934"/>
|
||||
|
||||
<TextView android:id="@+id/code_second"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="56944"/>
|
||||
|
||||
<TextView android:id="@+id/code_third"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="42738"/>
|
||||
|
||||
<TextView android:id="@+id/code_fourth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="20038"/>
|
||||
</TableRow>
|
||||
|
||||
<TableRow android:gravity="center_horizontal">
|
||||
<TextView android:id="@+id/code_fifth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="34431"/>
|
||||
|
||||
<TextView android:id="@+id/code_sixth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="24922"/>
|
||||
|
||||
<TextView android:id="@+id/code_seventh"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="58594"/>
|
||||
|
||||
<TextView android:id="@+id/code_eighth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="24109"/>
|
||||
</TableRow>
|
||||
|
||||
<TableRow android:gravity="center_horizontal">
|
||||
<TextView android:id="@+id/code_ninth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="00257"/>
|
||||
|
||||
<TextView android:id="@+id/code_tenth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="34956"/>
|
||||
|
||||
<TextView android:id="@+id/code_eleventh"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="32440"/>
|
||||
|
||||
<TextView android:id="@+id/code_twelth"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
style="@style/IdentityKey"
|
||||
tools:text="15774"/>
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
|
||||
<LinearLayout android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingStart="20dp">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/verified_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:layout_marginStart="5dp"
|
||||
android:textSize="17dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/verify_display_fragment__verified"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:textSize="17sp"
|
||||
android:lineSpacingExtra="3sp"
|
||||
android:text="@string/verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:title="@string/preferences__submit_debug_log"
|
||||
android:id="@+id/menu_submit_debug_logs"
|
||||
android:icon="@android:drawable/ic_menu_upload" />
|
||||
</menu>
|
@ -15,29 +15,29 @@
|
||||
android:key="pref_android_screen_lock_timeout"
|
||||
android:dependency="pref_android_screen_lock" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:key="pref_enable_passphrase_temporary"
|
||||
android:defaultValue="true"
|
||||
android:title="@string/preferences__enable_passphrase"
|
||||
android:summary="@string/preferences__lock_signal_and_message_notifications_with_a_passphrase" />
|
||||
<!-- <org.thoughtcrime.securesms.components.SwitchPreferenceCompat-->
|
||||
<!-- android:key="pref_enable_passphrase_temporary"-->
|
||||
<!-- android:defaultValue="true"-->
|
||||
<!-- android:title="@string/preferences__enable_passphrase"-->
|
||||
<!-- android:summary="@string/preferences__lock_signal_and_message_notifications_with_a_passphrase" />-->
|
||||
|
||||
<Preference
|
||||
android:key="pref_change_passphrase"
|
||||
android:title="@string/preferences__change_passphrase"
|
||||
android:summary="@string/preferences__change_your_passphrase"
|
||||
android:dependency="pref_enable_passphrase_temporary" />
|
||||
<!-- <Preference-->
|
||||
<!-- android:key="pref_change_passphrase"-->
|
||||
<!-- android:title="@string/preferences__change_passphrase"-->
|
||||
<!-- android:summary="@string/preferences__change_your_passphrase"-->
|
||||
<!-- android:dependency="pref_enable_passphrase_temporary" />-->
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_timeout_passphrase"
|
||||
android:title="@string/preferences__inactivity_timeout_passphrase"
|
||||
android:summary="@string/preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity"
|
||||
android:dependency="pref_enable_passphrase_temporary" />
|
||||
<!-- <org.thoughtcrime.securesms.components.SwitchPreferenceCompat-->
|
||||
<!-- android:defaultValue="false"-->
|
||||
<!-- android:key="pref_timeout_passphrase"-->
|
||||
<!-- android:title="@string/preferences__inactivity_timeout_passphrase"-->
|
||||
<!-- android:summary="@string/preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity"-->
|
||||
<!-- android:dependency="pref_enable_passphrase_temporary" />-->
|
||||
|
||||
<Preference
|
||||
android:title="@string/preferences__inactivity_timeout_interval"
|
||||
android:key="pref_timeout_interval"
|
||||
android:dependency="pref_timeout_passphrase" />
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/preferences__inactivity_timeout_interval"-->
|
||||
<!-- android:key="pref_timeout_interval"-->
|
||||
<!-- android:dependency="pref_timeout_passphrase" />-->
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
|
@ -1,14 +1,28 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
import org.session.libsession.database.dto.DatabaseAttachmentDTO
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
|
||||
interface MessageDataProvider {
|
||||
|
||||
fun getAttachment(uniqueID: String): DatabaseAttachmentDTO?
|
||||
//fun getAttachment(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
|
||||
fun getAttachmentPointer(attachmentID: String): SignalServiceAttachmentPointer?
|
||||
|
||||
fun getMessageID(serverID: Long): Long?
|
||||
fun deleteMessage(messageID: Long)
|
||||
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
|
||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun uploadAttachment(attachmentId: Long)
|
||||
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package org.session.libsession.database.dto
|
||||
|
||||
import android.util.Size
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import kotlin.math.round
|
||||
|
||||
class DatabaseAttachmentDTO {
|
||||
var contentType: String? = null
|
||||
|
||||
var fileName: String? = null
|
||||
|
||||
var url: String? = null
|
||||
|
||||
var caption: String? = null
|
||||
|
||||
var size: Int = 0
|
||||
|
||||
var key: ByteString? = null
|
||||
|
||||
var digest: ByteString? = null
|
||||
|
||||
var flags: Int = 0
|
||||
|
||||
var width: Int = 0
|
||||
|
||||
var height: Int = 0
|
||||
|
||||
val isVoiceNote: Boolean = false
|
||||
|
||||
var shouldHaveImageSize: Boolean = false
|
||||
|
||||
val isUploaded: Boolean = false
|
||||
|
||||
fun toProto(): SignalServiceProtos.AttachmentPointer? {
|
||||
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||
builder.contentType = this.contentType
|
||||
|
||||
if (!this.fileName.isNullOrEmpty()) {
|
||||
builder.fileName = this.fileName
|
||||
}
|
||||
if (!this.caption.isNullOrEmpty()) {
|
||||
builder.caption = this.caption
|
||||
}
|
||||
|
||||
builder.size = this.size
|
||||
builder.key = this.key
|
||||
builder.digest = this.digest
|
||||
builder.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
|
||||
|
||||
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
|
||||
if (this.shouldHaveImageSize) {
|
||||
if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) {
|
||||
val imageSize= Size(this.width, this.height)
|
||||
val imageWidth = round(imageSize.width.toDouble())
|
||||
val imageHeight = round(imageSize.height.toDouble())
|
||||
if (imageWidth > 0 && imageHeight > 0) {
|
||||
builder.width = imageWidth.toInt()
|
||||
builder.height = imageHeight.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.url = this.url
|
||||
|
||||
try {
|
||||
return builder.build()
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -38,7 +38,7 @@ interface StorageProtocol {
|
||||
fun markJobAsSucceeded(job: Job)
|
||||
fun markJobAsFailed(job: Job)
|
||||
fun getAllPendingJobs(type: String): List<Job>
|
||||
fun getAttachmentUploadJob(attachmentID: String): AttachmentUploadJob?
|
||||
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
|
||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||
fun isJobCanceled(job: Job): Boolean
|
||||
|
@ -4,12 +4,12 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.libsignal.util.Hex
|
||||
import org.session.libsignal.service.internal.util.Base64
|
||||
import org.session.libsignal.service.internal.util.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink
|
||||
@ -18,7 +18,7 @@ import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.collections.set
|
||||
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) {
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
||||
|
||||
companion object {
|
||||
// region Settings
|
||||
@ -49,6 +49,7 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
val server = "https://file.getsession.org"
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
|
@ -1,17 +1,94 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
class AttachmentUploadJob : Job {
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory
|
||||
|
||||
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception() {
|
||||
object NoAttachment : Error("No such attachment.")
|
||||
}
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
companion object {
|
||||
val TAG = AttachmentUploadJob::class.qualifiedName
|
||||
|
||||
val collection: String = "AttachmentUploadJobCollection"
|
||||
val maxFailureCount: Int = 20
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
TODO("Not yet implemented")
|
||||
try {
|
||||
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
val server = openGroup?.server ?: FileServerAPI.server
|
||||
|
||||
//TODO add some encryption stuff here
|
||||
val isEncryptionRequired = false
|
||||
//val isEncryptionRequired = (server == FileServerAPI.server)
|
||||
|
||||
val attachmentKey = Util.getSecretBytes(64)
|
||||
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val ciphertextLength = attachmentStream.length
|
||||
|
||||
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
|
||||
|
||||
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is Error && e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
Log.w(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
//TODO interaction stuff, not sure how to deal with that
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
private fun handleFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed due to error: $this.")
|
||||
delegate?.handleJobFailed(this, e)
|
||||
if (failureCount + 1 == AttachmentUploadJob.maxFailureCount) {
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun failAssociatedMessageSendJob(e: Exception) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
|
||||
MessageSender.handleFailedMessageSend(this.message!!, e)
|
||||
if (messageSendJob != null) {
|
||||
storage.markJobAsFailed(messageSendJob)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver
|
||||
import org.session.libsession.messaging.sending_receiving.handle
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
|
||||
class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
@ -10,6 +15,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageReceiveJob::class.qualifiedName
|
||||
|
||||
val collection: String = "MessageReceiveJobCollection"
|
||||
}
|
||||
|
||||
@ -18,6 +25,33 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
}
|
||||
|
||||
fun executeAsync(): Promise<Unit, Exception> {
|
||||
TODO("Not yet implemented")
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
try {
|
||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID)
|
||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||
this.handleSuccess()
|
||||
deferred.resolve(Unit)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Couldn't receive message due to error: $e.")
|
||||
val error = e as? MessageReceiver.Error
|
||||
error?.let {
|
||||
if (!error.isRetryable) this.handlePermanentFailure(error)
|
||||
}
|
||||
this.handleFailure(e)
|
||||
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
}
|
||||
|
||||
private fun handleFailure(e: Exception) {
|
||||
delegate?.handleJobFailed(this, e)
|
||||
}
|
||||
}
|
@ -1,9 +1,15 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
@ -11,10 +17,54 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageSendJob::class.qualifiedName
|
||||
|
||||
val collection: String = "MessageSendJobCollection"
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
TODO("Not yet implemented")
|
||||
val messageDataProvider = MessagingConfiguration.shared.messageDataProvider
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
|
||||
val attachmentsToUpload = attachments.filter { !it.isUploaded }
|
||||
attachmentsToUpload.forEach {
|
||||
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {
|
||||
// Wait for it to finish
|
||||
} else {
|
||||
val job = AttachmentUploadJob(it.attachmentId, message.threadID!!, message, id!!)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
if (attachmentsToUpload.isNotEmpty()) return // Wait for all attachments to upload before continuing
|
||||
}
|
||||
MessageSender.send(this.message, this.destination).success {
|
||||
this.handleSuccess()
|
||||
}.fail { exception ->
|
||||
Log.e(TAG, "Couldn't send message due to error: $exception.")
|
||||
val e = exception as? MessageSender.Error
|
||||
e?.let {
|
||||
if (!e.isRetryable) this.handlePermanentFailure(e)
|
||||
}
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(error: Exception) {
|
||||
delegate?.handleJobFailedPermanently(this, error)
|
||||
}
|
||||
|
||||
private fun handleFailure(error: Exception) {
|
||||
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!MessagingConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
}
|
||||
delegate?.handleJobFailed(this, error)
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ class LinkPreview() {
|
||||
|
||||
var title: String? = null
|
||||
var url: String? = null
|
||||
var attachmentID: String? = null
|
||||
var attachmentID: Long? = 0
|
||||
|
||||
companion object {
|
||||
const val TAG = "LinkPreview"
|
||||
@ -21,7 +21,7 @@ class LinkPreview() {
|
||||
}
|
||||
|
||||
//constructor
|
||||
internal constructor(title: String?, url: String, attachmentID: String?) : this() {
|
||||
internal constructor(title: String?, url: String, attachmentID: Long?) : this() {
|
||||
this.title = title
|
||||
this.url = url
|
||||
this.attachmentID = attachmentID
|
||||
@ -44,7 +44,7 @@ class LinkPreview() {
|
||||
title?.let { linkPreviewProto.title = title }
|
||||
val attachmentID = attachmentID
|
||||
attachmentID?.let {
|
||||
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachment(attachmentID)
|
||||
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
|
||||
}
|
||||
// Build
|
||||
|
@ -11,7 +11,7 @@ class Quote() {
|
||||
var timestamp: Long? = 0
|
||||
var publicKey: String? = null
|
||||
var text: String? = null
|
||||
var attachmentID: String? = null
|
||||
var attachmentID: Long? = null
|
||||
|
||||
companion object {
|
||||
const val TAG = "Quote"
|
||||
@ -25,7 +25,7 @@ class Quote() {
|
||||
}
|
||||
|
||||
//constructor
|
||||
internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: String?) : this() {
|
||||
internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: Long?) : this() {
|
||||
this.timestamp = timestamp
|
||||
this.publicKey = publicKey
|
||||
this.text = text
|
||||
@ -60,7 +60,7 @@ class Quote() {
|
||||
|
||||
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, messageDataProvider: MessageDataProvider) {
|
||||
val attachmentID = attachmentID ?: return
|
||||
val attachmentProto = messageDataProvider.getAttachment(attachmentID)
|
||||
val attachmentProto = messageDataProvider.getAttachmentStream(attachmentID)
|
||||
if (attachmentProto == null) {
|
||||
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
|
||||
return
|
||||
@ -74,7 +74,7 @@ class Quote() {
|
||||
}
|
||||
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
|
||||
quotedAttachmentProto.contentType = attachmentProto.contentType
|
||||
val fileName = attachmentProto.fileName
|
||||
val fileName = attachmentProto.fileName?.get()
|
||||
fileName?.let { quotedAttachmentProto.fileName = fileName }
|
||||
quotedAttachmentProto.thumbnail = attachmentProto.toProto()
|
||||
try {
|
||||
|
@ -11,7 +11,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
class VisibleMessage : Message() {
|
||||
|
||||
var text: String? = null
|
||||
var attachmentIDs = ArrayList<String>()
|
||||
var attachmentIDs = ArrayList<Long>()
|
||||
var quote: Quote? = null
|
||||
var linkPreview: LinkPreview? = null
|
||||
var contact: Contact? = null
|
||||
@ -90,7 +90,7 @@ class VisibleMessage : Message() {
|
||||
}
|
||||
}
|
||||
//Attachments
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachment(it) }
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(it) }
|
||||
if (!attachments.all { it.isUploaded }) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
|
@ -0,0 +1,123 @@
|
||||
package org.session.libsession.messaging.sending_receiving.attachments
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class SessionServiceAttachment protected constructor(val contentType: String?) {
|
||||
|
||||
var attachmentId: Long = 0
|
||||
var isGif: Boolean = false
|
||||
var isImage: Boolean = false
|
||||
var isVideo: Boolean = false
|
||||
var isAudio: Boolean = false
|
||||
var url: String = ""
|
||||
var key: ByteString? = null
|
||||
|
||||
abstract fun isStream(): Boolean
|
||||
abstract fun isPointer(): Boolean
|
||||
fun asStream(): SessionServiceAttachmentStream {
|
||||
return this as SessionServiceAttachmentStream
|
||||
}
|
||||
|
||||
fun asPointer(): SessionServiceAttachmentPointer {
|
||||
return this as SessionServiceAttachmentPointer
|
||||
}
|
||||
|
||||
fun shouldHaveImageSize(): Boolean {
|
||||
return (isVideo || isImage || isGif);
|
||||
}
|
||||
|
||||
class Builder internal constructor() {
|
||||
private var inputStream: InputStream? = null
|
||||
private var contentType: String? = null
|
||||
private var fileName: String? = null
|
||||
private var length: Long = 0
|
||||
private var listener: SignalServiceAttachment.ProgressListener? = null
|
||||
private var voiceNote = false
|
||||
private var width = 0
|
||||
private var height = 0
|
||||
private var caption: String? = null
|
||||
fun withStream(inputStream: InputStream?): Builder {
|
||||
this.inputStream = inputStream
|
||||
return this
|
||||
}
|
||||
|
||||
fun withContentType(contentType: String?): Builder {
|
||||
this.contentType = contentType
|
||||
return this
|
||||
}
|
||||
|
||||
fun withLength(length: Long): Builder {
|
||||
this.length = length
|
||||
return this
|
||||
}
|
||||
|
||||
fun withFileName(fileName: String?): Builder {
|
||||
this.fileName = fileName
|
||||
return this
|
||||
}
|
||||
|
||||
fun withListener(listener: SignalServiceAttachment.ProgressListener?): Builder {
|
||||
this.listener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
fun withVoiceNote(voiceNote: Boolean): Builder {
|
||||
this.voiceNote = voiceNote
|
||||
return this
|
||||
}
|
||||
|
||||
fun withWidth(width: Int): Builder {
|
||||
this.width = width
|
||||
return this
|
||||
}
|
||||
|
||||
fun withHeight(height: Int): Builder {
|
||||
this.height = height
|
||||
return this
|
||||
}
|
||||
|
||||
fun withCaption(caption: String?): Builder {
|
||||
this.caption = caption
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): SessionServiceAttachmentStream {
|
||||
requireNotNull(inputStream) { "Must specify stream!" }
|
||||
requireNotNull(contentType) { "No content type specified!" }
|
||||
require(length != 0L) { "No length specified!" }
|
||||
return SessionServiceAttachmentStream(inputStream, contentType, length, Optional.fromNullable(fileName), voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption), listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to receive progress information on upload/download of
|
||||
* an attachment.
|
||||
*/
|
||||
/*interface ProgressListener {
|
||||
/**
|
||||
* Called on a progress change event.
|
||||
*
|
||||
* @param total The total amount to transmit/receive in bytes.
|
||||
* @param progress The amount that has been transmitted/received in bytes thus far
|
||||
*/
|
||||
fun onAttachmentProgress(total: Long, progress: Long)
|
||||
}*/
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newStreamBuilder(): Builder {
|
||||
return Builder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// matches values in AttachmentDatabase.java
|
||||
enum class AttachmentState(val value: Int) {
|
||||
DONE(0),
|
||||
STARTED(1),
|
||||
PENDING(2),
|
||||
FAILED(3)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2017 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
package org.session.libsession.messaging.sending_receiving.attachments
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
|
||||
/**
|
||||
* Represents a received SignalServiceAttachment "handle." This
|
||||
* is a pointer to the actual attachment content, which needs to be
|
||||
* retrieved using [SignalServiceMessageReceiver.retrieveAttachment]
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
class SessionServiceAttachmentPointer(val id: Long, contentType: String?, key: ByteArray?,
|
||||
val size: Optional<Int>, val preview: Optional<ByteArray>,
|
||||
val width: Int, val height: Int,
|
||||
val digest: Optional<ByteArray>, val fileName: Optional<String>,
|
||||
val voiceNote: Boolean, val caption: Optional<String>, url: String) : SessionServiceAttachment(contentType) {
|
||||
override fun isStream(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isPointer(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user