mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-20 17:57:29 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3380293923 | ||
|
|
a549c1ec8b | ||
|
|
ad84997ce0 | ||
|
|
42e2576813 | ||
|
|
31b995fa98 | ||
|
|
0364bec995 | ||
|
|
aa39f3d0a3 | ||
|
|
db545f43ea | ||
|
|
bbe003a454 | ||
|
|
819f0f68f6 | ||
|
|
8c0160937b | ||
|
|
6de789dfe3 | ||
|
|
afa2bb3bf5 | ||
|
|
89e66c0741 | ||
|
|
0dc4afba99 | ||
|
|
152578e576 | ||
|
|
63d6ab6fa7 | ||
|
|
75c8c59d78 | ||
|
|
87a59b6a9b | ||
|
|
2001fa86cf | ||
|
|
52747782a7 | ||
|
|
66f2668326 | ||
|
|
b262efc24c | ||
|
|
ce7ad76447 | ||
|
|
9e98b6616e | ||
|
|
f4c9eaa904 | ||
|
|
f8a0988e5f | ||
|
|
bf919207ed | ||
|
|
dac6b5c992 | ||
|
|
7f8043777e | ||
|
|
854b3feb36 | ||
|
|
22447e6ddb | ||
|
|
be2ec36e1f | ||
|
|
98cf16479d | ||
|
|
584735cbd0 | ||
|
|
3741493cb7 | ||
|
|
4ea861fe5c | ||
|
|
cd3df4d3c1 | ||
|
|
881a1edccb | ||
|
|
1b7b574289 | ||
|
|
d1d7498447 | ||
|
|
50c18727e7 | ||
|
|
e9bfde470a | ||
|
|
68f718a210 |
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 660
|
||||
def canonicalVersionName = "4.64.3"
|
||||
def canonicalVersionCode = 666
|
||||
def canonicalVersionName = "4.65.1"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -304,7 +304,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.1.1'
|
||||
implementation 'org.signal:ringrtc-android:2.2.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
<issue id="StringFormatMatches" severity="error" />
|
||||
|
||||
<!-- L10N warnings -->
|
||||
<issue id="MissingTranslation" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="TypographyDashes" severity="error" >
|
||||
<ignore path="*/res/values-*" /> <!-- Ignore for non-English -->
|
||||
</issue>
|
||||
|
||||
<issue id="CanvasSize" severity="error" />
|
||||
<issue id="HardcodedText" severity="error" />
|
||||
|
||||
@@ -108,10 +108,11 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeCrashHandling();
|
||||
initializeAppDependencies();
|
||||
initializeFirstEverAppLaunch();
|
||||
@@ -143,6 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
*
|
||||
*/
|
||||
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -14,12 +18,15 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
@@ -33,7 +40,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
/**
|
||||
* Activity for displaying avatars full screen.
|
||||
*/
|
||||
public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String TAG = Log.tag(AvatarPreviewActivity.class);
|
||||
|
||||
@@ -58,7 +65,15 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
|
||||
setTheme(R.style.TextSecure_MediaPreview);
|
||||
setContentView(R.layout.contact_photo_preview_activity);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
postponeEnterTransition();
|
||||
TransitionInflater inflater = TransitionInflater.from(this);
|
||||
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
|
||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
@@ -79,24 +94,40 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
|
||||
GlideApp.with(this).load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
Resources resources = this.getResources();
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(avatar);
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Bitmap>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
|
||||
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
avatar.setImageDrawable(RoundedBitmapDrawableFactory.create(resources, resource));
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
startPostponedEnterTransition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
}
|
||||
});
|
||||
|
||||
toolbar.setTitle(recipient.getDisplayName(context));
|
||||
});
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
|
||||
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (BaseActivity.isMenuWorkaroundRequired()) {
|
||||
forceOverflowMenu();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from: http://stackoverflow.com/a/13098824
|
||||
*/
|
||||
private void forceOverflowMenu() {
|
||||
try {
|
||||
ViewConfiguration config = ViewConfiguration.get(this);
|
||||
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
|
||||
if(menuKeyField != null) {
|
||||
menuKeyField.setAccessible(true);
|
||||
menuKeyField.setBoolean(config, false);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,90 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
public abstract class BaseActivity extends FragmentActivity {
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
* screen lock.
|
||||
*/
|
||||
public abstract class BaseActivity extends AppCompatActivity {
|
||||
private static final String TAG = Log.tag(BaseActivity.class);
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
public static boolean isMenuWorkaroundRequired() {
|
||||
return VERSION.SDK_INT < VERSION_CODES.KITKAT &&
|
||||
VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 &&
|
||||
("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,5 +47,6 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
@@ -28,7 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
@@ -15,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
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;
|
||||
@@ -105,7 +103,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -115,26 +112,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
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());
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.lang.ref.WeakReference;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActivity
|
||||
implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.ScrollCallback
|
||||
|
||||
@@ -40,7 +40,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
@@ -93,7 +92,7 @@ import java.util.Set;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends Fragment
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
|
||||
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
|
||||
private final ImportStateHandler importStateHandler = new ImportStateHandler();
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
@@ -42,7 +41,7 @@ import org.whispersystems.signalservice.internal.push.DeviceLimitExceededExcepti
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
public class DeviceActivity extends PassphraseRequiredActivity
|
||||
implements Button.OnClickListener, ScanListener, DeviceLinkFragment.LinkClickedListener
|
||||
{
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class DeviceAddFragment extends Fragment {
|
||||
public class DeviceAddFragment extends LoggingFragment {
|
||||
|
||||
private ViewGroup container;
|
||||
private LinearLayout overlay;
|
||||
|
||||
@@ -53,7 +53,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Bundle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.Window;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
|
||||
@@ -26,7 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||
startActivity(intent);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(R.string.DeviceProvisioningActivity_cancel, (dialog12, which) -> {
|
||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||
dialog12.dismiss();
|
||||
finish();
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* Simply logs out lifecycle events.
|
||||
*/
|
||||
public abstract class LoggingFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(LoggingFragment.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
@@ -3,9 +3,8 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
public class MainFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
||||
@@ -77,7 +77,7 @@ import java.util.Map;
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public abstract class PassphraseActivity extends BaseActionBarActivity {
|
||||
public abstract class PassphraseActivity extends BaseActivity {
|
||||
|
||||
private static final String TAG = PassphraseActivity.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
||||
public abstract class PassphraseRequiredActivity extends BaseActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActivity.class.getSimpleName();
|
||||
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
public static final String NEXT_INTENT_EXTRA = "next_intent";
|
||||
@@ -49,7 +49,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onCreate()");
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
@@ -69,7 +68,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onResume()");
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
@@ -77,27 +75,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onPause()");
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onDestroy()");
|
||||
super.onDestroy();
|
||||
removeClearKeyReceiver(this);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import android.widget.Button;
|
||||
|
||||
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
|
||||
|
||||
public class PromptMmsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class PromptMmsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
|
||||
@@ -104,7 +104,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.transition.Transition;
|
||||
import android.transition.TransitionValues;
|
||||
import android.util.Property;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
|
||||
@TargetApi(21)
|
||||
abstract class CircleSquareImageViewTransition extends Transition {
|
||||
|
||||
private static final String CIRCLE_RATIO = "CIRCLE_RATIO";
|
||||
|
||||
private final boolean toCircle;
|
||||
|
||||
CircleSquareImageViewTransition(boolean toCircle) {
|
||||
this.toCircle = toCircle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureStartValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 0f : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureEndValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageView endImageView = (ImageView) endValues.view;
|
||||
float start = (float) startValues.values.get(CIRCLE_RATIO);
|
||||
float end = (float) endValues.values.get(CIRCLE_RATIO);
|
||||
|
||||
return ObjectAnimator.ofFloat(endImageView, new RadiusRatioProperty(), start, end);
|
||||
}
|
||||
|
||||
static final class RadiusRatioProperty extends Property<ImageView, Float> {
|
||||
|
||||
private float ratio;
|
||||
|
||||
RadiusRatioProperty() {
|
||||
super(Float.class, "circle_ratio");
|
||||
}
|
||||
|
||||
@Override
|
||||
final public void set(ImageView imageView, Float ratio) {
|
||||
this.ratio = ratio;
|
||||
Drawable imageViewDrawable = imageView.getDrawable();
|
||||
if (imageViewDrawable instanceof RoundedBitmapDrawable) {
|
||||
RoundedBitmapDrawable drawable = (RoundedBitmapDrawable) imageViewDrawable;
|
||||
if (ratio > 0.95) {
|
||||
drawable.setCircular(true);
|
||||
} else {
|
||||
drawable.setCornerRadius(Math.min(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()) * ratio * 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(ImageView object) {
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class CircleToSquareImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public CircleToSquareImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class SquareToCircleImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public SquareToCircleImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -89,7 +90,11 @@ public final class AudioWaveForm {
|
||||
AudioHash audioHash = attachment.getAudioHash();
|
||||
if (audioHash != null) {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
if (audioFileInfo.waveForm.length == 0) {
|
||||
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
} else if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
@@ -100,13 +105,19 @@ public final class AudioWaveForm {
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
|
||||
@@ -6,14 +6,15 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -94,9 +95,17 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
dateView.forceLayout();
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
dateView.setText(R.string.ConversationItem_error_not_delivered);
|
||||
int errorMsg;
|
||||
if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
errorMsg = R.string.ConversationItem_error_network_not_delivered;
|
||||
} else if (messageRecord.getRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
|
||||
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
|
||||
} else {
|
||||
errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details;
|
||||
}
|
||||
|
||||
dateView.setText(errorMsg);
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
} else {
|
||||
|
||||
@@ -205,6 +205,10 @@ public final class TransferControlView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void display(@Nullable final View view) {
|
||||
if (current == view) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current != null) {
|
||||
current.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactNameEditActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class ContactNameEditActivity extends PassphraseRequiredActivity {
|
||||
|
||||
public static final String KEY_NAME = "name";
|
||||
public static final String KEY_CONTACT_INDEX = "contact_index";
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
@@ -27,7 +27,7 @@ import java.util.List;
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
|
||||
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener {
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActivity implements ContactShareEditAdapter.EventListener {
|
||||
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_URIS = "contact_uris";
|
||||
|
||||
@@ -20,8 +20,7 @@ import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -44,7 +43,7 @@ import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.*;
|
||||
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 2323;
|
||||
private static final String KEY_CONTACT = "contact";
|
||||
|
||||
@@ -85,7 +85,7 @@ import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.GroupMembersDialog;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.PromptMmsActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
|
||||
@@ -108,9 +108,7 @@ 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.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
@@ -125,6 +123,7 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -173,6 +172,7 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
@@ -262,7 +262,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
*
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public class ConversationActivity extends PassphraseRequiredActivity
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
OnKeyboardShownListener,
|
||||
@@ -273,13 +273,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
StickerKeyboardProvider.StickerEventListener,
|
||||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
@@ -1306,50 +1309,28 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void handleUnverifiedRecipients() {
|
||||
List<Recipient> unverifiedRecipients = identityRecords.getUnverifiedRecipients();
|
||||
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 handleRecentSafetyNumberChange() {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
private void handleUntrustedRecipients() {
|
||||
List<Recipient> untrustedRecipients = identityRecords.getUntrustedRecipients();
|
||||
List<IdentityRecord> untrustedRecords = identityRecords.getUntrustedRecords();
|
||||
String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() {
|
||||
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) { }
|
||||
});
|
||||
}
|
||||
|
||||
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
|
||||
@@ -2329,10 +2310,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
handleManualMmsRequired();
|
||||
} else if (!forceSms && identityRecords.isUnverified()) {
|
||||
handleUnverifiedRecipients();
|
||||
} else if (!forceSms && identityRecords.isUntrusted()) {
|
||||
handleUntrustedRecipients();
|
||||
} else if (!forceSms && (identityRecords.isUnverified() || identityRecords.isUntrusted())) {
|
||||
handleRecentSafetyNumberChange();
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating);
|
||||
} else {
|
||||
@@ -2886,6 +2865,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
reactionOverlay.setListVerticalTranslation(translationY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(this, messageRecord))
|
||||
.show();
|
||||
} else if (messageRecord.isIdentityMismatchFailure()) {
|
||||
SafetyNumberChangeDialog.create(this, messageRecord).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
} else {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(this, messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
|
||||
@@ -50,7 +50,6 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -60,7 +59,8 @@ import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
@@ -133,7 +133,7 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationFragment extends Fragment {
|
||||
public class ConversationFragment extends LoggingFragment {
|
||||
private static final String TAG = ConversationFragment.class.getSimpleName();
|
||||
|
||||
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
|
||||
@@ -177,7 +177,7 @@ public class ConversationFragment extends Fragment {
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -989,6 +989,7 @@ public class ConversationFragment extends Fragment {
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||
void onCursorChanged();
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
@@ -1249,6 +1250,11 @@ public class ConversationFragment extends Fragment {
|
||||
|
||||
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
listener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -90,7 +90,6 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
@@ -1375,7 +1374,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
parent.onClick(v);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
context.startActivity(MessageDetailsActivity.getIntentForMessageDetails(context, messageRecord, conversationRecipient.getId(), messageRecord.getThreadId()));
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
|
||||
handleApproveIdentity();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Wrapper class for helping show a list of recipients that had recent safety number changes.
|
||||
*
|
||||
* Also provides helper methods for behavior used in multiple spots.
|
||||
*/
|
||||
final class ChangedRecipient {
|
||||
private final Recipient recipient;
|
||||
private final IdentityRecord record;
|
||||
|
||||
ChangedRecipient(@NonNull Recipient recipient, @NonNull IdentityRecord record) {
|
||||
this.recipient = recipient;
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@NonNull IdentityRecord getIdentityRecord() {
|
||||
return record;
|
||||
}
|
||||
|
||||
boolean isUnverified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.UNVERIFIED;
|
||||
}
|
||||
|
||||
boolean isVerified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, SafetyNumberChangeAdapter.ViewHolder> {
|
||||
|
||||
private final Callbacks callbacks;
|
||||
|
||||
SafetyNumberChangeAdapter(@NonNull Callbacks callbacks) {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.safety_number_change_recipient, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final ChangedRecipient changedRecipient = getItem(position);
|
||||
holder.bind(changedRecipient);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final AvatarImageView avatar;
|
||||
final FromTextView name;
|
||||
final TextView subtitle;
|
||||
final View viewButton;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
avatar = itemView.findViewById(R.id.safety_number_change_recipient_avatar);
|
||||
name = itemView.findViewById(R.id.safety_number_change_recipient_name);
|
||||
subtitle = itemView.findViewById(R.id.safety_number_change_recipient_subtitle);
|
||||
viewButton = itemView.findViewById(R.id.safety_number_change_recipient_view);
|
||||
}
|
||||
|
||||
void bind(@NonNull ChangedRecipient changedRecipient) {
|
||||
avatar.setRecipient(changedRecipient.getRecipient());
|
||||
name.setText(changedRecipient.getRecipient());
|
||||
|
||||
if (changedRecipient.isUnverified() || changedRecipient.isVerified()) {
|
||||
subtitle.setText(R.string.safety_number_change_dialog__previous_verified);
|
||||
|
||||
Drawable check = ContextCompat.getDrawable(itemView.getContext(), R.drawable.check);
|
||||
if (check != null) {
|
||||
check.setBounds(0, 0, ViewUtil.dpToPx(12), ViewUtil.dpToPx(12));
|
||||
subtitle.setCompoundDrawables(check, null, null, null);
|
||||
}
|
||||
} else if (changedRecipient.getRecipient().hasAUserSetDisplayName(itemView.getContext())) {
|
||||
subtitle.setText(changedRecipient.getRecipient().getE164().or(""));
|
||||
subtitle.setCompoundDrawables(null, null, null, null);
|
||||
} else {
|
||||
subtitle.setText("");
|
||||
}
|
||||
subtitle.setVisibility(TextUtils.isEmpty(subtitle.getText()) ? View.GONE : View.VISIBLE);
|
||||
|
||||
viewButton.setOnClickListener(view -> callbacks.onViewIdentityRecord(changedRecipient.getIdentityRecord()));
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks {
|
||||
|
||||
private static final String RECIPIENT_IDS_EXTRA = "recipient_ids";
|
||||
private static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
|
||||
private SafetyNumberChangeViewModel viewModel;
|
||||
private SafetyNumberChangeAdapter adapter;
|
||||
private View dialogView;
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
List<String> ids = Stream.of(identityRecords)
|
||||
.map(record -> record.getRecipientId().serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(Context context, MessageRecord messageRecord) {
|
||||
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
|
||||
.map(mismatch -> mismatch.getRecipientId(context).serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
arguments.putLong(MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private SafetyNumberChangeDialog() { }
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
List<RecipientId> recipientIds = Stream.of(getArguments().getStringArray(RECIPIENT_IDS_EXTRA)).map(RecipientId::from).toList();
|
||||
long messageId = getArguments().getLong(MESSAGE_ID_EXTRA, -1);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new SafetyNumberChangeViewModel.Factory(recipientIds, (messageId != -1) ? messageId : null)).get(SafetyNumberChangeViewModel.class);
|
||||
viewModel.getChangedRecipients().observe(getViewLifecycleOwner(), adapter::submitList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme());
|
||||
|
||||
configureView(dialogView);
|
||||
|
||||
builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
dialogView = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void configureView(View view) {
|
||||
RecyclerView list = view.findViewById(R.id.safety_number_change_dialog_list);
|
||||
adapter = new SafetyNumberChangeAdapter(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setItemAnimator(null);
|
||||
list.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
}
|
||||
|
||||
private void handleSendAnyway(DialogInterface dialogInterface, int which) {
|
||||
Activity activity = getActivity();
|
||||
Callback callback;
|
||||
if (activity instanceof Callback) {
|
||||
callback = (Callback) activity;
|
||||
} else {
|
||||
callback = null;
|
||||
}
|
||||
|
||||
LiveData<TrustAndVerifyResult> trustOrVerifyResultLiveData = viewModel.trustOrVerifyChangedRecipients();
|
||||
|
||||
Observer<TrustAndVerifyResult> observer = new Observer<TrustAndVerifyResult>() {
|
||||
@Override
|
||||
public void onChanged(TrustAndVerifyResult result) {
|
||||
if (callback != null) {
|
||||
switch (result) {
|
||||
case TRUST_AND_VERIFY:
|
||||
callback.onSendAnywayAfterSafetyNumberChange();
|
||||
break;
|
||||
case TRUST_VERIFY_AND_RESEND:
|
||||
callback.onMessageResentAfterSafetyNumberChange();
|
||||
break;
|
||||
}
|
||||
}
|
||||
trustOrVerifyResultLiveData.removeObserver(this);
|
||||
}
|
||||
};
|
||||
|
||||
trustOrVerifyResultLiveData.observeForever(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onSendAnywayAfterSafetyNumberChange();
|
||||
void onMessageResentAfterSafetyNumberChange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
final class SafetyNumberChangeRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
SafetyNumberChangeRepository(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull LiveData<SafetyNumberChangeState> getSafetyNumberChangeState(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MutableLiveData<SafetyNumberChangeState> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getSafetyNumberChangeStateInternal(recipientIds, messageId)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsInternal(changedRecipients)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipientsAndResend(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsAndResendInternal(changedRecipients, messageRecord)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull SafetyNumberChangeState getSafetyNumberChangeStateInternal(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MessageRecord messageRecord = null;
|
||||
if (messageId != null) {
|
||||
messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
|
||||
|
||||
List<ChangedRecipient> changedRecipients = Stream.of(DatabaseFactory.getIdentityDatabase(context).getIdentities(recipients).getIdentityRecords())
|
||||
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
|
||||
.toList();
|
||||
|
||||
return new SafetyNumberChangeState(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
IdentityRecord identityRecord = changedRecipient.getIdentityRecord();
|
||||
|
||||
if (changedRecipient.isUnverified()) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
} else {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_AND_VERIFY;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsAndResendInternal(@NonNull List<ChangedRecipient> changedRecipients,
|
||||
@NonNull MessageRecord messageRecord) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
identityKeyStore.saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
processOutgoingMessageRecord(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_VERIFY_AND_RESEND;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void processOutgoingMessageRecord(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
RecipientId id = changedRecipient.getRecipient().getId();
|
||||
IdentityKey identityKey = changedRecipient.getIdentityRecord().getIdentityKey();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(context, messageRecord, id);
|
||||
} else {
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SafetyNumberChangeState {
|
||||
|
||||
private final List<ChangedRecipient> changedRecipients;
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
SafetyNumberChangeState(List<ChangedRecipient> changedRecipients, @Nullable MessageRecord messageRecord) {
|
||||
this.changedRecipients = changedRecipients;
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
@NonNull List<ChangedRecipient> getChangedRecipients() {
|
||||
return changedRecipients;
|
||||
}
|
||||
|
||||
@Nullable MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository.SafetyNumberChangeState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SafetyNumberChangeViewModel extends ViewModel {
|
||||
|
||||
private final SafetyNumberChangeRepository safetyNumberChangeRepository;
|
||||
private final LiveData<SafetyNumberChangeState> safetyNumberChangeState;
|
||||
|
||||
private SafetyNumberChangeViewModel(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId, SafetyNumberChangeRepository safetyNumberChangeRepository) {
|
||||
this.safetyNumberChangeRepository = safetyNumberChangeRepository;
|
||||
safetyNumberChangeState = this.safetyNumberChangeRepository.getSafetyNumberChangeState(recipientIds, messageId);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<ChangedRecipient>> getChangedRecipients() {
|
||||
return Transformations.map(safetyNumberChangeState, SafetyNumberChangeState::getChangedRecipients);
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients() {
|
||||
SafetyNumberChangeState state = Objects.requireNonNull(safetyNumberChangeState.getValue());
|
||||
if (state.getMessageRecord() != null) {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipientsAndResend(state.getChangedRecipients(), state.getMessageRecord());
|
||||
} else {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipients(state.getChangedRecipients());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
private final List<RecipientId> recipientIds;
|
||||
private final Long messageId;
|
||||
|
||||
public Factory(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
this.recipientIds = recipientIds;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
SafetyNumberChangeRepository repo = new SafetyNumberChangeRepository(ApplicationDependencies.getApplication());
|
||||
return Objects.requireNonNull(modelClass.cast(new SafetyNumberChangeViewModel(recipientIds, messageId, repo)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
public enum TrustAndVerifyResult {
|
||||
TRUST_AND_VERIFY,
|
||||
TRUST_VERIFY_AND_RESEND,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -141,7 +141,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
||||
|
||||
casted.getConversationListItem().bind(conversation.getThreadRecord(),
|
||||
glideRequests,
|
||||
conversation.getLocale(),
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
@@ -213,7 +213,7 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
|
||||
}
|
||||
|
||||
void selectAllThreads() {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
for (int i = 0; i < super.getItemCount(); i++) {
|
||||
Conversation conversation = getItem(i);
|
||||
if (conversation != null && conversation.getThreadRecord().getThreadId() != -1) {
|
||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||
|
||||
@@ -14,19 +14,21 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
|
||||
|
||||
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
|
||||
|
||||
private static final ThrottledDebouncer THROTTLER = new ThrottledDebouncer(500);
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListDataSource.class);
|
||||
|
||||
protected final ThreadDatabase threadDatabase;
|
||||
@@ -37,8 +39,10 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
||||
ContentObserver contentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
invalidate();
|
||||
context.getContentResolver().unregisterContentObserver(this);
|
||||
THROTTLER.publish(() -> {
|
||||
invalidate();
|
||||
context.getContentResolver().unregisterContentObserver(this);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,14 +64,13 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
|
||||
Locale locale = Locale.getDefault();
|
||||
int totalCount = getTotalCount();
|
||||
int effectiveCount = params.requestedStartPosition;
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||
conversations.add(new Conversation(record, locale));
|
||||
conversations.add(new Conversation(record));
|
||||
effectiveCount++;
|
||||
}
|
||||
}
|
||||
@@ -86,12 +89,11 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<Conversation> conversations = new ArrayList<>(params.loadSize);
|
||||
Locale locale = Locale.getDefault();
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
conversations.add(new Conversation(record, locale));
|
||||
conversations.add(new Conversation(record));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,11 @@ import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -54,6 +53,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
@@ -129,6 +129,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
@@ -141,8 +142,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
MegaphoneActionController
|
||||
{
|
||||
public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562;
|
||||
public static final short PROFILE_NAMES_REQUEST_CODE_CREATE_NAME = 18473;
|
||||
public static final short PROFILE_NAMES_REQUEST_CODE_CONFIRM_NAME = 19563;
|
||||
|
||||
private static final String TAG = Log.tag(ConversationListFragment.class);
|
||||
|
||||
@@ -170,6 +169,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
|
||||
public static ConversationListFragment newInstance() {
|
||||
return new ConversationListFragment();
|
||||
@@ -257,6 +257,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
|
||||
if (activeAdapter != null) {
|
||||
activeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -325,20 +329,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isProfileCreatedRequestCode = requestCode == MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME ||
|
||||
requestCode ==PROFILE_NAMES_REQUEST_CODE_CREATE_NAME;
|
||||
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN) {
|
||||
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
|
||||
} else if (isProfileCreatedRequestCode) {
|
||||
Snackbar.make(fab, R.string.ConversationListFragment__your_profile_name_has_been_created, Snackbar.LENGTH_LONG).show();
|
||||
|
||||
if (requestCode == MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME) {
|
||||
viewModel.onMegaphoneCompleted(Megaphones.Event.MESSAGE_REQUESTS);
|
||||
}
|
||||
} else if (requestCode == PROFILE_NAMES_REQUEST_CODE_CONFIRM_NAME) {
|
||||
Snackbar.make(fab, R.string.ConversationListFragment__your_profile_name_has_been_saved, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,11 +955,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
|
||||
@Override
|
||||
public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder.itemView instanceof ConversationListItemAction) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (actionMode != null) {
|
||||
if (viewHolder.itemView instanceof ConversationListItemAction ||
|
||||
actionMode != null ||
|
||||
activeAdapter == searchAdapter)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -984,7 +976,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
|
||||
public void onChildDraw(@NonNull Canvas canvas, @NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dX, float dY, int actionState,
|
||||
boolean isCurrentlyActive)
|
||||
@@ -992,28 +984,32 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
View itemView = viewHolder.itemView;
|
||||
Paint p = new Paint();
|
||||
float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
||||
|
||||
if (dX > 0) {
|
||||
Bitmap icon = BitmapFactory.decodeResource(getResources(), getArchiveIconRes());
|
||||
Resources resources = getResources();
|
||||
|
||||
if (alpha > 0) p.setColor(getResources().getColor(R.color.green_500));
|
||||
else p.setColor(Color.WHITE);
|
||||
if (archiveDrawable == null) {
|
||||
archiveDrawable = ResourcesCompat.getDrawable(resources, getArchiveIconRes(), requireActivity().getTheme());
|
||||
Objects.requireNonNull(archiveDrawable).setBounds(0, 0, archiveDrawable.getIntrinsicWidth(), archiveDrawable.getIntrinsicHeight());
|
||||
}
|
||||
|
||||
c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
|
||||
(float) itemView.getBottom(), p);
|
||||
canvas.save();
|
||||
canvas.clipRect(itemView.getLeft(), itemView.getTop(), dX, itemView.getBottom());
|
||||
|
||||
c.drawBitmap(icon,
|
||||
(float) itemView.getLeft() + getResources().getDimension(R.dimen.conversation_list_fragment_archive_padding),
|
||||
(float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
|
||||
p);
|
||||
canvas.drawColor(alpha > 0 ? resources.getColor(R.color.green_500) : Color.WHITE);
|
||||
|
||||
canvas.translate(itemView.getLeft() + resources.getDimension(R.dimen.conversation_list_fragment_archive_padding),
|
||||
itemView.getTop() + (itemView.getBottom() - itemView.getTop() - archiveDrawable.getIntrinsicHeight()) / 2f);
|
||||
|
||||
archiveDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
viewHolder.itemView.setAlpha(alpha);
|
||||
viewHolder.itemView.setTranslationX(dX);
|
||||
} else {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,27 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Conversation {
|
||||
private final ThreadRecord threadRecord;
|
||||
private final Locale locale;
|
||||
|
||||
public Conversation(@NonNull ThreadRecord threadRecord, @NonNull Locale locale) {
|
||||
public Conversation(@NonNull ThreadRecord threadRecord) {
|
||||
this.threadRecord = threadRecord;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public @NonNull ThreadRecord getThreadRecord() {
|
||||
return threadRecord;
|
||||
}
|
||||
|
||||
public @NonNull Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Conversation that = (Conversation) o;
|
||||
return threadRecord.equals(that.threadRecord) &&
|
||||
locale.equals(that.locale);
|
||||
return threadRecord.equals(that.threadRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(threadRecord, locale);
|
||||
return threadRecord.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ public class AttachmentDatabase extends Database {
|
||||
static final String WIDTH = "width";
|
||||
static final String HEIGHT = "height";
|
||||
static final String CAPTION = "caption";
|
||||
private static final String DATA_HASH = "data_hash";
|
||||
static final String DATA_HASH = "data_hash";
|
||||
static final String VISUAL_HASH = "blur_hash";
|
||||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
@@ -496,14 +496,20 @@ public class AttachmentDatabase extends Database {
|
||||
database.beginTransaction();
|
||||
try {
|
||||
for (AttachmentId weakReference : removableWeakReferences) {
|
||||
Log.i(TAG, String.format("[deleteAttachmentOnDisk] Deleting weak reference for %s %s", data, weakReference));
|
||||
deletedCount += database.delete(TABLE_NAME, PART_ID_WHERE, weakReference.toStrings());
|
||||
Log.i(TAG, String.format("[deleteAttachmentOnDisk] Clearing weak reference for %s %s", data, weakReference));
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(DATA);
|
||||
values.putNull(DATA_RANDOM);
|
||||
values.putNull(DATA_HASH);
|
||||
values.putNull(THUMBNAIL);
|
||||
values.putNull(THUMBNAIL_RANDOM);
|
||||
deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings());
|
||||
}
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
String logMessage = String.format(Locale.US, "[deleteAttachmentOnDisk] Deleted %d/%d weak references for %s", deletedCount, removableWeakReferences.size(), data);
|
||||
String logMessage = String.format(Locale.US, "[deleteAttachmentOnDisk] Cleared %d/%d weak references for %s", deletedCount, removableWeakReferences.size(), data);
|
||||
if (deletedCount != removableWeakReferences.size()) {
|
||||
Log.w(TAG, logMessage);
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@@ -89,6 +91,10 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
if (Stream.of(fullSpecs).map(FullSpec::getJobSpec).allMatch(JobSpec::isMemoryOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
@@ -149,32 +155,38 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
public synchronized void updateJobs(@NonNull List<JobSpec> jobs) {
|
||||
if (Stream.of(jobs).allMatch(JobSpec::isMemoryOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (JobSpec job : jobs) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
values.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
values.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
values.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
values.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
values.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
Stream.of(jobs)
|
||||
.filterNot(JobSpec::isMemoryOnly)
|
||||
.forEach(job -> {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
values.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
values.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
values.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
values.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
values.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ job.getId() };
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ job.getId() };
|
||||
|
||||
db.update(Jobs.TABLE_NAME, values, query, args);
|
||||
}
|
||||
db.update(Jobs.TABLE_NAME, values, query, args);
|
||||
});
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@@ -228,6 +240,10 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
|
||||
if (job.isMemoryOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
@@ -247,21 +263,25 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
|
||||
for (ConstraintSpec constraintSpec : constraints) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
Stream.of(constraints)
|
||||
.filterNot(ConstraintSpec::isMemoryOnly)
|
||||
.forEach(constraintSpec -> {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
});
|
||||
}
|
||||
|
||||
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
|
||||
for (DependencySpec dependencySpec : dependencies) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
Stream.of(dependencies)
|
||||
.filterNot(DependencySpec::isMemoryOnly)
|
||||
.forEach(dependencySpec -> {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
|
||||
@@ -277,16 +297,19 @@ public class JobDatabase extends Database {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_INPUT_DATA)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1);
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1,
|
||||
false);
|
||||
}
|
||||
|
||||
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)),
|
||||
false);
|
||||
}
|
||||
|
||||
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class MediaDatabase extends Database {
|
||||
+ " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND "
|
||||
+ MmsDatabase.VIEW_ONCE + " = 0 AND "
|
||||
+ AttachmentDatabase.DATA + " IS NOT NULL AND "
|
||||
+ AttachmentDatabase.QUOTE + " = 0 AND "
|
||||
+ "(" + AttachmentDatabase.QUOTE + " = 0 OR (" + AttachmentDatabase.QUOTE + " = 1 AND " + AttachmentDatabase.DATA_HASH + " IS NULL)) AND "
|
||||
+ AttachmentDatabase.STICKER_PACK_ID + " IS NULL ";
|
||||
|
||||
private static final String UNIQUE_MEDIA_QUERY = "SELECT "
|
||||
|
||||
@@ -153,13 +153,15 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
}
|
||||
|
||||
public void setAllReactionsSeen() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
String query = REACTIONS_UNREAD + " != ?";
|
||||
String[] args = new String[] { "0" };
|
||||
|
||||
values.put(REACTIONS_UNREAD, 0);
|
||||
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
||||
|
||||
db.update(getTableName(), values, null, null);
|
||||
db.update(getTableName(), values, query, args);
|
||||
}
|
||||
|
||||
public void addReaction(long messageId, @NonNull ReactionRecord reaction) {
|
||||
|
||||
@@ -280,6 +280,18 @@ public class MmsSmsDatabase extends Database {
|
||||
else return id;
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageRecord(long messageId) {
|
||||
try {
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
} catch (NoSuchMessageException e1) {
|
||||
try {
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} catch (NoSuchMessageException e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
|
||||
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true);
|
||||
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true);
|
||||
|
||||
@@ -950,7 +950,7 @@ public class ThreadDatabase extends Database {
|
||||
.setRecipient(recipient)
|
||||
.setType(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE)))
|
||||
.setDistributionType(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE)))
|
||||
.setBody(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET)))
|
||||
.setBody(Util.emptyIfNull(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET))))
|
||||
.setDate(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE)))
|
||||
.setArchived(cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0)
|
||||
.setDeliveryStatus(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS)))
|
||||
|
||||
@@ -137,8 +137,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int LAST_SCROLLED = 62;
|
||||
private static final int LAST_PROFILE_FETCH = 63;
|
||||
private static final int SERVER_DELIVERED_TIMESTAMP = 64;
|
||||
private static final int QUOTE_CLEANUP = 65;
|
||||
|
||||
private static final int DATABASE_VERSION = 64;
|
||||
private static final int DATABASE_VERSION = 65;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -921,6 +922,39 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE push ADD COLUMN server_delivered_timestamp INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < QUOTE_CLEANUP) {
|
||||
String query = "SELECT _data " +
|
||||
"FROM (SELECT _data, MIN(quote) AS all_quotes " +
|
||||
"FROM part " +
|
||||
"WHERE _data NOT NULL AND data_hash NOT NULL " +
|
||||
"GROUP BY _data) " +
|
||||
"WHERE all_quotes = 1";
|
||||
|
||||
int count = 0;
|
||||
|
||||
try (Cursor cursor = db.rawQuery(query, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow("_data"));
|
||||
|
||||
if (new File(data).delete()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull("_data");
|
||||
values.putNull("data_random");
|
||||
values.putNull("thumbnail");
|
||||
values.putNull("thumbnail_random");
|
||||
values.putNull("data_hash");
|
||||
db.update("part", values, "_data = ?", new String[] { data });
|
||||
|
||||
count++;
|
||||
} else {
|
||||
Log.w(TAG, "[QuoteCleanup] Failed to delete " + data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "[QuoteCleanup] Cleaned up " + count + " quotes.");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.thoughtcrime.securesms.database.identity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -24,6 +27,10 @@ public final class IdentityRecordList {
|
||||
identityRecords.addAll(identityRecordList.identityRecords);
|
||||
}
|
||||
|
||||
public List<IdentityRecord> getIdentityRecords() {
|
||||
return Collections.unmodifiableList(identityRecords);
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
|
||||
|
||||
@@ -285,6 +285,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return networkFailures != null && !networkFailures.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFailedWithNetworkFailures() {
|
||||
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
@@ -7,6 +7,15 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate;
|
||||
import org.thoughtcrime.securesms.jobs.MarkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -134,8 +143,10 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
.setJobFactories(JobManagerFactories.getJobFactories(context))
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(context))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(context))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context)))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context), SignalExecutors.newCachedSingleThreadExecutor("signal-fast-job-storage")))
|
||||
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushDecryptMessageJob.KEY, PushProcessMessageJob.KEY, MarkerJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
@@ -21,15 +20,13 @@ import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -37,7 +34,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
||||
public class GiphyActivity extends PassphraseRequiredActivity
|
||||
implements GiphyActivityToolbar.OnLayoutChangedListener,
|
||||
GiphyActivityToolbar.OnFilterChangedListener,
|
||||
GiphyAdapter.OnItemClickListener
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -17,6 +16,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.giph.net.GiphyLoader;
|
||||
@@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class GiphyFragment extends Fragment implements LoaderManager.LoaderCallbacks<List<GiphyImage>>, GiphyAdapter.OnItemClickListener {
|
||||
public abstract class GiphyFragment extends LoggingFragment implements LoaderManager.LoaderCallbacks<List<GiphyImage>>, GiphyAdapter.OnItemClickListener {
|
||||
|
||||
private static final String TAG = GiphyFragment.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements Resou
|
||||
public boolean handles(@NonNull File source, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
Log.i(TAG, "Checking item for encrypted Bitmap cache decoder: " + source.toString());
|
||||
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
@@ -41,12 +39,10 @@ public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements Resou
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
|
||||
public @Nullable Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
Log.i(TAG, "Encrypted Bitmap cache decoder running: " + source.toString());
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ public class EncryptedGifCacheDecoder extends EncryptedCoder implements Resource
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull File source, @NonNull Options options) {
|
||||
Log.i(TAG, "Checking item for encrypted GIF cache decoder: " + source.toString());
|
||||
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
@@ -39,9 +37,8 @@ public class EncryptedGifCacheDecoder extends EncryptedCoder implements Resource
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
public @Nullable Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
Log.i(TAG, "Encrypted GIF cache decoder running...");
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.decode(inputStream, width, height, options);
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class AddGroupDetailsActivity extends PassphraseRequiredActionBarActivity implements AddGroupDetailsFragment.Callback {
|
||||
public class AddGroupDetailsActivity extends PassphraseRequiredActivity implements AddGroupDetailsFragment.Callback {
|
||||
|
||||
private static final String EXTRA_RECIPIENTS = "recipient_ids";
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||
@@ -46,7 +47,7 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class AddGroupDetailsFragment extends Fragment {
|
||||
public class AddGroupDetailsFragment extends LoggingFragment {
|
||||
|
||||
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
|
||||
private static final short REQUEST_CODE_AVATAR = 27621;
|
||||
|
||||
@@ -10,13 +10,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class ManageGroupActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class ManageGroupActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
@@ -53,7 +53,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ManageGroupFragment extends Fragment {
|
||||
public class ManageGroupFragment extends LoggingFragment {
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
||||
|
||||
@@ -7,13 +7,13 @@ import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class PendingMemberInvitesActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class PendingMemberInvitesActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package org.thoughtcrime.securesms.help;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -18,27 +14,23 @@ import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.util.AppSignatureUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class HelpFragment extends Fragment {
|
||||
public class HelpFragment extends LoggingFragment {
|
||||
|
||||
private EditText problem;
|
||||
private CheckBox includeDebugLogs;
|
||||
|
||||
@@ -245,6 +245,7 @@ public abstract class Job {
|
||||
private final String queue;
|
||||
private final List<String> constraintKeys;
|
||||
private final Data inputData;
|
||||
private final boolean memoryOnly;
|
||||
|
||||
private Parameters(@NonNull String id,
|
||||
long createTime,
|
||||
@@ -254,7 +255,8 @@ public abstract class Job {
|
||||
int maxInstances,
|
||||
@Nullable String queue,
|
||||
@NonNull List<String> constraintKeys,
|
||||
@Nullable Data inputData)
|
||||
@Nullable Data inputData,
|
||||
boolean memoryOnly)
|
||||
{
|
||||
this.id = id;
|
||||
this.createTime = createTime;
|
||||
@@ -265,6 +267,7 @@ public abstract class Job {
|
||||
this.queue = queue;
|
||||
this.constraintKeys = constraintKeys;
|
||||
this.inputData = inputData;
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
@NonNull String getId() {
|
||||
@@ -303,8 +306,12 @@ public abstract class Job {
|
||||
return inputData;
|
||||
}
|
||||
|
||||
boolean isMemoryOnly() {
|
||||
return memoryOnly;
|
||||
}
|
||||
|
||||
public Builder toBuilder() {
|
||||
return new Builder(id, createTime, maxBackoff, lifespan, maxAttempts, maxInstances, queue, constraintKeys, inputData);
|
||||
return new Builder(id, createTime, maxBackoff, lifespan, maxAttempts, maxInstances, queue, constraintKeys, inputData, memoryOnly);
|
||||
}
|
||||
|
||||
|
||||
@@ -319,13 +326,14 @@ public abstract class Job {
|
||||
private String queue;
|
||||
private List<String> constraintKeys;
|
||||
private Data inputData;
|
||||
private boolean memoryOnly;
|
||||
|
||||
public Builder() {
|
||||
this(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
Builder(@NonNull String id) {
|
||||
this(id, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(30), IMMORTAL, 1, UNLIMITED, null, new LinkedList<>(), null);
|
||||
this(id, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(30), IMMORTAL, 1, UNLIMITED, null, new LinkedList<>(), null, false);
|
||||
}
|
||||
|
||||
private Builder(@NonNull String id,
|
||||
@@ -336,7 +344,8 @@ public abstract class Job {
|
||||
int maxInstances,
|
||||
@Nullable String queue,
|
||||
@NonNull List<String> constraintKeys,
|
||||
@Nullable Data inputData)
|
||||
@Nullable Data inputData,
|
||||
boolean memoryOnly)
|
||||
{
|
||||
this.id = id;
|
||||
this.createTime = createTime;
|
||||
@@ -347,6 +356,7 @@ public abstract class Job {
|
||||
this.queue = queue;
|
||||
this.constraintKeys = constraintKeys;
|
||||
this.inputData = inputData;
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
/** Should only be invoked by {@link JobController} */
|
||||
@@ -424,6 +434,17 @@ public abstract class Job {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify whether or not you want this job to only live in memory. If true, this job will
|
||||
* *not* survive application death. This defaults to false, and should be used with care.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
public @NonNull Builder setMemoryOnly(boolean memoryOnly) {
|
||||
this.memoryOnly = memoryOnly;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input data that will be made availabe to the job when it is run.
|
||||
* Should only be set by {@link JobController}.
|
||||
@@ -434,7 +455,7 @@ public abstract class Job {
|
||||
}
|
||||
|
||||
public @NonNull Parameters build() {
|
||||
return new Parameters(id, createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys, inputData);
|
||||
return new Parameters(id, createTime, lifespan, maxAttempts, maxBackoff, maxInstances, queue, constraintKeys, inputData, memoryOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ class JobController {
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void flush() {
|
||||
jobStorage.flush();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void submitNewJobChain(@NonNull List<List<Job>> chain) {
|
||||
chain = Stream.of(chain).filterNot(List::isEmpty).toList();
|
||||
@@ -237,11 +242,11 @@ class JobController {
|
||||
* When the job returned from this method has been run, you must call {@link #onJobFinished(Job)}.
|
||||
*/
|
||||
@WorkerThread
|
||||
synchronized @NonNull Job pullNextEligibleJobForExecution() {
|
||||
synchronized @NonNull Job pullNextEligibleJobForExecution(@NonNull JobPredicate predicate) {
|
||||
try {
|
||||
Job job;
|
||||
|
||||
while ((job = getNextEligibleJobForExecution()) == null) {
|
||||
while ((job = getNextEligibleJobForExecution(predicate)) == null) {
|
||||
if (runningJobs.isEmpty()) {
|
||||
debouncer.publish(callback::onEmpty);
|
||||
}
|
||||
@@ -349,14 +354,20 @@ class JobController {
|
||||
job.getParameters().getMaxInstances(),
|
||||
dataSerializer.serialize(job.serialize()),
|
||||
null,
|
||||
false);
|
||||
false,
|
||||
job.getParameters().isMemoryOnly());
|
||||
|
||||
List<ConstraintSpec> constraintSpecs = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(jobSpec.getId(), key))
|
||||
.map(key -> new ConstraintSpec(jobSpec.getId(), key, jobSpec.isMemoryOnly()))
|
||||
.toList();
|
||||
|
||||
List<DependencySpec> dependencySpecs = Stream.of(dependsOn)
|
||||
.map(depends -> new DependencySpec(job.getId(), depends))
|
||||
.map(depends -> {
|
||||
JobSpec dependsOnJobSpec = jobStorage.getJobSpec(depends);
|
||||
boolean memoryOnly = job.getParameters().isMemoryOnly() || (dependsOnJobSpec != null && dependsOnJobSpec.isMemoryOnly());
|
||||
|
||||
return new DependencySpec(job.getId(), depends, memoryOnly);
|
||||
})
|
||||
.toList();
|
||||
|
||||
return new FullSpec(jobSpec, constraintSpecs, dependencySpecs);
|
||||
@@ -366,7 +377,7 @@ class JobController {
|
||||
private void scheduleJobs(@NonNull List<Job> jobs) {
|
||||
for (Job job : jobs) {
|
||||
List<Constraint> constraints = Stream.of(job.getParameters().getConstraintKeys())
|
||||
.map(key -> new ConstraintSpec(job.getId(), key))
|
||||
.map(key -> new ConstraintSpec(job.getId(), key, job.getParameters().isMemoryOnly()))
|
||||
.map(ConstraintSpec::getFactoryKey)
|
||||
.map(constraintInstantiator::instantiate)
|
||||
.toList();
|
||||
@@ -376,8 +387,10 @@ class JobController {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable Job getNextEligibleJobForExecution() {
|
||||
List<JobSpec> jobSpecs = jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis());
|
||||
private @Nullable Job getNextEligibleJobForExecution(@NonNull JobPredicate predicate) {
|
||||
List<JobSpec> jobSpecs = Stream.of(jobStorage.getPendingJobsWithNoDependenciesInCreatedOrder(System.currentTimeMillis()))
|
||||
.filter(predicate::shouldRun)
|
||||
.toList();
|
||||
|
||||
for (JobSpec jobSpec : jobSpecs) {
|
||||
List<ConstraintSpec> constraintSpecs = jobStorage.getConstraintSpecs(jobSpec.getId());
|
||||
@@ -455,7 +468,8 @@ class JobController {
|
||||
jobSpec.getMaxInstances(),
|
||||
jobSpec.getSerializedData(),
|
||||
dataSerializer.serialize(inputData),
|
||||
jobSpec.isRunning());
|
||||
jobSpec.isRunning(),
|
||||
jobSpec.isMemoryOnly());
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.jobmanager;
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
@@ -14,6 +16,8 @@ import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.NonMainThreadExecutor;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -26,9 +30,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -41,18 +43,21 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
|
||||
public static final int CURRENT_VERSION = 7;
|
||||
|
||||
private final Application application;
|
||||
private final Configuration configuration;
|
||||
private final ExecutorService executor;
|
||||
private final JobController jobController;
|
||||
private final JobTracker jobTracker;
|
||||
private final Application application;
|
||||
private final Configuration configuration;
|
||||
private final Executor executor;
|
||||
private final JobController jobController;
|
||||
private final JobTracker jobTracker;
|
||||
|
||||
@GuardedBy("emptyQueueListeners")
|
||||
private final Set<EmptyQueueListener> emptyQueueListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private volatile boolean initialized;
|
||||
|
||||
public JobManager(@NonNull Application application, @NonNull Configuration configuration) {
|
||||
this.application = application;
|
||||
this.configuration = configuration;
|
||||
this.executor = configuration.getExecutorFactory().newSingleThreadExecutor("signal-JobManager");
|
||||
this.executor = new NonMainThreadExecutor(configuration.getExecutorFactory().newSingleThreadExecutor("signal-JobManager"));
|
||||
this.jobTracker = configuration.getJobTracker();
|
||||
this.jobController = new JobController(application,
|
||||
configuration.getJobStorage(),
|
||||
@@ -66,25 +71,30 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
this::onEmptyQueue);
|
||||
|
||||
executor.execute(() -> {
|
||||
if (WorkManagerMigrator.needsMigration(application)) {
|
||||
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
||||
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
||||
}
|
||||
synchronized (this) {
|
||||
if (WorkManagerMigrator.needsMigration(application)) {
|
||||
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
||||
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
||||
}
|
||||
|
||||
JobStorage jobStorage = configuration.getJobStorage();
|
||||
jobStorage.init();
|
||||
JobStorage jobStorage = configuration.getJobStorage();
|
||||
jobStorage.init();
|
||||
|
||||
int latestVersion = configuration.getJobMigrator().migrate(jobStorage, configuration.getDataSerializer());
|
||||
TextSecurePreferences.setJobManagerVersion(application, latestVersion);
|
||||
int latestVersion = configuration.getJobMigrator().migrate(jobStorage, configuration.getDataSerializer());
|
||||
TextSecurePreferences.setJobManagerVersion(application, latestVersion);
|
||||
|
||||
jobController.init();
|
||||
jobController.init();
|
||||
|
||||
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
||||
constraintObserver.register(this);
|
||||
}
|
||||
for (ConstraintObserver constraintObserver : configuration.getConstraintObservers()) {
|
||||
constraintObserver.register(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
application.startService(new Intent(application, KeepAliveService.class));
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
application.startService(new Intent(application, KeepAliveService.class));
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -93,11 +103,18 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
* Begins the execution of jobs.
|
||||
*/
|
||||
public void beginJobLoop() {
|
||||
executor.execute(() -> {
|
||||
runOnExecutor(()-> {
|
||||
int id = 0;
|
||||
|
||||
for (int i = 0; i < configuration.getJobThreadCount(); i++) {
|
||||
new JobRunner(application, i + 1, jobController).start();
|
||||
new JobRunner(application, ++id, jobController, JobPredicate.NONE).start();
|
||||
}
|
||||
wakeUp();
|
||||
|
||||
for (JobPredicate predicate : configuration.getReservedJobRunners()) {
|
||||
new JobRunner(application, ++id, jobController, predicate).start();
|
||||
}
|
||||
|
||||
jobController.wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,9 +155,9 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
public void add(@NonNull Job job, @NonNull Collection<String> dependsOn) {
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
|
||||
executor.execute(() -> {
|
||||
runOnExecutor(() -> {
|
||||
jobController.submitJobWithExistingDependencies(job, dependsOn, null);
|
||||
wakeUp();
|
||||
jobController.wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,9 +168,9 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
public void add(@NonNull Job job, @Nullable String dependsOnQueue) {
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
|
||||
executor.execute(() -> {
|
||||
runOnExecutor(() -> {
|
||||
jobController.submitJobWithExistingDependencies(job, Collections.emptyList(), dependsOnQueue);
|
||||
wakeUp();
|
||||
jobController.wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,9 +181,9 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
public void add(@NonNull Job job, @NonNull Collection<String> dependsOn, @Nullable String dependsOnQueue) {
|
||||
jobTracker.onStateChange(job, JobTracker.JobState.PENDING);
|
||||
|
||||
executor.execute(() -> {
|
||||
runOnExecutor(() -> {
|
||||
jobController.submitJobWithExistingDependencies(job, dependsOn, dependsOnQueue);
|
||||
wakeUp();
|
||||
jobController.wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,14 +212,14 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
* moment. Just like a normal failure, all later jobs in the same chain will also be failed.
|
||||
*/
|
||||
public void cancel(@NonNull String id) {
|
||||
executor.execute(() -> jobController.cancelJob(id));
|
||||
runOnExecutor(() -> jobController.cancelJob(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all jobs in the specified queue. See {@link #cancel(String)} for details.
|
||||
*/
|
||||
public void cancelAllInQueue(@NonNull String queue) {
|
||||
executor.execute(() -> jobController.cancelAllInQueue(queue));
|
||||
runOnExecutor(() -> jobController.cancelAllInQueue(queue));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,10 +264,22 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
*/
|
||||
@WorkerThread
|
||||
public @NonNull String getDebugInfo() {
|
||||
Future<String> result = executor.submit(jobController::getDebugInfo);
|
||||
AtomicReference<String> result = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
runOnExecutor(() -> {
|
||||
result.set(jobController.getDebugInfo());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
return result.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
boolean finished = latch.await(10, TimeUnit.SECONDS);
|
||||
if (finished) {
|
||||
return result.get();
|
||||
} else {
|
||||
return "Timed out waiting for Job info.";
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Failed to retrieve Job info.", e);
|
||||
return "Failed to retrieve Job info.";
|
||||
}
|
||||
@@ -260,8 +289,10 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
* Adds a listener that will be notified when the job queue has been drained.
|
||||
*/
|
||||
void addOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.add(listener);
|
||||
runOnExecutor(() -> {
|
||||
synchronized (emptyQueueListeners) {
|
||||
emptyQueueListeners.add(listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,8 +300,10 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
* Removes a listener that was added via {@link #addOnEmptyQueueListener(EmptyQueueListener)}.
|
||||
*/
|
||||
void removeOnEmptyQueueListener(@NonNull EmptyQueueListener listener) {
|
||||
executor.execute(() -> {
|
||||
emptyQueueListeners.remove(listener);
|
||||
runOnExecutor(() -> {
|
||||
synchronized (emptyQueueListeners) {
|
||||
emptyQueueListeners.remove(listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,11 +313,31 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
wakeUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks until all pending operations are finished.
|
||||
*/
|
||||
@WorkerThread
|
||||
public void flush() {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
runOnExecutor(() -> {
|
||||
jobController.flush();
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
Log.i(TAG, "Successfully flushed.");
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Failed to finish flushing.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokes the system to take another pass at the job queue.
|
||||
*/
|
||||
void wakeUp() {
|
||||
executor.execute(jobController::wakeUp);
|
||||
runOnExecutor(jobController::wakeUp);
|
||||
}
|
||||
|
||||
private void enqueueChain(@NonNull Chain chain) {
|
||||
@@ -294,20 +347,46 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
runOnExecutor(() -> {
|
||||
jobController.submitNewJobChain(chain.getJobListChain());
|
||||
wakeUp();
|
||||
jobController.wakeUp();
|
||||
});
|
||||
}
|
||||
|
||||
private void onEmptyQueue() {
|
||||
executor.execute(() -> {
|
||||
for (EmptyQueueListener listener : emptyQueueListeners) {
|
||||
listener.onQueueEmpty();
|
||||
runOnExecutor(() -> {
|
||||
synchronized (emptyQueueListeners) {
|
||||
for (EmptyQueueListener listener : emptyQueueListeners) {
|
||||
listener.onQueueEmpty();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Anything that you want to ensure happens off of the main thread and after initialization, run
|
||||
* it through here.
|
||||
*/
|
||||
private void runOnExecutor(@NonNull Runnable runnable) {
|
||||
executor.execute(() -> {
|
||||
waitUntilInitialized();
|
||||
runnable.run();
|
||||
});
|
||||
}
|
||||
|
||||
private void waitUntilInitialized() {
|
||||
if (!initialized) {
|
||||
Log.i(TAG, "Waiting for initialization...");
|
||||
synchronized (this) {
|
||||
while (!initialized) {
|
||||
Util.wait(this, 0);
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Initialization complete.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface EmptyQueueListener {
|
||||
void onQueueEmpty();
|
||||
}
|
||||
@@ -373,6 +452,7 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
private final JobStorage jobStorage;
|
||||
private final JobMigrator jobMigrator;
|
||||
private final JobTracker jobTracker;
|
||||
private final List<JobPredicate> reservedJobRunners;
|
||||
|
||||
private Configuration(int jobThreadCount,
|
||||
@NonNull ExecutorFactory executorFactory,
|
||||
@@ -382,17 +462,19 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
@NonNull Data.Serializer dataSerializer,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull JobMigrator jobMigrator,
|
||||
@NonNull JobTracker jobTracker)
|
||||
@NonNull JobTracker jobTracker,
|
||||
@NonNull List<JobPredicate> reservedJobRunners)
|
||||
{
|
||||
this.executorFactory = executorFactory;
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
this.jobInstantiator = jobInstantiator;
|
||||
this.constraintInstantiator = constraintInstantiator;
|
||||
this.constraintObservers = constraintObservers;
|
||||
this.constraintObservers = new ArrayList<>(constraintObservers);
|
||||
this.dataSerializer = dataSerializer;
|
||||
this.jobStorage = jobStorage;
|
||||
this.jobMigrator = jobMigrator;
|
||||
this.jobTracker = jobTracker;
|
||||
this.reservedJobRunners = new ArrayList<>(reservedJobRunners);
|
||||
}
|
||||
|
||||
int getJobThreadCount() {
|
||||
@@ -432,6 +514,10 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
return jobTracker;
|
||||
}
|
||||
|
||||
@NonNull List<JobPredicate> getReservedJobRunners() {
|
||||
return reservedJobRunners;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private ExecutorFactory executorFactory = new DefaultExecutorFactory();
|
||||
@@ -443,12 +529,18 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
private JobStorage jobStorage = null;
|
||||
private JobMigrator jobMigrator = null;
|
||||
private JobTracker jobTracker = new JobTracker();
|
||||
private List<JobPredicate> reservedJobRunners = new ArrayList<>();
|
||||
|
||||
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
|
||||
this.jobThreadCount = jobThreadCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder addReservedJobRunner(@NonNull JobPredicate predicate) {
|
||||
this.reservedJobRunners.add(predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) {
|
||||
this.executorFactory = executorFactory;
|
||||
return this;
|
||||
@@ -493,7 +585,8 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
dataSerializer,
|
||||
jobStorage,
|
||||
jobMigrator,
|
||||
jobTracker);
|
||||
jobTracker,
|
||||
reservedJobRunners);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,8 @@ public class JobMigrator {
|
||||
jobSpec.getMaxInstances(),
|
||||
dataSerializer.serialize(updatedJobData.getData()),
|
||||
jobSpec.getSerializedInputData(),
|
||||
jobSpec.isRunning());
|
||||
jobSpec.isRunning(),
|
||||
jobSpec.isMemoryOnly());
|
||||
|
||||
iter.set(updatedJobSpec);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
public interface JobPredicate {
|
||||
JobPredicate NONE = jobSpec -> true;
|
||||
|
||||
boolean shouldRun(@NonNull JobSpec jobSpec);
|
||||
}
|
||||
@@ -29,20 +29,22 @@ class JobRunner extends Thread {
|
||||
private final Application application;
|
||||
private final int id;
|
||||
private final JobController jobController;
|
||||
private final JobPredicate jobPredicate;
|
||||
|
||||
JobRunner(@NonNull Application application, int id, @NonNull JobController jobController) {
|
||||
JobRunner(@NonNull Application application, int id, @NonNull JobController jobController, @NonNull JobPredicate predicate) {
|
||||
super("signal-JobRunner-" + id);
|
||||
|
||||
this.application = application;
|
||||
this.id = id;
|
||||
this.jobController = jobController;
|
||||
this.jobPredicate = predicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void run() {
|
||||
//noinspection InfiniteLoopStatement
|
||||
while (true) {
|
||||
Job job = jobController.pullNextEligibleJobForExecution();
|
||||
Job job = jobController.pullNextEligibleJobForExecution(jobPredicate);
|
||||
Job.Result result = run(job);
|
||||
|
||||
jobController.onJobFinished(job);
|
||||
@@ -67,6 +69,7 @@ class JobRunner extends Thread {
|
||||
}
|
||||
|
||||
private Job.Result run(@NonNull Job job) {
|
||||
long runStartTime = System.currentTimeMillis();
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Running job."));
|
||||
|
||||
if (isJobExpired(job)) {
|
||||
@@ -94,7 +97,7 @@ class JobRunner extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
printResult(job, result);
|
||||
printResult(job, result, runStartTime);
|
||||
|
||||
if (result.isRetry() &&
|
||||
job.getRunAttempt() + 1 >= job.getParameters().getMaxAttempts() &&
|
||||
@@ -117,13 +120,13 @@ class JobRunner extends Thread {
|
||||
return job.getParameters().getLifespan() != Job.Parameters.IMMORTAL && expirationTime <= System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void printResult(@NonNull Job job, @NonNull Job.Result result) {
|
||||
private void printResult(@NonNull Job job, @NonNull Job.Result result, long runStartTime) {
|
||||
if (result.getException() != null) {
|
||||
Log.e(TAG, JobLogger.format(job, String.valueOf(id), "Job failed with a fatal exception. Crash imminent."));
|
||||
} else if (result.isFailure()) {
|
||||
Log.w(TAG, JobLogger.format(job, String.valueOf(id), "Job failed."));
|
||||
} else {
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result: " + result));
|
||||
Log.i(TAG, JobLogger.format(job, String.valueOf(id), "Job finished with result " + result + " in " + (System.currentTimeMillis() - runStartTime) + " ms."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.JobPredicate;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@link JobPredicate} that will only run jobs with the provided factory keys.
|
||||
*/
|
||||
public final class FactoryJobPredicate implements JobPredicate {
|
||||
|
||||
private final Set<String> factories;
|
||||
|
||||
public FactoryJobPredicate(String... factories) {
|
||||
this.factories = new HashSet<>(Arrays.asList(factories));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRun(@NonNull JobSpec jobSpec) {
|
||||
return factories.contains(jobSpec.getFactoryKey());
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,18 @@ public class WebsocketDrainedConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = WebsocketDrainedConstraintObserver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
private volatile Notifier notifier;
|
||||
|
||||
public WebsocketDrainedConstraintObserver() {
|
||||
ApplicationDependencies.getInitialMessageRetriever().addListener(() -> {
|
||||
notifier.onConstraintMet(REASON);
|
||||
if (notifier != null) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import java.util.Objects;
|
||||
|
||||
public final class ConstraintSpec {
|
||||
|
||||
private final String jobSpecId;
|
||||
private final String factoryKey;
|
||||
private final String jobSpecId;
|
||||
private final String factoryKey;
|
||||
private final boolean memoryOnly;
|
||||
|
||||
public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey) {
|
||||
public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey, boolean memoryOnly) {
|
||||
this.jobSpecId = jobSpecId;
|
||||
this.factoryKey = factoryKey;
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
public String getJobSpecId() {
|
||||
@@ -22,22 +24,27 @@ public final class ConstraintSpec {
|
||||
return factoryKey;
|
||||
}
|
||||
|
||||
public boolean isMemoryOnly() {
|
||||
return memoryOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ConstraintSpec that = (ConstraintSpec) o;
|
||||
return Objects.equals(jobSpecId, that.jobSpecId) &&
|
||||
Objects.equals(factoryKey, that.factoryKey);
|
||||
return Objects.equals(jobSpecId, that.jobSpecId) &&
|
||||
Objects.equals(factoryKey, that.factoryKey) &&
|
||||
memoryOnly == that.memoryOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobSpecId, factoryKey);
|
||||
return Objects.hash(jobSpecId, factoryKey, memoryOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: JOB::%s | factoryKey: %s", jobSpecId, factoryKey);
|
||||
return String.format("jobSpecId: JOB::%s | factoryKey: %s | memoryOnly: %b", jobSpecId, factoryKey, memoryOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import java.util.Objects;
|
||||
|
||||
public final class DependencySpec {
|
||||
|
||||
private final String jobId;
|
||||
private final String dependsOnJobId;
|
||||
private final String jobId;
|
||||
private final String dependsOnJobId;
|
||||
private final boolean memoryOnly;
|
||||
|
||||
public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId) {
|
||||
public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId, boolean memoryOnly) {
|
||||
this.jobId = jobId;
|
||||
this.dependsOnJobId = dependsOnJobId;
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
public @NonNull String getJobId() {
|
||||
@@ -22,22 +24,27 @@ public final class DependencySpec {
|
||||
return dependsOnJobId;
|
||||
}
|
||||
|
||||
public boolean isMemoryOnly() {
|
||||
return memoryOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DependencySpec that = (DependencySpec) o;
|
||||
return Objects.equals(jobId, that.jobId) &&
|
||||
Objects.equals(dependsOnJobId, that.dependsOnJobId);
|
||||
return Objects.equals(jobId, that.jobId) &&
|
||||
Objects.equals(dependsOnJobId, that.dependsOnJobId) &&
|
||||
memoryOnly == that.memoryOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(jobId, dependsOnJobId);
|
||||
return Objects.hash(jobId, dependsOnJobId, memoryOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("jobSpecId: JOB::%s | dependsOnJobSpecId: JOB::%s", jobId, dependsOnJobId);
|
||||
return String.format("jobSpecId: JOB::%s | dependsOnJobSpecId: JOB::%s | memoryOnly: %b", jobId, dependsOnJobId, memoryOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ public final class FullSpec {
|
||||
return dependencySpecs;
|
||||
}
|
||||
|
||||
public boolean isMemoryOnly() {
|
||||
return jobSpec.isMemoryOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
@@ -21,6 +21,7 @@ public final class JobSpec {
|
||||
private final String serializedData;
|
||||
private final String serializedInputData;
|
||||
private final boolean isRunning;
|
||||
private final boolean memoryOnly;
|
||||
|
||||
public JobSpec(@NonNull String id,
|
||||
@NonNull String factoryKey,
|
||||
@@ -34,7 +35,8 @@ public final class JobSpec {
|
||||
int maxInstances,
|
||||
@NonNull String serializedData,
|
||||
@Nullable String serializedInputData,
|
||||
boolean isRunning)
|
||||
boolean isRunning,
|
||||
boolean memoryOnly)
|
||||
{
|
||||
this.id = id;
|
||||
this.factoryKey = factoryKey;
|
||||
@@ -49,6 +51,7 @@ public final class JobSpec {
|
||||
this.serializedData = serializedData;
|
||||
this.serializedInputData = serializedInputData;
|
||||
this.isRunning = isRunning;
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
public @NonNull String getId() {
|
||||
@@ -103,6 +106,10 @@ public final class JobSpec {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
public boolean isMemoryOnly() {
|
||||
return memoryOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@@ -116,6 +123,7 @@ public final class JobSpec {
|
||||
lifespan == jobSpec.lifespan &&
|
||||
maxInstances == jobSpec.maxInstances &&
|
||||
isRunning == jobSpec.isRunning &&
|
||||
memoryOnly == jobSpec.memoryOnly &&
|
||||
Objects.equals(id, jobSpec.id) &&
|
||||
Objects.equals(factoryKey, jobSpec.factoryKey) &&
|
||||
Objects.equals(queueKey, jobSpec.queueKey) &&
|
||||
@@ -125,13 +133,13 @@ public final class JobSpec {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, serializedInputData, isRunning);
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, serializedInputData, isRunning, memoryOnly);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s | inputData: %s",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData, serializedInputData);
|
||||
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | memoryOnly: %b",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, memoryOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ public interface JobStorage {
|
||||
@WorkerThread
|
||||
void init();
|
||||
|
||||
@WorkerThread
|
||||
void flush();
|
||||
|
||||
@WorkerThread
|
||||
void insertJobs(@NonNull List<FullSpec> fullSpecs);
|
||||
|
||||
|
||||
@@ -69,12 +69,13 @@ final class WorkManagerDatabase extends SQLiteOpenHelper {
|
||||
Job.Parameters.UNLIMITED,
|
||||
dataSerializer.serialize(DataMigrator.convert(data)),
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
|
||||
|
||||
|
||||
if (cursor.getInt(cursor.getColumnIndexOrThrow("required_network_type")) != 0) {
|
||||
constraints.add(new ConstraintSpec(id, NetworkConstraint.KEY));
|
||||
constraints.add(new ConstraintSpec(id, NetworkConstraint.KEY, false));
|
||||
}
|
||||
|
||||
fullSpecs.add(new FullSpec(jobSpec, constraints, Collections.emptyList()));
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@@ -26,17 +27,23 @@ import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class FastJobStorage implements JobStorage {
|
||||
|
||||
private static final String TAG = Log.tag(FastJobStorage.class);
|
||||
|
||||
private final JobDatabase jobDatabase;
|
||||
private final Executor serialExecutor;
|
||||
|
||||
private final List<JobSpec> jobs;
|
||||
private final Map<String, List<ConstraintSpec>> constraintsByJobId;
|
||||
private final Map<String, List<DependencySpec>> dependenciesByJobId;
|
||||
|
||||
public FastJobStorage(@NonNull JobDatabase jobDatabase) {
|
||||
public FastJobStorage(@NonNull JobDatabase jobDatabase, @NonNull Executor serialExecutor) {
|
||||
this.jobDatabase = jobDatabase;
|
||||
this.serialExecutor = serialExecutor;
|
||||
this.jobs = new ArrayList<>();
|
||||
this.constraintsByJobId = new HashMap<>();
|
||||
this.dependenciesByJobId = new HashMap<>();
|
||||
@@ -63,9 +70,27 @@ public class FastJobStorage implements JobStorage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
serialExecutor.execute(latch::countDown);
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted while waiting to flush!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
jobDatabase.insertJobs(fullSpecs);
|
||||
List<FullSpec> durable = Stream.of(fullSpecs).filterNot(FullSpec::isMemoryOnly).toList();
|
||||
if (durable.size() > 0) {
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.insertJobs(durable);
|
||||
});
|
||||
}
|
||||
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
jobs.add(fullSpec.getJobSpec());
|
||||
@@ -146,7 +171,12 @@ public class FastJobStorage implements JobStorage {
|
||||
|
||||
@Override
|
||||
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
|
||||
jobDatabase.updateJobRunningState(id, isRunning);
|
||||
JobSpec job = getJobById(id);
|
||||
if (job == null || !job.isMemoryOnly()) {
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.updateJobRunningState(id, isRunning);
|
||||
});
|
||||
}
|
||||
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
@@ -165,7 +195,8 @@ public class FastJobStorage implements JobStorage {
|
||||
existing.getMaxInstances(),
|
||||
existing.getSerializedData(),
|
||||
existing.getSerializedInputData(),
|
||||
isRunning);
|
||||
isRunning,
|
||||
existing.isMemoryOnly());
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
@@ -173,7 +204,12 @@ public class FastJobStorage implements JobStorage {
|
||||
|
||||
@Override
|
||||
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime, @NonNull String serializedData) {
|
||||
jobDatabase.updateJobAfterRetry(id, isRunning, runAttempt, nextRunAttemptTime, serializedData);
|
||||
JobSpec job = getJobById(id);
|
||||
if (job == null || !job.isMemoryOnly()) {
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.updateJobAfterRetry(id, isRunning, runAttempt, nextRunAttemptTime, serializedData);
|
||||
});
|
||||
}
|
||||
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
@@ -192,7 +228,8 @@ public class FastJobStorage implements JobStorage {
|
||||
existing.getMaxInstances(),
|
||||
serializedData,
|
||||
existing.getSerializedInputData(),
|
||||
isRunning);
|
||||
isRunning,
|
||||
existing.isMemoryOnly());
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
@@ -200,8 +237,9 @@ public class FastJobStorage implements JobStorage {
|
||||
|
||||
@Override
|
||||
public synchronized void updateAllJobsToBePending() {
|
||||
jobDatabase.updateAllJobsToBePending();
|
||||
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.updateAllJobsToBePending();
|
||||
});
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
@@ -218,14 +256,27 @@ public class FastJobStorage implements JobStorage {
|
||||
existing.getMaxInstances(),
|
||||
existing.getSerializedData(),
|
||||
existing.getSerializedInputData(),
|
||||
false);
|
||||
false,
|
||||
existing.isMemoryOnly());
|
||||
iter.set(updated);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateJobs(@NonNull List<JobSpec> jobSpecs) {
|
||||
jobDatabase.updateJobs(jobSpecs);
|
||||
List<JobSpec> durable = new ArrayList<>(jobSpecs.size());
|
||||
for (JobSpec update : jobSpecs) {
|
||||
JobSpec found = getJobById(update.getId());
|
||||
if (found == null || !found.isMemoryOnly()) {
|
||||
durable.add(update);
|
||||
}
|
||||
}
|
||||
|
||||
if (durable.size() > 0) {
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.updateJobs(durable);
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, JobSpec> updates = Stream.of(jobSpecs).collect(Collectors.toMap(JobSpec::getId));
|
||||
ListIterator<JobSpec> iter = jobs.listIterator();
|
||||
@@ -247,7 +298,19 @@ public class FastJobStorage implements JobStorage {
|
||||
|
||||
@Override
|
||||
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
|
||||
jobDatabase.deleteJobs(jobIds);
|
||||
List<String> durableIds = new ArrayList<>(jobIds.size());
|
||||
for (String id : jobIds) {
|
||||
JobSpec job = getJobById(id);
|
||||
if (job == null || !job.isMemoryOnly()) {
|
||||
durableIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (durableIds.size() > 0) {
|
||||
serialExecutor.execute(() -> {
|
||||
jobDatabase.deleteJobs(durableIds);
|
||||
});
|
||||
}
|
||||
|
||||
Set<String> deleteIds = new HashSet<>(jobIds);
|
||||
|
||||
@@ -323,4 +386,14 @@ public class FastJobStorage implements JobStorage {
|
||||
.flatMap(Stream::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private JobSpec getJobById(@NonNull String id) {
|
||||
for (JobSpec job : jobs) {
|
||||
if (job.getId().equals(id)) {
|
||||
return job;
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Was looking for job with ID JOB::" + id + ", but it doesn't exist in memory!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,9 @@ public class PushGroupSendJob extends PushSendJob {
|
||||
} else if (!identityMismatches.isEmpty()) {
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
|
||||
List<RecipientId> mismatchRecipientIds = Stream.of(identityMismatches).map(mismatch -> mismatch.getRecipientId(context)).toList();
|
||||
RetrieveProfileJob.enqueue(mismatchRecipientIds);
|
||||
}
|
||||
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
||||
warn(TAG, e);
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
@@ -165,8 +166,10 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
warn(TAG, "Failure", uie);
|
||||
database.addMismatchedIdentity(messageId, Recipient.external(context, uie.getIdentifier()).getId(), uie.getIdentityKey());
|
||||
RecipientId recipientId = Recipient.external(context, uie.getIdentifier()).getId();
|
||||
database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey());
|
||||
database.markAsSentFailed(messageId);
|
||||
RetrieveProfileJob.enqueue(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
@@ -122,9 +124,11 @@ public class PushTextSendJob extends PushSendJob {
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
|
||||
} catch (UntrustedIdentityException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
database.addMismatchedIdentity(record.getId(), Recipient.external(context, e.getIdentifier()).getId(), e.getIdentityKey());
|
||||
RecipientId recipientId = Recipient.external(context, e.getIdentifier()).getId();
|
||||
database.addMismatchedIdentity(record.getId(), recipientId, e.getIdentityKey());
|
||||
database.markAsSentFailed(record.getId());
|
||||
database.markAsPush(record.getId());
|
||||
RetrieveProfileJob.enqueue(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -69,13 +70,18 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
registrationLockV1 = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin());
|
||||
SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin());
|
||||
Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() +
|
||||
"\n Capabilities:" +
|
||||
"\n Storage? " + capabilities.isStorage() +
|
||||
"\n GV2? " + capabilities.isGv2() +
|
||||
"\n UUID? " + capabilities.isUuid()) ;
|
||||
|
||||
SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages,
|
||||
registrationLockV1, registrationLockV2,
|
||||
unidentifiedAccessKey, universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(kbsValues.hasPin()));
|
||||
capabilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -84,6 +84,17 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the necessary job to refresh the profile of the requested recipient. Works for any
|
||||
* RecipientId, including individuals, groups, or yourself.
|
||||
*
|
||||
* Identical to {@link #enqueue(Collection)})}
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull RecipientId recipientId) {
|
||||
ApplicationDependencies.getJobManager().add(forRecipient(recipientId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the necessary jobs to refresh the profiles of the requested recipients. Works for any
|
||||
* RecipientIds, including individuals, groups, or yourself.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.net.Network;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -10,6 +12,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
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.recipients.RecipientUtil;
|
||||
@@ -43,6 +46,8 @@ public class TypingSendJob extends BaseJob {
|
||||
.setQueue(getQueue(threadId))
|
||||
.setMaxAttempts(1)
|
||||
.setLifespan(TimeUnit.SECONDS.toMillis(5))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMemoryOnly(true)
|
||||
.build(),
|
||||
threadId,
|
||||
typing);
|
||||
|
||||
@@ -13,16 +13,16 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
|
||||
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends Fragment {
|
||||
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends LoggingFragment {
|
||||
|
||||
private TextView title;
|
||||
private LearnMoreTextView description;
|
||||
|
||||
@@ -10,14 +10,14 @@ import androidx.annotation.Nullable;
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.BaseActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class CreateKbsPinActivity extends BaseActionBarActivity {
|
||||
public class CreateKbsPinActivity extends BaseActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = 27698;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.BaseActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class KbsMigrationActivity extends BaseActionBarActivity {
|
||||
public class KbsMigrationActivity extends BaseActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = CreateKbsPinActivity.REQUEST_NEW_PIN;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
@@ -19,6 +20,7 @@ public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionH
|
||||
Log.e(TAG, "", e);
|
||||
SignalStore.blockUntilAllWritesFinished();
|
||||
Log.blockUntilAllWritesFinished();
|
||||
ApplicationDependencies.getJobManager().flush();
|
||||
originalHandler.uncaughtException(t, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.logsubmit;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.AppCapabilities;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
|
||||
public final class LogSectionCapabilities implements LogSection {
|
||||
|
||||
@Override
|
||||
public @NonNull String getTitle() {
|
||||
return "CAPABILITIES";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CharSequence getContent(@NonNull Context context) {
|
||||
Recipient self = Recipient.self();
|
||||
if (!self.isRegistered()) {
|
||||
return "Unregistered";
|
||||
} else {
|
||||
SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(false);
|
||||
|
||||
return new StringBuilder().append("Local device UUID : ").append(capabilities.isUuid()).append("\n")
|
||||
.append("Global UUID : ").append(self.getUuidCapability()).append("\n")
|
||||
.append("Local device GV2 : ").append(capabilities.isGv2()).append("\n")
|
||||
.append("Global GV2 : ").append(self.getGroupsV2Capability()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.BaseActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SubmitDebugLogActivity extends BaseActionBarActivity implements SubmitDebugLogAdapter.Listener {
|
||||
public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugLogAdapter.Listener {
|
||||
|
||||
private RecyclerView lineList;
|
||||
private SubmitDebugLogAdapter adapter;
|
||||
@@ -225,7 +225,7 @@ public class SubmitDebugLogActivity extends BaseActionBarActivity implements Sub
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.SubmitDebugLogActivity_success)
|
||||
.setCancelable(false)
|
||||
.setNeutralButton(R.string.SubmitDebugLogActivity_ok, (d, w) -> finish())
|
||||
.setNeutralButton(android.R.string.ok, (d, w) -> finish())
|
||||
.setPositiveButton(R.string.SubmitDebugLogActivity_share, (d, w) -> {
|
||||
ShareCompat.IntentBuilder.from(this)
|
||||
.setText(url)
|
||||
|
||||
@@ -60,6 +60,7 @@ public class SubmitDebugLogRepository {
|
||||
}
|
||||
add(new LogSectionPin());
|
||||
add(new LogSectionThreads());
|
||||
add(new LogSectionCapabilities());
|
||||
add(new LogSectionFeatureFlags());
|
||||
add(new LogSectionPermissions());
|
||||
add(new LogSectionLogcat());
|
||||
|
||||
@@ -21,7 +21,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
@@ -34,10 +34,9 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class LongMessageActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String KEY_CONVERSATION_RECIPIENT = "recipient_id";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
@@ -35,7 +35,7 @@ import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -53,7 +53,7 @@ import java.util.List;
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
public final class MediaOverviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
@@ -36,6 +35,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
@@ -50,9 +50,9 @@ import java.io.ByteArrayOutputStream;
|
||||
/**
|
||||
* Camera capture implemented with the legacy camera API's. Should only be used if sdk < 21.
|
||||
*/
|
||||
public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||
TextureView.SurfaceTextureListener,
|
||||
Camera1Controller.EventListener
|
||||
public class Camera1Fragment extends LoggingFragment implements CameraFragment,
|
||||
TextureView.SurfaceTextureListener,
|
||||
Camera1Controller.EventListener
|
||||
{
|
||||
|
||||
private static final String TAG = Camera1Fragment.class.getSimpleName();
|
||||
|
||||
@@ -22,12 +22,12 @@ import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.InviteActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
@@ -35,7 +35,7 @@ import java.util.List;
|
||||
/**
|
||||
* Fragment that selects Signal contacts. Intended to be used in the camera-first capture flow.
|
||||
*/
|
||||
public class CameraContactSelectionFragment extends Fragment implements CameraContactAdapter.CameraContactListener {
|
||||
public class CameraContactSelectionFragment extends LoggingFragment implements CameraContactAdapter.CameraContactListener {
|
||||
|
||||
private Controller controller;
|
||||
private MediaSendViewModel mediaSendViewModel;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user