Compare commits

...

44 Commits

Author SHA1 Message Date
Greyson Parrelli
3380293923 Bump version to 4.65.1 2020-06-26 15:40:23 -04:00
Greyson Parrelli
a549c1ec8b Updated language translations. 2020-06-26 15:38:48 -04:00
Greyson Parrelli
ad84997ce0 Fix display of quotes in 'All Media' view. 2020-06-26 15:33:08 -04:00
Alan Evans
42e2576813 Prevent repeat attempts when waveforms cannot be generated. 2020-06-26 16:18:27 -03:00
Cody Henthorne
31b995fa98 Retrieve profiles on mismatch to notify user of updates quicker. 2020-06-26 14:25:39 -03:00
Greyson Parrelli
0364bec995 Allow skipping if you hit a network error during PIN restore. 2020-06-26 14:25:39 -03:00
Alan Evans
aa39f3d0a3 Fix create new pin option in registration flow. 2020-06-26 13:29:00 -03:00
Greyson Parrelli
db545f43ea Remove profile name reminder megaphone. 2020-06-26 11:52:00 -04:00
Cody Henthorne
bbe003a454 Improve messaging and UX around safety number changes. 2020-06-26 11:10:54 -04:00
Greyson Parrelli
819f0f68f6 Fix issue with some search results returning empty. 2020-06-26 10:46:44 -04:00
Greyson Parrelli
8c0160937b Fix crash with 'select all' in conversation list.
Fixes #9790
2020-06-26 10:12:16 -04:00
Cody Henthorne
6de789dfe3 Prevent attachment download button re-animation. 2020-06-26 10:10:34 -04:00
Greyson Parrelli
afa2bb3bf5 Disallow swipe actions in search mode.
Fixes #9771
2020-06-26 10:08:01 -04:00
Greyson Parrelli
89e66c0741 Bump version to 4.65.0 2020-06-25 18:14:54 -04:00
Greyson Parrelli
0dc4afba99 Updated language translations. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
152578e576 Add reserved job runners for inbound and outbound messages. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
63d6ab6fa7 Throttle conversation list update frequency.
This helps fast phones process messages faster by reducing contention on
the database while processing a large batch of messages.
2020-06-25 18:14:54 -04:00
Greyson Parrelli
75c8c59d78 Reduce notification update interval. 2020-06-25 18:14:54 -04:00
Greyson Parrelli
87a59b6a9b Add support for memory-only jobs. 2020-06-25 18:14:54 -04:00
Alan Evans
2001fa86cf Log capabilities. 2020-06-25 18:14:54 -04:00
Alan Evans
52747782a7 Full screen avatar circle to square shape transition. 2020-06-25 18:14:54 -04:00
Fumiaki Yoshimatsu
66f2668326 Do not cache locale in each conversation object.
Fixes #9751
2020-06-25 18:14:54 -04:00
Cody Henthorne
b262efc24c Clear up warnings in string resource file. 2020-06-25 18:14:54 -04:00
Alan Evans
ce7ad76447 Cycle Versioned Profiles feature flag. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
9e98b6616e Log job run time. 2020-06-25 08:29:48 -04:00
Alan Evans
f4c9eaa904 Remove some unused resources. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
f8a0988e5f Various JobManager performance improvements. 2020-06-25 08:29:48 -04:00
Greyson Parrelli
bf919207ed Various logging improvements.
* Improve lifecycle logging.
* Remove 'action bar' from base activity names.
* Remove some unnecessary glide logs.
2020-06-25 08:29:48 -04:00
Greyson Parrelli
dac6b5c992 Bump version to 4.64.7 2020-06-24 20:09:31 -04:00
Greyson Parrelli
7f8043777e Updated language translations. 2020-06-24 20:09:00 -04:00
Greyson Parrelli
854b3feb36 Reduce verbosity of job logs. 2020-06-24 20:00:42 -04:00
Greyson Parrelli
22447e6ddb Fix theming issue with snackbar. 2020-06-24 20:00:42 -04:00
Alan Evans
be2ec36e1f Fix clipping issues with archive icon.
Fixes #8344
2020-06-24 20:00:12 -04:00
Greyson Parrelli
98cf16479d Bump version to 4.64.6 2020-06-24 10:58:13 -04:00
Greyson Parrelli
584735cbd0 Updated language translations. 2020-06-24 10:57:45 -04:00
Alan Evans
3741493cb7 Remove frame rate reporter and unused FPS ringbuffer. 2020-06-24 11:44:35 -03:00
Greyson Parrelli
4ea861fe5c Improve 'mark all read' performance. 2020-06-24 10:34:52 -04:00
Jim Gustafson
cd3df4d3c1 Update to ringrtc v2.2.0 2020-06-24 09:50:43 -04:00
Alan Evans
881a1edccb Bump version to 4.64.5 2020-06-22 10:53:52 -03:00
Alan Evans
1b7b574289 Updated language translations. 2020-06-22 10:50:27 -03:00
Alan Evans
d1d7498447 Fix text colors when system theme doesn't match. 2020-06-22 10:02:18 -03:00
Greyson Parrelli
50c18727e7 Bump version to 4.64.4 2020-06-21 12:23:31 -04:00
Greyson Parrelli
e9bfde470a Updated language translations. 2020-06-21 12:23:10 -04:00
Greyson Parrelli
68f718a210 Fix issue with conversation list times not updating.
Just started calling notifyDataSetChanged() in onResume() to provide
some sort of time update regularity.
2020-06-21 12:20:18 -04:00
408 changed files with 3368 additions and 2705 deletions

View File

@@ -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'

View File

@@ -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" />

View File

@@ -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

View File

@@ -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")

View File

@@ -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));
});

View File

@@ -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)));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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());

View File

@@ -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

View File

@@ -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")

View File

@@ -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();

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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

View File

@@ -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();
})

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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()) {

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)));
}
}
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.conversation.ui.error;
public enum TrustAndVerifyResult {
TRUST_AND_VERIFY,
TRUST_VERIFY_AND_RESEND,
UNKNOWN
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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 "

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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)))

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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";

View File

@@ -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;

View File

@@ -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";

View File

@@ -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);

View File

@@ -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";

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -76,7 +76,8 @@ public class JobMigrator {
jobSpec.getMaxInstances(),
dataSerializer.serialize(updatedJobData.getData()),
jobSpec.getSerializedInputData(),
jobSpec.isRunning());
jobSpec.isRunning(),
jobSpec.isMemoryOnly());
iter.set(updatedJobSpec);
}

View File

@@ -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);
}

View File

@@ -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."));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -32,6 +32,9 @@ public final class FullSpec {
return dependencySpecs;
}
public boolean isMemoryOnly() {
return jobSpec.isMemoryOnly();
}
@Override
public boolean equals(Object o) {

View File

@@ -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);
}
}

View File

@@ -11,6 +11,9 @@ public interface JobStorage {
@WorkerThread
void init();
@WorkerThread
void flush();
@WorkerThread
void insertJobs(@NonNull List<FullSpec> fullSpecs);

View File

@@ -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()));

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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)

View File

@@ -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());

View File

@@ -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";

View File

@@ -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";

View File

@@ -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();

View File

@@ -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