mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-18 08:49:08 +00:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a01bec3a11 | ||
|
|
3868175b85 | ||
|
|
904cb01067 | ||
|
|
5c0cb425a6 | ||
|
|
9dbb2ef630 | ||
|
|
bafd2817ee | ||
|
|
3380293923 | ||
|
|
a549c1ec8b | ||
|
|
ad84997ce0 | ||
|
|
42e2576813 | ||
|
|
31b995fa98 | ||
|
|
0364bec995 | ||
|
|
aa39f3d0a3 | ||
|
|
db545f43ea | ||
|
|
bbe003a454 | ||
|
|
819f0f68f6 | ||
|
|
8c0160937b | ||
|
|
6de789dfe3 | ||
|
|
afa2bb3bf5 | ||
|
|
89e66c0741 | ||
|
|
0dc4afba99 | ||
|
|
152578e576 | ||
|
|
63d6ab6fa7 | ||
|
|
75c8c59d78 | ||
|
|
87a59b6a9b | ||
|
|
2001fa86cf | ||
|
|
52747782a7 | ||
|
|
66f2668326 | ||
|
|
b262efc24c | ||
|
|
ce7ad76447 | ||
|
|
9e98b6616e | ||
|
|
f4c9eaa904 | ||
|
|
f8a0988e5f | ||
|
|
bf919207ed | ||
|
|
dac6b5c992 | ||
|
|
7f8043777e | ||
|
|
854b3feb36 | ||
|
|
22447e6ddb | ||
|
|
be2ec36e1f | ||
|
|
98cf16479d | ||
|
|
584735cbd0 | ||
|
|
3741493cb7 | ||
|
|
4ea861fe5c | ||
|
|
cd3df4d3c1 | ||
|
|
881a1edccb | ||
|
|
1b7b574289 | ||
|
|
d1d7498447 | ||
|
|
50c18727e7 | ||
|
|
e9bfde470a | ||
|
|
68f718a210 | ||
|
|
c3e528ad4b | ||
|
|
28af97c400 | ||
|
|
c2e4c343ab | ||
|
|
8a78589c2f | ||
|
|
841ee18435 | ||
|
|
71f54701d2 | ||
|
|
1c99939dfa | ||
|
|
50462cecd0 | ||
|
|
aa6a32f023 | ||
|
|
c4dc9064e3 | ||
|
|
bc5be10a0e | ||
|
|
98d9b57379 | ||
|
|
021a16050a | ||
|
|
555104aff0 | ||
|
|
95d63b78f4 | ||
|
|
80f9e1f4f1 | ||
|
|
a77997a4de | ||
|
|
ec4eb8e2a9 | ||
|
|
1bdeade71e | ||
|
|
629ba105cb | ||
|
|
891a1af995 | ||
|
|
0fbc6ac151 | ||
|
|
a6384d1b73 | ||
|
|
2fb9514890 | ||
|
|
fe89794505 | ||
|
|
08800c9faf | ||
|
|
469a4700d2 | ||
|
|
6707f974a5 | ||
|
|
c122cada2b | ||
|
|
96f02d8c95 | ||
|
|
dd717b60b8 | ||
|
|
3c20c7f4b4 | ||
|
|
1a09e70a04 | ||
|
|
027453bbd2 | ||
|
|
b621efa4a5 | ||
|
|
2915e4698c | ||
|
|
b687b1a4c5 | ||
|
|
b53827f32b | ||
|
|
d9641128a8 | ||
|
|
dfb5562142 | ||
|
|
d467c04749 | ||
|
|
3d7cffef2b | ||
|
|
f2fe81d9b5 | ||
|
|
cf98a22269 | ||
|
|
49f75d7036 | ||
|
|
ce940235b0 | ||
|
|
f5626f678d | ||
|
|
b3a59c3946 | ||
|
|
93c390c4fc | ||
|
|
941ab5a98f | ||
|
|
2ecdf803c0 | ||
|
|
5b2a399392 | ||
|
|
a9ea1d7606 | ||
|
|
1ce8ac2de6 | ||
|
|
e2019579fb | ||
|
|
fb3c6e56ee | ||
|
|
3fad007ae0 | ||
|
|
8891b6c930 | ||
|
|
400c592acf | ||
|
|
e13f3254ad |
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 655
|
||||
def canonicalVersionName = "4.63.2"
|
||||
def canonicalVersionCode = 667
|
||||
def canonicalVersionName = "4.65.2"
|
||||
|
||||
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.0.3'
|
||||
implementation 'org.signal:ringrtc-android:2.2.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
<issue id="StringFormatMatches" severity="error" />
|
||||
|
||||
<!-- L10N warnings -->
|
||||
<issue id="MissingTranslation" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="TypographyDashes" severity="error" >
|
||||
<ignore path="*/res/values-*" /> <!-- Ignore for non-English -->
|
||||
</issue>
|
||||
|
||||
<issue id="CanvasSize" severity="error" />
|
||||
<issue id="HardcodedText" severity="error" />
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
@@ -184,7 +184,7 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".stickers.StickerPackPreviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -243,24 +243,24 @@
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".MessageDetailsActivity"
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:label="@string/AndroidManifest__message_details"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".GroupCreateActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
@@ -274,7 +274,7 @@
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".NewConversationActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".giph.ui.GiphyActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -359,7 +359,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -410,10 +410,6 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".RecipientPreferenceActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -462,44 +458,44 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactNameEditActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.SharedContactDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ShortcutLauncherActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity
|
||||
android:name=".maps.PlacePickerActivity"
|
||||
android:label="@string/PlacePickerActivity_title"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addtogroup.AddToGroupsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
@@ -700,11 +696,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
|
||||
|
||||
<receiver android:name=".notifications.DeleteNotificationReceiver">
|
||||
<intent-filter>
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -107,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();
|
||||
@@ -133,6 +135,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this);
|
||||
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
@@ -141,6 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
*
|
||||
*/
|
||||
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
@@ -272,12 +272,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
|
||||
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
|
||||
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
|
||||
|
||||
requireActivity().startActivity(intent);
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,26 +94,42 @@ 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;
|
||||
}
|
||||
|
||||
toolbar.setTitle(recipient.toShortString(context));
|
||||
@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));
|
||||
});
|
||||
|
||||
avatar.setOnClickListener(v -> toggleUiVisibility());
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
|
||||
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (BaseActivity.isMenuWorkaroundRequired()) {
|
||||
forceOverflowMenu();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from: http://stackoverflow.com/a/13098824
|
||||
*/
|
||||
private void forceOverflowMenu() {
|
||||
try {
|
||||
ViewConfiguration config = ViewConfiguration.get(this);
|
||||
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
|
||||
if(menuKeyField != null) {
|
||||
menuKeyField.setAccessible(true);
|
||||
menuKeyField.setBoolean(config, false);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,90 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
public abstract class BaseActivity extends FragmentActivity {
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
* screen lock.
|
||||
*/
|
||||
public abstract class BaseActivity extends AppCompatActivity {
|
||||
private static final String TAG = Log.tag(BaseActivity.class);
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
public static boolean isMenuWorkaroundRequired() {
|
||||
return VERSION.SDK_INT < VERSION_CODES.KITKAT &&
|
||||
VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 &&
|
||||
("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,5 +47,6 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,7 @@ public interface BindableConversationListItem extends Unbindable {
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
|
||||
void setBatchMode(boolean batchMode);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
@@ -28,7 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
@@ -15,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
@@ -51,7 +49,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
|
||||
String name = recipient.toShortString(context);
|
||||
String name = recipient.getDisplayName(context);
|
||||
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
|
||||
@@ -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());
|
||||
@@ -175,7 +152,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
!legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
0, null);
|
||||
0,
|
||||
0,
|
||||
null);
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.lang.ref.WeakReference;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActivity
|
||||
implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.ScrollCallback
|
||||
|
||||
@@ -40,7 +40,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
@@ -93,7 +92,7 @@ import java.util.Set;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends Fragment
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
@@ -271,7 +270,7 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
if (listCallback != null && FeatureFlags.newGroupUI()) {
|
||||
if (listCallback != null) {
|
||||
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
|
||||
} else {
|
||||
@@ -518,7 +517,7 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
if (isMulti() && FeatureFlags.newGroupUI()) {
|
||||
if (isMulti()) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
|
||||
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
|
||||
private final ImportStateHandler importStateHandler = new ImportStateHandler();
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
@@ -42,7 +41,7 @@ import org.whispersystems.signalservice.internal.push.DeviceLimitExceededExcepti
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
public class DeviceActivity extends PassphraseRequiredActivity
|
||||
implements Button.OnClickListener, ScanListener, DeviceLinkFragment.LinkClickedListener
|
||||
{
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class DeviceAddFragment extends Fragment {
|
||||
public class DeviceAddFragment extends LoggingFragment {
|
||||
|
||||
private ViewGroup container;
|
||||
private LinearLayout overlay;
|
||||
|
||||
@@ -53,7 +53,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Bundle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.Window;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
|
||||
@@ -26,7 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||
startActivity(intent);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(R.string.DeviceProvisioningActivity_cancel, (dialog12, which) -> {
|
||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||
dialog12.dismiss();
|
||||
finish();
|
||||
})
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel;
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel.RecipientsPanelChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
|
||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Activity to create and update {@link GroupId.V1} groups
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
implements OnRecipientDeletedListener,
|
||||
RecipientsPanelChangedListener
|
||||
{
|
||||
|
||||
private final static String TAG = GroupCreateActivity.class.getSimpleName();
|
||||
|
||||
private static final String GROUP_ID_EXTRA = "group_id";
|
||||
private static final String GROUP_THREAD_EXTRA = "group_thread";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
|
||||
private static final int PICK_CONTACT = 1;
|
||||
|
||||
private EditText groupName;
|
||||
private ListView listView;
|
||||
private ImageView avatar;
|
||||
private TextView creatingText;
|
||||
private Bitmap avatarBmp;
|
||||
|
||||
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
|
||||
|
||||
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
||||
Intent intent = new Intent(context, GroupCreateActivity.class);
|
||||
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
setContentView(R.layout.group_create_activity);
|
||||
//noinspection ConstantConditions
|
||||
initializeAppBar();
|
||||
initializeResources();
|
||||
initializeExistingGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
private boolean isSignalGroup() {
|
||||
return TextSecurePreferences.isPushRegistered(this) && !getAdapter().hasNonPushMembers();
|
||||
}
|
||||
|
||||
private void disableSignalGroupViews(int reasonResId) {
|
||||
View pushDisabled = findViewById(R.id.push_disabled);
|
||||
pushDisabled.setVisibility(View.VISIBLE);
|
||||
((TextView) findViewById(R.id.push_disabled_reason)).setText(reasonResId);
|
||||
avatar.setEnabled(false);
|
||||
groupName.setEnabled(false);
|
||||
}
|
||||
|
||||
private void enableSignalGroupViews() {
|
||||
findViewById(R.id.push_disabled).setVisibility(View.GONE);
|
||||
avatar.setEnabled(true);
|
||||
groupName.setEnabled(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void updateViewState() {
|
||||
if (!TextSecurePreferences.isPushRegistered(this)) {
|
||||
disableSignalGroupViews(R.string.GroupCreateActivity_youre_not_registered_for_signal);
|
||||
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
|
||||
} else if (getAdapter().hasNonPushMembers()) {
|
||||
disableSignalGroupViews(R.string.GroupCreateActivity_contacts_dont_support_push);
|
||||
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
|
||||
} else {
|
||||
enableSignalGroupViews();
|
||||
getSupportActionBar().setTitle(groupToUpdate.isPresent()
|
||||
? R.string.GroupCreateActivity_actionbar_edit_title
|
||||
: R.string.GroupCreateActivity_actionbar_title);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isActiveInDirectory(Recipient recipient) {
|
||||
return recipient.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED;
|
||||
}
|
||||
|
||||
private void addSelectedContacts(@NonNull Recipient... recipients) {
|
||||
new AddMembersTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipients);
|
||||
}
|
||||
|
||||
private void addSelectedContacts(@NonNull Collection<Recipient> recipients) {
|
||||
addSelectedContacts(recipients.toArray(new Recipient[recipients.size()]));
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
Drawable upIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24);
|
||||
getSupportActionBar().setHomeAsUpIndicator(upIcon);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
|
||||
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
|
||||
|
||||
listView = findViewById(R.id.selected_contacts_list);
|
||||
avatar = findViewById(R.id.avatar);
|
||||
groupName = findViewById(R.id.group_name);
|
||||
creatingText = findViewById(R.id.creating_group_text);
|
||||
|
||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
|
||||
adapter.setOnRecipientDeletedListener(this);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||
recipientsPanel.setPanelChangeListener(this);
|
||||
|
||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
||||
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
|
||||
}
|
||||
|
||||
private Drawable getDefaultGroupAvatar() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
|
||||
}
|
||||
|
||||
private void initializeExistingGroup() {
|
||||
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
|
||||
|
||||
if (groupId != null) {
|
||||
GroupId.V1 groupIdV1 = groupId.requireV1();
|
||||
|
||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
|
||||
|
||||
if (FeatureFlags.newGroupUI()) {
|
||||
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.group_create, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.menu_create_group:
|
||||
if (groupToUpdate.isPresent()) handleGroupUpdate();
|
||||
else handleGroupCreate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientDeleted(Recipient recipient) {
|
||||
getAdapter().remove(recipient);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientsPanelUpdate(List<Recipient> recipients) {
|
||||
if (recipients != null && !recipients.isEmpty()) addSelectedContacts(recipients);
|
||||
}
|
||||
|
||||
private void handleGroupCreate() {
|
||||
if (getAdapter().getCount() < 1) {
|
||||
Log.i(TAG, getString(R.string.GroupCreateActivity_contacts_no_members));
|
||||
Toast.makeText(getApplicationContext(), R.string.GroupCreateActivity_contacts_no_members, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (isSignalGroup()) {
|
||||
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else {
|
||||
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupUpdate() {
|
||||
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
|
||||
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleOpenConversation(long threadId, Recipient recipient) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private SelectedRecipientsAdapter getAdapter() {
|
||||
return (SelectedRecipientsAdapter) listView.getAdapter();
|
||||
}
|
||||
|
||||
private @Nullable String getGroupName() {
|
||||
return groupName.getText() != null ? groupName.getText().toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
|
||||
if (data == null || resultCode != Activity.RESULT_OK)
|
||||
return;
|
||||
|
||||
switch (reqCode) {
|
||||
case PICK_CONTACT:
|
||||
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
|
||||
|
||||
for (RecipientId contact : selected) {
|
||||
Recipient recipient = Recipient.resolved(contact);
|
||||
addSelectedContacts(recipient);
|
||||
}
|
||||
|
||||
break;
|
||||
case REQUEST_CODE_SELECT_AVATAR:
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
avatarBmp = null;
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
return;
|
||||
}
|
||||
|
||||
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
|
||||
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(decryptableUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
setAvatar(decryptableUri, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class AddRecipientButtonListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class);
|
||||
if (groupToUpdate.isPresent()) {
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH);
|
||||
} else {
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH | DisplayMode.FLAG_SMS);
|
||||
}
|
||||
startActivityForResult(intent, PICK_CONTACT);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateMmsGroupTask extends AsyncTask<Void,Void,GroupActionResult> {
|
||||
private final GroupCreateActivity activity;
|
||||
private final Set<Recipient> members;
|
||||
|
||||
public CreateMmsGroupTask(GroupCreateActivity activity, Set<Recipient> members) {
|
||||
this.activity = activity;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupActionResult doInBackground(Void... avoid) {
|
||||
List<RecipientId> memberAddresses = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : members) {
|
||||
memberAddresses.add(recipient.getId());
|
||||
}
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
|
||||
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(GroupActionResult result) {
|
||||
activity.handleOpenConversation(result.getThreadId(), result.getGroupRecipient());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Void... values) {
|
||||
super.onProgressUpdate(values);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class SignalGroupTask extends AsyncTask<Void,Void,Optional<GroupActionResult>> {
|
||||
|
||||
protected GroupCreateActivity activity;
|
||||
protected Bitmap avatar;
|
||||
protected Set<Recipient> members;
|
||||
protected String name;
|
||||
|
||||
public SignalGroupTask(GroupCreateActivity activity,
|
||||
Bitmap avatar,
|
||||
String name,
|
||||
Set<Recipient> members)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.avatar = avatar;
|
||||
this.name = name;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
activity.findViewById(R.id.group_details_layout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE);
|
||||
activity.findViewById(R.id.menu_create_group).setVisibility(View.GONE);
|
||||
final int titleResId = activity.groupToUpdate.isPresent()
|
||||
? R.string.GroupCreateActivity_updating_group
|
||||
: R.string.GroupCreateActivity_creating_group;
|
||||
activity.creatingText.setText(activity.getString(titleResId, activity.getGroupName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> groupActionResultOptional) {
|
||||
if (activity.isFinishing()) return;
|
||||
activity.findViewById(R.id.group_details_layout).setVisibility(View.VISIBLE);
|
||||
activity.findViewById(R.id.creating_group_layout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.menu_create_group).setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateSignalGroupTask extends SignalGroupTask {
|
||||
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) {
|
||||
super(activity, avatar, name, members);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> result) {
|
||||
if (result.isPresent() && result.get().getThreadId() > -1) {
|
||||
if (!activity.isFinishing()) {
|
||||
activity.handleOpenConversation(result.get().getThreadId(), result.get().getGroupRecipient());
|
||||
}
|
||||
} else {
|
||||
super.onPostExecute(result);
|
||||
Toast.makeText(activity.getApplicationContext(),
|
||||
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
|
||||
private final GroupId.V1 groupId;
|
||||
|
||||
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
|
||||
Bitmap avatar, String name, Set<Recipient> members)
|
||||
{
|
||||
super(activity, avatar, name, members);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> result) {
|
||||
if (result.isPresent() && result.get().getThreadId() > -1) {
|
||||
if (!activity.isFinishing()) {
|
||||
Intent intent = activity.getIntent();
|
||||
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
|
||||
activity.setResult(RESULT_OK, intent);
|
||||
activity.finish();
|
||||
}
|
||||
} else {
|
||||
super.onPostExecute(result);
|
||||
Toast.makeText(activity.getApplicationContext(),
|
||||
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddMembersTask extends AsyncTask<Recipient,Void,List<AddMembersTask.Result>> {
|
||||
static class Result {
|
||||
Optional<Recipient> recipient;
|
||||
boolean isPush;
|
||||
String reason;
|
||||
|
||||
public Result(@Nullable Recipient recipient, boolean isPush, @Nullable String reason) {
|
||||
this.recipient = Optional.fromNullable(recipient);
|
||||
this.isPush = isPush;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
private GroupCreateActivity activity;
|
||||
private boolean failIfNotPush;
|
||||
|
||||
public AddMembersTask(@NonNull GroupCreateActivity activity) {
|
||||
this.activity = activity;
|
||||
this.failIfNotPush = activity.groupToUpdate.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Result> doInBackground(Recipient... recipients) {
|
||||
final List<Result> results = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
boolean isPush = isActiveInDirectory(recipient);
|
||||
|
||||
if (failIfNotPush && !isPush) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
|
||||
recipient.toShortString(activity))));
|
||||
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getE164().or(""))) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_youre_already_in_the_group)));
|
||||
} else {
|
||||
results.add(new Result(recipient, isPush, null));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Result> results) {
|
||||
if (activity.isFinishing()) return;
|
||||
|
||||
for (Result result : results) {
|
||||
if (result.recipient.isPresent()) {
|
||||
activity.getAdapter().add(result.recipient.get(), result.isPush);
|
||||
} else {
|
||||
Toast.makeText(activity, result.reason, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
activity.updateViewState();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
|
||||
private GroupCreateActivity activity;
|
||||
|
||||
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
|
||||
super(activity,
|
||||
R.string.GroupCreateActivity_loading_group_details,
|
||||
R.string.please_wait);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
|
||||
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
||||
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
|
||||
existingContacts.addAll(recipients);
|
||||
|
||||
if (group.isPresent()) {
|
||||
Bitmap avatar = null;
|
||||
try {
|
||||
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar.");
|
||||
}
|
||||
return Optional.of(new GroupData(groupIds[0],
|
||||
existingContacts,
|
||||
avatar,
|
||||
BitmapUtil.toByteArray(avatar),
|
||||
group.get().getTitle()));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupData> group) {
|
||||
super.onPostExecute(group);
|
||||
|
||||
if (group.isPresent() && !activity.isFinishing()) {
|
||||
activity.groupToUpdate = group;
|
||||
|
||||
activity.groupName.setText(group.get().name);
|
||||
if (group.get().avatarBmp != null) {
|
||||
activity.setAvatar(group.get().avatarBytes, group.get().avatarBmp);
|
||||
}
|
||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
|
||||
adapter.setOnRecipientDeletedListener(activity);
|
||||
activity.listView.setAdapter(adapter);
|
||||
activity.updateViewState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void setAvatar(T model, Bitmap bitmap) {
|
||||
avatarBmp = bitmap;
|
||||
GlideApp.with(this)
|
||||
.load(model)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
private static class GroupData {
|
||||
GroupId.V1 id;
|
||||
Set<Recipient> recipients;
|
||||
Bitmap avatarBmp;
|
||||
byte[] avatarBytes;
|
||||
String name;
|
||||
|
||||
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||
this.id = id;
|
||||
this.recipients = recipients;
|
||||
this.avatarBmp = avatarBmp;
|
||||
this.avatarBytes = avatarBytes;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* Simply logs out lifecycle events.
|
||||
*/
|
||||
public abstract class LoggingFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(LoggingFragment.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
@@ -3,9 +3,8 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
public class MainFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
||||
@@ -77,7 +77,7 @@ import java.util.Map;
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events
|
||||
@@ -181,7 +181,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
|
||||
String from;
|
||||
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
|
||||
else if (mediaItem.recipient != null) from = mediaItem.recipient.toShortString(this);
|
||||
else if (mediaItem.recipient != null) from = mediaItem.recipient.getDisplayName(this);
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
@@ -193,7 +193,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
if (threadRecipient.isLocalNumber()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.toShortString(this);
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
|
||||
@@ -1,451 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
|
||||
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
|
||||
|
||||
public static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String IS_PUSH_GROUP_EXTRA = "is_push_group";
|
||||
public static final String TYPE_EXTRA = "type";
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private long threadId;
|
||||
private boolean isPushGroup;
|
||||
private ConversationItem conversationItem;
|
||||
private ViewGroup itemParent;
|
||||
private View metadataContainer;
|
||||
private View expiresContainer;
|
||||
private TextView errorText;
|
||||
private View resendButton;
|
||||
private TextView sentDate;
|
||||
private TextView receivedDate;
|
||||
private TextView expiresInText;
|
||||
private View receivedContainer;
|
||||
private TextView transport;
|
||||
private TextView toFrom;
|
||||
private ListView recipientsList;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean running;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.message_details_activity);
|
||||
running = true;
|
||||
|
||||
initializeResources();
|
||||
initializeActionBar();
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
running = false;
|
||||
}
|
||||
|
||||
private void initializeActionBar() {
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient.observe(this, r -> setActionBarColor(r.getColor()));
|
||||
|
||||
setActionBarColor(recipient.get().getColor());
|
||||
}
|
||||
|
||||
private void setActionBarColor(MaterialColor color) {
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
inflater = LayoutInflater.from(this);
|
||||
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
|
||||
|
||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
||||
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
|
||||
glideRequests = GlideApp.with(this);
|
||||
itemParent = header.findViewById(R.id.item_container);
|
||||
recipientsList = findViewById(R.id.recipients_list);
|
||||
metadataContainer = header.findViewById(R.id.metadata_container);
|
||||
errorText = header.findViewById(R.id.error_text);
|
||||
resendButton = header.findViewById(R.id.resend_button);
|
||||
sentDate = header.findViewById(R.id.sent_time);
|
||||
receivedContainer = header.findViewById(R.id.received_container);
|
||||
receivedDate = header.findViewById(R.id.received_time);
|
||||
transport = header.findViewById(R.id.transport);
|
||||
toFrom = header.findViewById(R.id.tofrom);
|
||||
expiresContainer = header.findViewById(R.id.expires_container);
|
||||
expiresInText = header.findViewById(R.id.expires_in);
|
||||
recipientsList.setHeaderDividersEnabled(false);
|
||||
recipientsList.addHeaderView(header, null, false);
|
||||
}
|
||||
|
||||
private void updateTransport(MessageRecord messageRecord) {
|
||||
final String transportText;
|
||||
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
|
||||
transportText = "-";
|
||||
} else if (messageRecord.isPending()) {
|
||||
transportText = getString(R.string.ConversationFragment_pending);
|
||||
} else if (messageRecord.isPush()) {
|
||||
transportText = getString(R.string.ConversationFragment_push);
|
||||
} else if (messageRecord.isMms()) {
|
||||
transportText = getString(R.string.ConversationFragment_mms);
|
||||
} else {
|
||||
transportText = getString(R.string.ConversationFragment_sms);
|
||||
}
|
||||
|
||||
transport.setText(transportText);
|
||||
}
|
||||
|
||||
private void updateTime(MessageRecord messageRecord) {
|
||||
sentDate.setOnLongClickListener(null);
|
||||
receivedDate.setOnLongClickListener(null);
|
||||
|
||||
if (messageRecord.isPending() || messageRecord.isFailed()) {
|
||||
sentDate.setText("-");
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
Locale dateLocale = dynamicLanguage.getCurrentLocale();
|
||||
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
|
||||
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
|
||||
sentDate.setOnLongClickListener(v -> {
|
||||
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
|
||||
return true;
|
||||
});
|
||||
|
||||
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
|
||||
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
|
||||
receivedDate.setOnLongClickListener(v -> {
|
||||
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
|
||||
return true;
|
||||
});
|
||||
receivedContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateExpirationTime(final MessageRecord messageRecord) {
|
||||
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
|
||||
expiresContainer.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
expiresContainer.setVisibility(View.VISIBLE);
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
|
||||
long remaining = messageRecord.getExpiresIn() - elapsed;
|
||||
|
||||
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
|
||||
expiresInText.setText(duration);
|
||||
|
||||
if (running) {
|
||||
Util.runOnMainDelayed(this, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
|
||||
final int toFromRes;
|
||||
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__with;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__to;
|
||||
} else {
|
||||
toFromRes = R.string.message_details_header__from;
|
||||
}
|
||||
toFrom.setText(toFromRes);
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
||||
Parcelable state = recipientsList.onSaveInstanceState();
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
||||
recipientsList.onRestoreInstanceState(state);
|
||||
}
|
||||
|
||||
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
||||
if (conversationItem == null) {
|
||||
if (messageRecord.isGroupAction()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
|
||||
} else {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
|
||||
}
|
||||
itemParent.addView(conversationItem);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
|
||||
switch (type) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
|
||||
return reader.getNext();
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
|
||||
return mmsReader.getNext();
|
||||
default:
|
||||
throw new AssertionError("no valid message type specified");
|
||||
}
|
||||
}
|
||||
|
||||
private void copyToClipboard(@NonNull String text) {
|
||||
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
|
||||
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
|
||||
|
||||
if (messageRecord == null) {
|
||||
finish();
|
||||
} else {
|
||||
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
recipientsList.setAdapter(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
|
||||
|
||||
private final WeakReference<Context> weakContext;
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
this.weakContext = new WeakReference<>(context);
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return weakContext.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
|
||||
Context context = getContext();
|
||||
|
||||
if (context == null) {
|
||||
Log.w(TAG, "associated context is destroyed, finishing early");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
|
||||
|
||||
if (!messageRecord.getRecipient().isGroup()) {
|
||||
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1));
|
||||
} else {
|
||||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||
|
||||
if (receiptInfoList.isEmpty()) {
|
||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient recipient : group) {
|
||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
||||
}
|
||||
} else {
|
||||
for (GroupReceiptInfo info : receiptInfoList) {
|
||||
recipients.add(new RecipientDeliveryStatus(Recipient.resolved(info.getRecipientId()),
|
||||
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
|
||||
info.isUnidentified(),
|
||||
info.getTimestamp()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
|
||||
if (getContext() == null) {
|
||||
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
|
||||
return;
|
||||
}
|
||||
|
||||
inflateMessageViewIfAbsent(messageRecord);
|
||||
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
|
||||
|
||||
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
|
||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
|
||||
|
||||
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
|
||||
errorText.setVisibility(View.VISIBLE);
|
||||
resendButton.setVisibility(View.VISIBLE);
|
||||
resendButton.setOnClickListener(this::onResendClicked);
|
||||
metadataContainer.setVisibility(View.GONE);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
errorText.setVisibility(View.VISIBLE);
|
||||
resendButton.setVisibility(View.GONE);
|
||||
resendButton.setOnClickListener(null);
|
||||
metadataContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
updateTransport(messageRecord);
|
||||
updateTime(messageRecord);
|
||||
updateExpirationTime(messageRecord);
|
||||
errorText.setVisibility(View.GONE);
|
||||
resendButton.setVisibility(View.GONE);
|
||||
resendButton.setOnClickListener(null);
|
||||
metadataContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
|
||||
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
|
||||
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
|
||||
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
|
||||
else return RecipientDeliveryStatus.Status.PENDING;
|
||||
}
|
||||
|
||||
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
|
||||
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private void onResendClicked(View v) {
|
||||
resendButton.setVisibility(View.GONE);
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final MessageRecord record;
|
||||
private final List<RecipientDeliveryStatus> members;
|
||||
private final boolean isPushGroup;
|
||||
private final StableIdGenerator<RecipientId> idGenerator;
|
||||
|
||||
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
|
||||
boolean isPushGroup)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.record = record;
|
||||
this.isPushGroup = isPushGroup;
|
||||
this.members = members;
|
||||
this.idGenerator = new StableIdGenerator<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return members.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return members.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return idGenerator.getId(members.get(position).recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false);
|
||||
}
|
||||
|
||||
RecipientDeliveryStatus member = members.get(position);
|
||||
|
||||
((MessageRecipientListItem)convertView).set(glideRequests, record, member, isPushGroup);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
((MessageRecipientListItem)view).unbind();
|
||||
}
|
||||
|
||||
|
||||
static class RecipientDeliveryStatus {
|
||||
|
||||
enum Status {
|
||||
UNKNOWN, PENDING, SENT, DELIVERED, READ
|
||||
}
|
||||
|
||||
private final Recipient recipient;
|
||||
private final Status deliveryStatus;
|
||||
private final boolean isUnidentified;
|
||||
private final long timestamp;
|
||||
|
||||
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
|
||||
this.recipient = recipient;
|
||||
this.deliveryStatus = deliveryStatus;
|
||||
this.isUnidentified = isUnidentified;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
Status getDeliveryStatus() {
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
boolean isUnidentified() {
|
||||
return isUnidentified;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* A simple view to show the recipients of a message
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageRecipientListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
|
||||
|
||||
private RecipientDeliveryStatus member;
|
||||
private GlideRequests glideRequests;
|
||||
private FromTextView fromView;
|
||||
private TextView errorDescription;
|
||||
private TextView actionDescription;
|
||||
private Button conflictButton;
|
||||
private AvatarImageView contactPhotoImage;
|
||||
private ImageView unidentifiedDeliveryIcon;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
|
||||
public MessageRecipientListItem(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MessageRecipientListItem(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
this.fromView = findViewById(R.id.from);
|
||||
this.errorDescription = findViewById(R.id.error_description);
|
||||
this.actionDescription = findViewById(R.id.action_description);
|
||||
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
|
||||
this.conflictButton = findViewById(R.id.conflict_button);
|
||||
this.unidentifiedDeliveryIcon = findViewById(R.id.ud_indicator);
|
||||
this.deliveryStatusView = findViewById(R.id.delivery_status);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
observeMember();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
unsubscribeFromMember();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public void set(final GlideRequests glideRequests,
|
||||
final MessageRecord record,
|
||||
final RecipientDeliveryStatus member,
|
||||
final boolean isPushGroup)
|
||||
{
|
||||
unsubscribeFromMember();
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.member = member;
|
||||
observeMember();
|
||||
|
||||
fromView.setText(member.getRecipient());
|
||||
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
|
||||
setIssueIndicators(record, isPushGroup);
|
||||
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
private void observeMember() {
|
||||
if (isAttachedToWindow() && member != null && member.getRecipient() != null) {
|
||||
member.getRecipient().live().observeForever(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void unsubscribeFromMember() {
|
||||
if (member != null && member.getRecipient() != null) member.getRecipient().live().removeForeverObserver(this);
|
||||
}
|
||||
|
||||
private void setIssueIndicators(final MessageRecord record,
|
||||
final boolean isPushGroup)
|
||||
{
|
||||
final NetworkFailure networkFailure = getNetworkFailure(record);
|
||||
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
|
||||
|
||||
String errorText = "";
|
||||
|
||||
if (keyMismatch != null) {
|
||||
conflictButton.setVisibility(View.VISIBLE);
|
||||
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
|
||||
conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), record, keyMismatch).show());
|
||||
} else if ((networkFailure != null && !record.isPending()) || (!isPushGroup && record.isFailed())) {
|
||||
conflictButton.setVisibility(View.GONE);
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
|
||||
} else {
|
||||
if (record.isOutgoing()) {
|
||||
if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) {
|
||||
deliveryStatusView.setVisibility(View.GONE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) {
|
||||
deliveryStatusView.setRead();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) {
|
||||
deliveryStatusView.setDelivered();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) {
|
||||
deliveryStatusView.setSent();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
deliveryStatusView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
conflictButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
errorDescription.setText(errorText);
|
||||
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private NetworkFailure getNetworkFailure(final MessageRecord record) {
|
||||
if (record.hasNetworkFailures()) {
|
||||
for (final NetworkFailure failure : record.getNetworkFailures()) {
|
||||
if (failure.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
|
||||
if (record.isIdentityMismatchFailure()) {
|
||||
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return mismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
unsubscribeFromMember();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
if (this.member != null && this.member.getRecipient().equals(recipient)) {
|
||||
Log.d(TAG, "onRecipientChanged -- valid");
|
||||
fromView.setText(recipient);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, false);
|
||||
} else {
|
||||
Log.d(TAG, "onRecipientChanged -- invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public abstract class PassphraseActivity extends BaseActionBarActivity {
|
||||
public abstract class PassphraseActivity extends BaseActivity {
|
||||
|
||||
private static final String TAG = PassphraseActivity.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
||||
public abstract class PassphraseRequiredActivity extends BaseActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActivity.class.getSimpleName();
|
||||
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
public static final String NEXT_INTENT_EXTRA = "next_intent";
|
||||
@@ -49,7 +49,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onCreate()");
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
@@ -69,7 +68,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onResume()");
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
@@ -77,27 +75,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onPause()");
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onDestroy()");
|
||||
super.onDestroy();
|
||||
removeClearKeyReceiver(this);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import android.widget.Button;
|
||||
|
||||
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
|
||||
|
||||
public class PromptMmsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class PromptMmsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
|
||||
@@ -1,770 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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.Target;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.color.MaterialColors;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.database.loaders.RecipientMediaLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ContactPreference;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient";
|
||||
|
||||
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
|
||||
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
|
||||
private static final String PREFERENCE_CALL_TONE = "pref_key_recipient_call_ringtone";
|
||||
private static final String PREFERENCE_MESSAGE_VIBRATE = "pref_key_recipient_vibrate";
|
||||
private static final String PREFERENCE_CALL_VIBRATE = "pref_key_recipient_call_vibrate";
|
||||
private static final String PREFERENCE_BLOCK = "pref_key_recipient_block";
|
||||
private static final String PREFERENCE_COLOR = "pref_key_recipient_color";
|
||||
private static final String PREFERENCE_IDENTITY = "pref_key_recipient_identity";
|
||||
private static final String PREFERENCE_ABOUT = "pref_key_number";
|
||||
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
|
||||
|
||||
private ImageView avatar;
|
||||
private GlideRequests glideRequests;
|
||||
private RecipientId recipientId;
|
||||
private TextView threadPhotoRailLabel;
|
||||
private ThreadPhotoRailView threadPhotoRailView;
|
||||
private CollapsingToolbarLayout toolbarLayout;
|
||||
|
||||
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
|
||||
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
|
||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle instanceState, boolean ready) {
|
||||
setContentView(R.layout.recipient_preference_activity);
|
||||
this.glideRequests = GlideApp.with(this);
|
||||
this.recipientId = getIntent().getParcelableExtra(RECIPIENT_ID);
|
||||
|
||||
LiveRecipient recipient = Recipient.live(recipientId);
|
||||
|
||||
initializeToolbar();
|
||||
setHeader(recipient.get());
|
||||
recipient.observe(this, this::setHeader);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preference_fragment);
|
||||
fragment.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbarLayout = findViewById(R.id.collapsing_toolbar);
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.threadPhotoRailView = findViewById(R.id.recent_photos);
|
||||
this.threadPhotoRailLabel = findViewById(R.id.rail_label);
|
||||
|
||||
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
|
||||
this.threadPhotoRailView.setListener(mediaRecord ->
|
||||
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
|
||||
mediaRecord,
|
||||
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
|
||||
|
||||
SimpleTask.run(
|
||||
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
|
||||
(threadId) -> {
|
||||
if (threadId == null) {
|
||||
Log.i(TAG, "No thread id for recipient.");
|
||||
} else {
|
||||
this.threadPhotoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(this, threadId)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setLogo(null);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recipient_preference_root), (v, insets) -> {
|
||||
ViewUtil.setTopMargin(toolbar, insets.getSystemWindowInsetTop());
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeader(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
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();
|
||||
|
||||
glideRequests.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) {
|
||||
avatar.setOnClickListener(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
avatar.setOnClickListener(v -> startActivity(AvatarPreviewActivity.intentFromRecipientId(RecipientPreferenceActivity.this, recipient.getId()),
|
||||
AvatarPreviewActivity.createTransitionBundle(RecipientPreferenceActivity.this, avatar)));
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(this.avatar);
|
||||
|
||||
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
||||
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
|
||||
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
|
||||
this.toolbarLayout.setTitle(recipient.toShortString(this));
|
||||
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new RecipientMediaLoader(this, recipientId, RecipientMediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null && data.getCount() > 0) {
|
||||
this.threadPhotoRailLabel.setVisibility(View.VISIBLE);
|
||||
this.threadPhotoRailView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.threadPhotoRailLabel.setVisibility(View.GONE);
|
||||
this.threadPhotoRailView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.threadPhotoRailView.setCursor(glideRequests, data);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(RECIPIENT_ID, recipientId);
|
||||
initFragment(R.id.preference_fragment, new RecipientPreferenceFragment(), null, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
this.threadPhotoRailView.setCursor(glideRequests, null);
|
||||
}
|
||||
|
||||
public static class RecipientPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
private LiveRecipient recipient;
|
||||
private boolean canHaveSafetyNumber;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
Log.i(TAG, "onCreate (fragment)");
|
||||
super.onCreate(icicle);
|
||||
|
||||
initializeRecipients();
|
||||
|
||||
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
|
||||
|
||||
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
((SwitchPreferenceCompat) customNotificationsPref).setChecked(recipient.get().getNotificationChannel() != null);
|
||||
customNotificationsPref.setOnPreferenceChangeListener(new CustomNotificationsChangedListener());
|
||||
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
this.findPreference(PREFERENCE_MESSAGE_VIBRATE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
|
||||
if (recipient.get().getNotificationChannel() != null) {
|
||||
final Context context = requireContext();
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(getContext());
|
||||
db.setMessageRingtone(recipient.getId(), NotificationChannels.getMessageRingtone(context, recipient.get()));
|
||||
db.setMessageVibrate(recipient.getId(), NotificationChannels.getMessageVibrate(context, recipient.get()) ? VibrateState.ENABLED : VibrateState.DISABLED);
|
||||
NotificationChannels.ensureCustomChannelConsistency(context);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
}
|
||||
} else {
|
||||
customNotificationsPref.setVisible(false);
|
||||
}
|
||||
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE)
|
||||
.setOnPreferenceChangeListener(new RingtoneChangeListener(false));
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE)
|
||||
.setOnPreferenceClickListener(new RingtoneClickedListener(false));
|
||||
this.findPreference(PREFERENCE_CALL_TONE)
|
||||
.setOnPreferenceChangeListener(new RingtoneChangeListener(true));
|
||||
this.findPreference(PREFERENCE_CALL_TONE)
|
||||
.setOnPreferenceClickListener(new RingtoneClickedListener(true));
|
||||
this.findPreference(PREFERENCE_MESSAGE_VIBRATE)
|
||||
.setOnPreferenceChangeListener(new VibrateChangeListener(false));
|
||||
this.findPreference(PREFERENCE_CALL_VIBRATE)
|
||||
.setOnPreferenceChangeListener(new VibrateChangeListener(true));
|
||||
this.findPreference(PREFERENCE_MUTED)
|
||||
.setOnPreferenceClickListener(new MuteClickedListener());
|
||||
this.findPreference(PREFERENCE_BLOCK)
|
||||
.setOnPreferenceClickListener(new BlockClickedListener());
|
||||
this.findPreference(PREFERENCE_COLOR)
|
||||
.setOnPreferenceChangeListener(new ColorChangeListener());
|
||||
((ContactPreference)this.findPreference(PREFERENCE_ABOUT))
|
||||
.setListener(new AboutNumberClickedListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
Log.i(TAG, "onCreatePreferences...");
|
||||
addPreferencesFromResource(R.xml.recipient_preferences);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setSummaries(recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
findPreference(PREFERENCE_MESSAGE_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_MESSAGE_TONE), uri);
|
||||
} else if (requestCode == 2 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
findPreference(PREFERENCE_CALL_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_CALL_TONE), uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setLayoutAnimation(null);
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
private void initializeRecipients() {
|
||||
this.recipient = Recipient.live(getArguments().getParcelable(RECIPIENT_ID));
|
||||
this.recipient.observe(this, this::setSummaries);
|
||||
}
|
||||
|
||||
private void setSummaries(Recipient recipient) {
|
||||
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
|
||||
Preference customPreference = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
Preference ringtoneMessagePreference = this.findPreference(PREFERENCE_MESSAGE_TONE);
|
||||
Preference ringtoneCallPreference = this.findPreference(PREFERENCE_CALL_TONE);
|
||||
ListPreference vibrateMessagePreference = (ListPreference) this.findPreference(PREFERENCE_MESSAGE_VIBRATE);
|
||||
ListPreference vibrateCallPreference = (ListPreference) this.findPreference(PREFERENCE_CALL_VIBRATE);
|
||||
ColorPickerPreference colorPreference = (ColorPickerPreference) this.findPreference(PREFERENCE_COLOR);
|
||||
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
|
||||
Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
|
||||
PreferenceCategory callCategory = (PreferenceCategory)this.findPreference("call_settings");
|
||||
PreferenceCategory aboutCategory = (PreferenceCategory)this.findPreference("about");
|
||||
PreferenceCategory aboutDivider = (PreferenceCategory)this.findPreference("about_divider");
|
||||
ContactPreference aboutPreference = (ContactPreference)this.findPreference(PREFERENCE_ABOUT);
|
||||
PreferenceCategory privacyCategory = (PreferenceCategory) this.findPreference("privacy_settings");
|
||||
PreferenceCategory divider = (PreferenceCategory) this.findPreference("divider");
|
||||
|
||||
mutePreference.setChecked(recipient.isMuted());
|
||||
|
||||
ringtoneMessagePreference.setSummary(ringtoneMessagePreference.isEnabled() ? getRingtoneSummary(getContext(), recipient.getMessageRingtone()) : "");
|
||||
ringtoneCallPreference.setSummary(getRingtoneSummary(getContext(), recipient.getCallRingtone()));
|
||||
|
||||
Pair<String, Integer> vibrateMessageSummary = getVibrateSummary(getContext(), recipient.getMessageVibrate());
|
||||
Pair<String, Integer> vibrateCallSummary = getVibrateSummary(getContext(), recipient.getCallVibrate());
|
||||
|
||||
vibrateMessagePreference.setSummary(vibrateMessagePreference.isEnabled() ? vibrateMessageSummary.first : "");
|
||||
vibrateMessagePreference.setValueIndex(vibrateMessageSummary.second);
|
||||
|
||||
vibrateCallPreference.setSummary(vibrateCallSummary.first);
|
||||
vibrateCallPreference.setValueIndex(vibrateCallSummary.second);
|
||||
|
||||
blockPreference.setVisible(RecipientUtil.isBlockable(recipient));
|
||||
if (recipient.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
|
||||
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
mutePreference.setVisible(false);
|
||||
customPreference.setVisible(false);
|
||||
ringtoneMessagePreference.setVisible(false);
|
||||
vibrateMessagePreference.setVisible(false);
|
||||
|
||||
if (identityPreference != null) identityPreference.setVisible(false);
|
||||
if (aboutCategory != null) aboutCategory.setVisible(false);
|
||||
if (aboutDivider != null) aboutDivider.setVisible(false);
|
||||
if (privacyCategory != null) privacyCategory.setVisible(false);
|
||||
if (divider != null) divider.setVisible(false);
|
||||
if (callCategory != null) callCategory.setVisible(false);
|
||||
}
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (colorPreference != null) colorPreference.setVisible(false);
|
||||
if (identityPreference != null) identityPreference.setVisible(false);
|
||||
if (callCategory != null) callCategory.setVisible(false);
|
||||
if (aboutCategory != null) aboutCategory.setVisible(false);
|
||||
if (aboutDivider != null) aboutDivider.setVisible(false);
|
||||
if (divider != null) divider.setVisible(false);
|
||||
} else {
|
||||
colorPreference.setColors(MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireActivity()));
|
||||
colorPreference.setColor(recipient.getColor().toActionBarColor(requireActivity()));
|
||||
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
|
||||
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
|
||||
} else {
|
||||
aboutPreference.setTitle(formatRecipient(recipient));
|
||||
aboutPreference.setSummary(recipient.getCustomLabel());
|
||||
}
|
||||
|
||||
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
|
||||
|
||||
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
@Override
|
||||
public void onSuccess(Optional<IdentityRecord> result) {
|
||||
if (result.isPresent()) {
|
||||
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
|
||||
if (identityPreference != null) identityPreference.setEnabled(true);
|
||||
} else if (canHaveSafetyNumber) {
|
||||
if (identityPreference != null) identityPreference.setSummary(R.string.RecipientPreferenceActivity_available_once_a_message_has_been_sent_or_received);
|
||||
if (identityPreference != null) identityPreference.setEnabled(false);
|
||||
} else {
|
||||
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (recipient.isMmsGroup() && privacyCategory != null) {
|
||||
privacyCategory.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String formatRecipient(@NonNull Recipient recipient) {
|
||||
if (recipient.getE164().isPresent()) return PhoneNumberUtils.formatNumber(recipient.requireE164());
|
||||
else if (recipient.getEmail().isPresent()) return recipient.requireEmail();
|
||||
else return "";
|
||||
}
|
||||
|
||||
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
|
||||
if (ringtone == null) {
|
||||
return context.getString(R.string.preferences__default);
|
||||
} else if (ringtone.toString().isEmpty()) {
|
||||
return context.getString(R.string.preferences__silent);
|
||||
} else {
|
||||
Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone);
|
||||
|
||||
if (tone != null) {
|
||||
return tone.getTitle(context);
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.preferences__default);
|
||||
}
|
||||
|
||||
private @NonNull Pair<String, Integer> getVibrateSummary(@NonNull Context context, @NonNull VibrateState vibrateState) {
|
||||
if (vibrateState == VibrateState.DEFAULT) {
|
||||
return new Pair<>(context.getString(R.string.preferences__default), 0);
|
||||
} else if (vibrateState == VibrateState.ENABLED) {
|
||||
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_enabled), 1);
|
||||
} else {
|
||||
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_disabled), 2);
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
private final boolean calls;
|
||||
|
||||
RingtoneChangeListener(boolean calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = preference.getContext();
|
||||
|
||||
Uri value = (Uri)newValue;
|
||||
|
||||
Uri defaultValue;
|
||||
|
||||
if (calls) defaultValue = TextSecurePreferences.getCallNotificationRingtone(context);
|
||||
else defaultValue = TextSecurePreferences.getNotificationRingtone(context);
|
||||
|
||||
if (defaultValue.equals(value)) value = null;
|
||||
else if (value == null) value = Uri.EMPTY;
|
||||
|
||||
|
||||
new AsyncTask<Uri, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Uri... params) {
|
||||
if (calls) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipient.getId(), params[0]);
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), params[0]);
|
||||
NotificationChannels.updateMessageRingtone(context, recipient.get(), params[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, value);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private final boolean calls;
|
||||
|
||||
RingtoneClickedListener(boolean calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Uri current;
|
||||
Uri defaultUri;
|
||||
|
||||
if (calls) {
|
||||
current = recipient.get().getCallRingtone();
|
||||
defaultUri = TextSecurePreferences.getCallNotificationRingtone(getContext());
|
||||
} else {
|
||||
current = recipient.get().getMessageRingtone();
|
||||
defaultUri = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
}
|
||||
|
||||
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
else if (current.toString().isEmpty()) current = null;
|
||||
|
||||
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, calls ? RingtoneManager.TYPE_RINGTONE : RingtoneManager.TYPE_NOTIFICATION);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
|
||||
|
||||
startActivityForResult(intent, calls ? 2 : 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class VibrateChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
private final boolean call;
|
||||
|
||||
VibrateChangeListener(boolean call) {
|
||||
this.call = call;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
int value = Integer.parseInt((String) newValue);
|
||||
final VibrateState vibrateState = VibrateState.fromId(value);
|
||||
final Context context = preference.getContext();
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (call) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipient.getId(), vibrateState);
|
||||
}
|
||||
else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.getId(), vibrateState);
|
||||
NotificationChannels.updateMessageVibrate(context, recipient.get(), vibrateState);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ColorChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return true;
|
||||
|
||||
final int value = (Integer) newValue;
|
||||
final MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, value);
|
||||
final MaterialColor currentColor = recipient.get().getColor();
|
||||
|
||||
if (selectedColor == null) return true;
|
||||
|
||||
if (preference.isEnabled() && !currentColor.equals(selectedColor)) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setColor(recipient.getId(), selectedColor);
|
||||
|
||||
if (recipient.get().resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(recipient.getId()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class MuteClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (recipient.get().isMuted()) handleUnmute(preference.getContext());
|
||||
else handleMute(preference.getContext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleMute(@NonNull Context context) {
|
||||
MuteDialog.show(context, until -> setMuted(context, recipient.get(), until));
|
||||
|
||||
setSummaries(recipient.get());
|
||||
}
|
||||
|
||||
private void handleUnmute(@NonNull Context context) {
|
||||
setMuted(context, recipient.get(), 0);
|
||||
}
|
||||
|
||||
private void setMuted(@NonNull final Context context, final Recipient recipient, final long until) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setMuted(recipient.getId(), until);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private final IdentityRecord identityKey;
|
||||
|
||||
private IdentityClickedListener(IdentityRecord identityKey) {
|
||||
Log.i(TAG, "Identity record: " + identityKey);
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Context context = preference.getContext();
|
||||
|
||||
if (recipient.get().isBlocked()) {
|
||||
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
|
||||
} else {
|
||||
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutNumberClickedListener implements ContactPreference.Listener {
|
||||
|
||||
@Override
|
||||
public void onMessageClicked() {
|
||||
CommunicationActions.startConversation(getContext(), recipient.get(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecureCallClicked() {
|
||||
CommunicationActions.startVoiceCall(getActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecureVideoClicked() {
|
||||
CommunicationActions.startVideoCall(getActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInSecureCallClicked() {
|
||||
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
if (recipient.get().hasE164()) {
|
||||
Util.copyToClipboard(requireContext(), recipient.get().requireE164());
|
||||
ServiceUtil.getVibrator(requireContext()).vibrate(250);
|
||||
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomNotificationsChangedListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = preference.getContext();
|
||||
final boolean enabled = (boolean) newValue;
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (enabled) {
|
||||
String channel = NotificationChannels.createChannelFor(context, recipient.get());
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), channel);
|
||||
} else {
|
||||
NotificationChannels.deleteChannelFor(context, recipient.get());
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -486,7 +486,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString(getContext()))));
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
|
||||
@@ -528,9 +528,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
callScreen.setLocalRenderer(event.getLocalRenderer());
|
||||
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
||||
|
||||
viewModel.updateFromWebRtcViewModel(event);
|
||||
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
|
||||
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
|
||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||
|
||||
if (enableVideo) {
|
||||
enableVideoIfAvailable = false;
|
||||
handleSetMuteVideo(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.transition.Transition;
|
||||
import android.transition.TransitionValues;
|
||||
import android.util.Property;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
|
||||
@TargetApi(21)
|
||||
abstract class CircleSquareImageViewTransition extends Transition {
|
||||
|
||||
private static final String CIRCLE_RATIO = "CIRCLE_RATIO";
|
||||
|
||||
private final boolean toCircle;
|
||||
|
||||
CircleSquareImageViewTransition(boolean toCircle) {
|
||||
this.toCircle = toCircle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureStartValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 0f : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureEndValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageView endImageView = (ImageView) endValues.view;
|
||||
float start = (float) startValues.values.get(CIRCLE_RATIO);
|
||||
float end = (float) endValues.values.get(CIRCLE_RATIO);
|
||||
|
||||
return ObjectAnimator.ofFloat(endImageView, new RadiusRatioProperty(), start, end);
|
||||
}
|
||||
|
||||
static final class RadiusRatioProperty extends Property<ImageView, Float> {
|
||||
|
||||
private float ratio;
|
||||
|
||||
RadiusRatioProperty() {
|
||||
super(Float.class, "circle_ratio");
|
||||
}
|
||||
|
||||
@Override
|
||||
final public void set(ImageView imageView, Float ratio) {
|
||||
this.ratio = ratio;
|
||||
Drawable imageViewDrawable = imageView.getDrawable();
|
||||
if (imageViewDrawable instanceof RoundedBitmapDrawable) {
|
||||
RoundedBitmapDrawable drawable = (RoundedBitmapDrawable) imageViewDrawable;
|
||||
if (ratio > 0.95) {
|
||||
drawable.setCircular(true);
|
||||
} else {
|
||||
drawable.setCornerRadius(Math.min(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()) * ratio * 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(ImageView object) {
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class CircleToSquareImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public CircleToSquareImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class SquareToCircleImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public SquareToCircleImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -89,7 +90,11 @@ public final class AudioWaveForm {
|
||||
AudioHash audioHash = attachment.getAudioHash();
|
||||
if (audioHash != null) {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
if (audioFileInfo.waveForm.length == 0) {
|
||||
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
} else if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
@@ -100,13 +105,19 @@ public final class AudioWaveForm {
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
@@ -40,7 +41,6 @@ import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -96,7 +96,9 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
for (String table : tables) {
|
||||
if (table.equals(MmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMessage, null, count);
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count);
|
||||
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count);
|
||||
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
|
||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
||||
@@ -283,11 +285,15 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
|
||||
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
|
||||
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
|
||||
String where = MmsDatabase.ID + " = ?";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.color;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -52,7 +54,7 @@ public class MaterialColors {
|
||||
return null;
|
||||
}
|
||||
|
||||
public int[] asConversationColorArray(@NonNull Context context) {
|
||||
public @ColorInt int[] asConversationColorArray(@NonNull Context context) {
|
||||
int[] results = new int[colors.size()];
|
||||
int index = 0;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
@@ -26,8 +25,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -113,6 +112,9 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
this.fallbackPhotoProvider = fallbackPhotoProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the actual profile picture.
|
||||
*/
|
||||
public void setRecipient(@NonNull Recipient recipient) {
|
||||
if (recipient.isLocalNumber()) {
|
||||
setAvatar(GlideApp.with(this), null, false);
|
||||
@@ -122,6 +124,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the note to self icon.
|
||||
*/
|
||||
public void setAvatar(@Nullable Recipient recipient) {
|
||||
setAvatar(GlideApp.with(this), recipient, false);
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
|
||||
if (recipient != null) {
|
||||
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
|
||||
@@ -165,7 +174,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
if (quickContactEnabled) {
|
||||
super.setOnClickListener(v -> {
|
||||
Context context = getContext();
|
||||
if (FeatureFlags.newGroupUI() && recipient.isPushGroup()) {
|
||||
if (recipient.isPushGroup()) {
|
||||
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
|
||||
ManageGroupActivity.createTransitionBundle(context, this));
|
||||
} else {
|
||||
@@ -173,7 +182,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
|
||||
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
|
||||
} else {
|
||||
context.startActivity(RecipientPreferenceActivity.getLaunchIntent(context, recipient.getId()));
|
||||
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
|
||||
ManageRecipientActivity.createTransitionBundle(context, this));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -42,7 +42,7 @@ public class FromTextView extends EmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
String fromString = recipient.toShortString(getContext());
|
||||
String fromString = recipient.getDisplayName(getContext());
|
||||
|
||||
int typeface;
|
||||
|
||||
@@ -61,19 +61,6 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
} else if (!FeatureFlags.profileDisplay() && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
|
||||
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
|
||||
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL){
|
||||
builder.append(profileName);
|
||||
builder.append(fromSpan);
|
||||
} else {
|
||||
builder.append(fromSpan);
|
||||
builder.append(profileName);
|
||||
}
|
||||
} else {
|
||||
builder.append(fromSpan);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
|
||||
authorView.setText(author.isLocalNumber() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.toShortString(getContext()));
|
||||
: author.getDisplayName(getContext()));
|
||||
|
||||
// We use the raw color resource because Android 4.x was struggling with tints here
|
||||
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private boolean showVideoForOutgoing = false;
|
||||
private long callConnectedTime = -1;
|
||||
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
@@ -97,7 +98,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel) {
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
|
||||
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
|
||||
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
|
||||
|
||||
@@ -106,6 +107,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
|
||||
|
||||
if (enableVideo) {
|
||||
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
showVideoForOutgoing = false;
|
||||
}
|
||||
|
||||
updateLocalRenderState(webRtcViewModel.getState());
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getLocalCameraState().isEnabled(),
|
||||
@@ -177,8 +185,8 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
|
||||
if (shouldDisplayLocalVideo) return state;
|
||||
else return WebRtcLocalRenderState.GONE;
|
||||
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
|
||||
else return WebRtcLocalRenderState.GONE;
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls getRealWebRtcControls(boolean neverDisplayControls, @NonNull WebRtcControls controls) {
|
||||
|
||||
@@ -274,7 +274,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
String stringId = recipient.isGroup() ? recipient.requireGroupId().toString() : recipient.getE164().or(recipient.getEmail()).or("");
|
||||
|
||||
recentConversations.addRow(new Object[] { recipient.getId().serialize(),
|
||||
recipient.toShortString(getContext()),
|
||||
recipient.getDisplayName(getContext()),
|
||||
stringId,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
||||
"",
|
||||
|
||||
@@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.InsetDrawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -18,7 +16,16 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
|
||||
public final class FallbackPhoto80dp implements FallbackContactPhoto {
|
||||
|
||||
@DrawableRes private final int drawable80dp;
|
||||
private final MaterialColor backgroundColor;
|
||||
|
||||
public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull MaterialColor backgroundColor) {
|
||||
this.drawable80dp = drawable80dp;
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
return buildDrawable(context);
|
||||
@@ -40,13 +47,13 @@ public final class GroupFallbackPhoto80 implements FallbackContactPhoto {
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable)));
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, R.drawable.ic_group_80);
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
|
||||
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(24);
|
||||
|
||||
DrawableCompat.setTint(background, MaterialColor.ULTRAMARINE.toAvatarColor(context));
|
||||
DrawableCompat.setTint(background, backgroundColor.toAvatarColor(context));
|
||||
|
||||
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactNameEditActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class ContactNameEditActivity extends PassphraseRequiredActivity {
|
||||
|
||||
public static final String KEY_NAME = "name";
|
||||
public static final String KEY_CONTACT_INDEX = "contact_index";
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
@@ -27,7 +27,7 @@ import java.util.List;
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
import static org.thoughtcrime.securesms.contactshare.ContactShareEditViewModel.*;
|
||||
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActionBarActivity implements ContactShareEditAdapter.EventListener {
|
||||
public class ContactShareEditActivity extends PassphraseRequiredActivity implements ContactShareEditAdapter.EventListener {
|
||||
|
||||
public static final String KEY_CONTACTS = "contacts";
|
||||
private static final String KEY_CONTACT_URIS = "contact_uris";
|
||||
|
||||
@@ -20,8 +20,7 @@ import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -44,7 +43,7 @@ import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.*;
|
||||
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final int CODE_ADD_EDIT_CONTACT = 2323;
|
||||
private static final String KEY_CONTACT = "contact";
|
||||
|
||||
@@ -82,14 +82,12 @@ import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.GroupCreateActivity;
|
||||
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.RecipientPreferenceActivity;
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
@@ -110,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;
|
||||
@@ -127,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;
|
||||
@@ -175,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;
|
||||
@@ -207,6 +205,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -225,7 +224,6 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
@@ -264,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,
|
||||
@@ -275,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";
|
||||
@@ -495,7 +496,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
titleView.setTitle(glideRequests, recipientSnapshot);
|
||||
setActionBarColor(recipientSnapshot.getColor());
|
||||
setBlockedUserState(recipientSnapshot, isSecureText, isDefaultSms);
|
||||
setGroupShareProfileReminder(recipientSnapshot);
|
||||
calculateCharactersRemaining();
|
||||
|
||||
if (recipientSnapshot.getGroupId().isPresent() && recipientSnapshot.getGroupId().get().isV2()) {
|
||||
@@ -757,10 +757,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
} else {
|
||||
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
|
||||
}
|
||||
} else if (isActiveV2Group || isActiveGroup && FeatureFlags.newGroupUI()) {
|
||||
inflater.inflate(R.menu.conversation_push_group_v2_options, menu);
|
||||
} else if (isActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_push_group_options, menu);
|
||||
inflater.inflate(R.menu.conversation_active_group_options, menu);
|
||||
} else if (isActiveV2Group || isActiveGroup) {
|
||||
inflater.inflate(R.menu.conversation_active_group_options, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,9 +802,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
}
|
||||
|
||||
if (FeatureFlags.newGroupUI() && isPushGroupConversation()) {
|
||||
hideMenuItem(menu, R.id.menu_group_recipients);
|
||||
}
|
||||
hideMenuItem(menu, R.id.menu_group_recipients);
|
||||
|
||||
if (isActiveV2Group) {
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
@@ -884,8 +881,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
case R.id.menu_group_recipients: handleDisplayGroupRecipients(); return true;
|
||||
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
||||
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
||||
case R.id.menu_edit_group: handleEditPushGroupV1(); return true;
|
||||
case R.id.menu_group_settings: handleManagePushGroup(); return true;
|
||||
case R.id.menu_group_settings: handleManageGroup(); return true;
|
||||
case R.id.menu_leave: handleLeavePushGroup(); return true;
|
||||
case R.id.menu_invite: handleInviteLink(); return true;
|
||||
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
|
||||
@@ -1042,14 +1038,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void handleConversationSettings() {
|
||||
if (FeatureFlags.newGroupUI() && isPushGroupConversation()) {
|
||||
handleManagePushGroup();
|
||||
if (isGroupConversation()) {
|
||||
handleManageGroup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInMessageRequest()) return;
|
||||
|
||||
Intent intent = RecipientPreferenceActivity.getLaunchIntent(this, recipient.getId());
|
||||
Intent intent = ManageRecipientActivity.newIntentFromConversation(this, recipient.getId());
|
||||
startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
|
||||
}
|
||||
|
||||
@@ -1206,12 +1202,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
null);
|
||||
}
|
||||
|
||||
private void handleEditPushGroupV1() {
|
||||
startActivityForResult(GroupCreateActivity.newEditGroupIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV1()), GROUP_EDIT);
|
||||
}
|
||||
|
||||
private void handleManagePushGroup() {
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requirePush()),
|
||||
private void handleManageGroup() {
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()),
|
||||
GROUP_EDIT,
|
||||
ManageGroupActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image)));
|
||||
}
|
||||
@@ -1317,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) {
|
||||
@@ -1965,7 +1935,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
titleView.setVerified(identityRecords.isVerified());
|
||||
setBlockedUserState(recipient, isSecureText, isDefaultSms);
|
||||
setActionBarColor(recipient.getColor());
|
||||
setGroupShareProfileReminder(recipient);
|
||||
updateReminders();
|
||||
updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId());
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
@@ -2147,12 +2116,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
if (recipient.isBlocked() && !FeatureFlags.messageRequests()) {
|
||||
unblockButton.setVisibility(View.VISIBLE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
} else if (!isSecureText && isPushGroupConversation()) {
|
||||
if (!isSecureText && isPushGroupConversation()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
@@ -2170,19 +2134,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipient.isPushGroup() && !recipient.isProfileSharing()) {
|
||||
groupShareProfileView.get().setRecipient(recipient);
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
} else if (groupShareProfileView.resolved()) {
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
@@ -2359,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 {
|
||||
@@ -2396,10 +2345,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
long id = fragment.stageOutgoingMessage(message);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
if (!FeatureFlags.messageRequests() && initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), threadId, () -> fragment.releaseOutgoingMessage(id));
|
||||
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
@@ -2470,10 +2415,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
if (!FeatureFlags.messageRequests() && initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
}, result -> {
|
||||
sendComplete(result);
|
||||
@@ -2517,10 +2458,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
new AsyncTask<OutgoingTextMessage, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(OutgoingTextMessage... messages) {
|
||||
if (!FeatureFlags.messageRequests() && initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
}
|
||||
|
||||
@@ -2928,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()) {
|
||||
@@ -3150,7 +3102,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
String[] unverifiedNames = new String[unverifiedIdentities.size()];
|
||||
|
||||
for (int i=0;i<unverifiedIdentities.size();i++) {
|
||||
unverifiedNames[i] = Recipient.resolved(unverifiedIdentities.get(i).getRecipientId()).toShortString(ConversationActivity.this);
|
||||
unverifiedNames[i] = Recipient.resolved(unverifiedIdentities.get(i).getRecipientId()).getDisplayName(ConversationActivity.this);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ConversationActivity.this);
|
||||
|
||||
@@ -479,7 +479,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
return headerView != null;
|
||||
}
|
||||
|
||||
private boolean hasFooter() {
|
||||
public boolean hasFooter() {
|
||||
return footerView != null;
|
||||
}
|
||||
|
||||
@@ -510,6 +510,10 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getLastVisibleMessageRecord(int position) {
|
||||
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
|
||||
}
|
||||
|
||||
static class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
|
||||
super(itemView);
|
||||
|
||||
@@ -7,26 +7,32 @@ final class ConversationData {
|
||||
private final long threadId;
|
||||
private final long lastSeen;
|
||||
private final int lastSeenPosition;
|
||||
private final int lastScrolledPosition;
|
||||
private final boolean hasSent;
|
||||
private final boolean isMessageRequestAccepted;
|
||||
private final boolean hasPreMessageRequestMessages;
|
||||
private final int jumpToPosition;
|
||||
private final int threadSize;
|
||||
|
||||
ConversationData(long threadId,
|
||||
long lastSeen,
|
||||
int lastSeenPosition,
|
||||
int lastScrolledPosition,
|
||||
boolean hasSent,
|
||||
boolean isMessageRequestAccepted,
|
||||
boolean hasPreMessageRequestMessages,
|
||||
int jumpToPosition)
|
||||
int jumpToPosition,
|
||||
int threadSize)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.lastSeen = lastSeen;
|
||||
this.lastSeenPosition = lastSeenPosition;
|
||||
this.hasSent = hasSent;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
|
||||
this.jumpToPosition = jumpToPosition;
|
||||
this.threadId = threadId;
|
||||
this.lastSeen = lastSeen;
|
||||
this.lastSeenPosition = lastSeenPosition;
|
||||
this.lastScrolledPosition = lastScrolledPosition;
|
||||
this.hasSent = hasSent;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
|
||||
this.jumpToPosition = jumpToPosition;
|
||||
this.threadSize = threadSize;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
@@ -41,6 +47,10 @@ final class ConversationData {
|
||||
return lastSeenPosition;
|
||||
}
|
||||
|
||||
int getLastScrolledPosition() {
|
||||
return lastScrolledPosition;
|
||||
}
|
||||
|
||||
boolean hasSent() {
|
||||
return hasSent;
|
||||
}
|
||||
@@ -57,7 +67,15 @@ final class ConversationData {
|
||||
return jumpToPosition >= 0;
|
||||
}
|
||||
|
||||
boolean shouldScrollToLastSeen() {
|
||||
return lastSeenPosition > 0;
|
||||
}
|
||||
|
||||
int getJumpToPosition() {
|
||||
return jumpToPosition;
|
||||
}
|
||||
|
||||
int getThreadSize() {
|
||||
return threadSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
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;
|
||||
@@ -30,16 +32,13 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
|
||||
private final Context context;
|
||||
private final long threadId;
|
||||
private final DataUpdatedCallback dataUpdateCallback;
|
||||
|
||||
private ConversationDataSource(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull Invalidator invalidator,
|
||||
@NonNull DataUpdatedCallback dataUpdateCallback)
|
||||
@NonNull Invalidator invalidator)
|
||||
{
|
||||
this.context = context;
|
||||
this.threadId = threadId;
|
||||
this.dataUpdateCallback = dataUpdateCallback;
|
||||
|
||||
ContentObserver contentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
@@ -66,10 +65,6 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
int totalCount = db.getConversationCount(threadId);
|
||||
int effectiveCount = params.requestedStartPosition;
|
||||
|
||||
if (totalCount == 0 || params.requestedStartPosition > totalCount) {
|
||||
|
||||
}
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||
@@ -79,13 +74,12 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
}
|
||||
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult result = ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.messages, params.requestedStartPosition, result.total);
|
||||
Util.runOnMain(dataUpdateCallback::onDataUpdated);
|
||||
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
|
||||
}
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,59 +98,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
|
||||
callback.onResult(records);
|
||||
|
||||
if (!isInvalid()) {
|
||||
Util.runOnMain(dataUpdateCallback::onDataUpdated);
|
||||
}
|
||||
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
private static @NonNull SizeFixResult ensureMultipleOfPageSize(@NonNull List<MessageRecord> records,
|
||||
int startPosition,
|
||||
int pageSize,
|
||||
int total)
|
||||
{
|
||||
if (records.size() + startPosition == total || (records.size() != 0 && records.size() % pageSize == 0)) {
|
||||
return new SizeFixResult(records, total);
|
||||
}
|
||||
|
||||
if (records.size() < pageSize) {
|
||||
Log.w(TAG, "Hit a miscalculation where we don't have the full dataset, but it's smaller than a page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
|
||||
return new SizeFixResult(records, records.size() + startPosition);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Hit a miscalculation where our data size isn't a multiple of the page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
|
||||
int overflow = records.size() % pageSize;
|
||||
|
||||
return new SizeFixResult(records.subList(0, records.size() - overflow), total);
|
||||
}
|
||||
|
||||
private static class SizeFixResult {
|
||||
final List<MessageRecord> messages;
|
||||
final int total;
|
||||
|
||||
private SizeFixResult(@NonNull List<MessageRecord> messages, int total) {
|
||||
this.messages = messages;
|
||||
this.total = total;
|
||||
}
|
||||
}
|
||||
|
||||
interface DataUpdatedCallback {
|
||||
void onDataUpdated();
|
||||
}
|
||||
|
||||
static class Invalidator {
|
||||
private Runnable callback;
|
||||
|
||||
synchronized void invalidate() {
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void observe(@NonNull Runnable callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
|
||||
@@ -164,18 +106,16 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
|
||||
private final Context context;
|
||||
private final long threadId;
|
||||
private final Invalidator invalidator;
|
||||
private final DataUpdatedCallback callback;
|
||||
|
||||
Factory(Context context, long threadId, @NonNull Invalidator invalidator, @NonNull DataUpdatedCallback callback) {
|
||||
Factory(Context context, long threadId, @NonNull Invalidator invalidator) {
|
||||
this.context = context;
|
||||
this.threadId = threadId;
|
||||
this.invalidator = invalidator;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, MessageRecord> create() {
|
||||
return new ConversationDataSource(context, threadId, invalidator, callback);
|
||||
return new ConversationDataSource(context, threadId, invalidator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
@@ -51,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;
|
||||
@@ -61,8 +59,8 @@ import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
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;
|
||||
@@ -75,7 +73,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickList
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -88,6 +85,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
@@ -113,6 +111,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -134,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;
|
||||
@@ -163,8 +162,7 @@ public class ConversationFragment extends Fragment {
|
||||
private ConversationBannerView emptyConversationBanner;
|
||||
private MessageRequestViewModel messageRequestViewModel;
|
||||
private ConversationViewModel conversationViewModel;
|
||||
|
||||
private Deferred deferred = new Deferred();
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
@@ -179,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
|
||||
@@ -198,12 +196,11 @@ public class ConversationFragment extends Fragment {
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setItemAnimator(null);
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
|
||||
}
|
||||
snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
|
||||
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
|
||||
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
|
||||
topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
|
||||
initializeLoadMoreView(topLoadMoreView);
|
||||
initializeLoadMoreView(bottomLoadMoreView);
|
||||
|
||||
@@ -226,16 +223,12 @@ public class ConversationFragment extends Fragment {
|
||||
Log.i(TAG, "submitList skipped an invalid list");
|
||||
}
|
||||
});
|
||||
conversationViewModel.getConversationMetadata().observe(this, data -> deferred.defer(() -> presentConversationMetadata(data)));
|
||||
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setupListLayoutListeners() {
|
||||
if (!FeatureFlags.messageRequests()) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
|
||||
|
||||
list.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@@ -284,6 +277,23 @@ public class ConversationFragment extends Fragment {
|
||||
initializeTypingObserver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
int lastVisiblePosition = getListLayoutManager().findLastVisibleItemPosition();
|
||||
int firstVisiblePosition = getListLayoutManager().findFirstCompletelyVisibleItemPosition();
|
||||
|
||||
final long lastVisibleMessageTimestamp;
|
||||
if (firstVisiblePosition != 0 && lastVisiblePosition != RecyclerView.NO_POSITION) {
|
||||
MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition);
|
||||
|
||||
lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0;
|
||||
} else {
|
||||
lastVisibleMessageTimestamp = 0;
|
||||
}
|
||||
SignalExecutors.BOUNDED.submit(() -> DatabaseFactory.getThreadDatabase(requireContext()).setLastScrolled(threadId, lastVisibleMessageTimestamp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
@@ -312,7 +322,7 @@ public class ConversationFragment extends Fragment {
|
||||
}
|
||||
|
||||
int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition());
|
||||
scrollToLastSeenPosition(position);
|
||||
snapToTopDataObserver.requestScrollPosition(position);
|
||||
}
|
||||
|
||||
private void initializeMessageRequestViewModel() {
|
||||
@@ -406,7 +416,6 @@ public class ConversationFragment extends Fragment {
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
|
||||
|
||||
deferred.setDeferred(true);
|
||||
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
|
||||
|
||||
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
|
||||
@@ -425,12 +434,12 @@ public class ConversationFragment extends Fragment {
|
||||
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
|
||||
ConversationAdapter.initializePool(list.getRecycledViewPool());
|
||||
|
||||
adapter.registerAdapterDataObserver(new DataObserver());
|
||||
adapter.registerAdapterDataObserver(snapToTopDataObserver);
|
||||
|
||||
setLastSeen(conversationViewModel.getLastSeen());
|
||||
|
||||
emptyConversationBanner.setVisibility(View.GONE);
|
||||
} else if (FeatureFlags.messageRequests() && threadId == -1) {
|
||||
} else if (threadId == -1) {
|
||||
emptyConversationBanner.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -546,7 +555,7 @@ public class ConversationFragment extends Fragment {
|
||||
this.threadId = threadId;
|
||||
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
|
||||
|
||||
deferred.setDeferred(true);
|
||||
snapToTopDataObserver.requestScrollPosition(0);
|
||||
conversationViewModel.onConversationDataAvailable(threadId, -1);
|
||||
initializeListAdapter();
|
||||
}
|
||||
@@ -698,13 +707,7 @@ public class ConversationFragment extends Fragment {
|
||||
|
||||
|
||||
private void handleDisplayDetails(MessageRecord message) {
|
||||
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
|
||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
intent.putExtra(MessageDetailsActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, recipient.get().isGroup() && message.isPush());
|
||||
startActivity(intent);
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId));
|
||||
}
|
||||
|
||||
private void handleForwardMessage(MessageRecord message) {
|
||||
@@ -866,47 +869,49 @@ public class ConversationFragment extends Fragment {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
adapter.setFooterView(conversationBanner);
|
||||
} else {
|
||||
adapter.setFooterView(null);
|
||||
}
|
||||
adapter.setFooterView(conversationBanner);
|
||||
|
||||
setLastSeen(conversation.getLastSeen());
|
||||
|
||||
if (FeatureFlags.messageRequests() && !conversation.hasPreMessageRequestMessages()) {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
} else {
|
||||
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
adapter.setHeaderView(unknownSenderView);
|
||||
} else {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
Runnable afterScroll = () -> {
|
||||
if (!conversation.isMessageRequestAccepted()) {
|
||||
snapToTopDataObserver.requestScrollPosition(adapter.getItemCount() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
listener.onCursorChanged();
|
||||
setLastSeen(conversation.getLastSeen());
|
||||
|
||||
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
|
||||
if (!conversation.hasPreMessageRequestMessages()) {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
} else {
|
||||
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
adapter.setHeaderView(unknownSenderView);
|
||||
} else {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation.shouldJumpToMessage()) {
|
||||
scrollToStartingPosition(conversation.getJumpToPosition());
|
||||
listener.onCursorChanged();
|
||||
};
|
||||
|
||||
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
|
||||
int lastScrolledPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastScrolledPosition());
|
||||
|
||||
if (conversation.getThreadSize() == 0) {
|
||||
afterScroll.run();
|
||||
} else if (conversation.shouldJumpToMessage()) {
|
||||
snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition())
|
||||
.withOnScrollRequestComplete(() -> {
|
||||
afterScroll.run();
|
||||
getListAdapter().pulseHighlightItem(conversation.getJumpToPosition());
|
||||
})
|
||||
.submit();
|
||||
} else if (conversation.isMessageRequestAccepted()) {
|
||||
scrollToLastSeenPosition(lastSeenPosition);
|
||||
} else if (FeatureFlags.messageRequests()) {
|
||||
list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollToStartingPosition(int startingPosition) {
|
||||
list.post(() -> {
|
||||
list.getLayoutManager().scrollToPosition(startingPosition);
|
||||
getListAdapter().pulseHighlightItem(startingPosition);
|
||||
});
|
||||
}
|
||||
|
||||
private void scrollToLastSeenPosition(int lastSeenPosition) {
|
||||
if (lastSeenPosition > 0) {
|
||||
list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
|
||||
snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
|
||||
.withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, list.getHeight()))
|
||||
.withOnScrollRequestComplete(afterScroll)
|
||||
.submit();
|
||||
} else {
|
||||
snapToTopDataObserver.buildScrollPosition(adapter.getItemCount() - 1)
|
||||
.withOnScrollRequestComplete(afterScroll)
|
||||
.submit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,24 +946,21 @@ public class ConversationFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
|
||||
if (position >= 0) {
|
||||
list.scrollToPosition(position);
|
||||
|
||||
if (getListAdapter() == null || getListAdapter().getItem(position) == null) {
|
||||
Log.i(TAG, "[moveToMessagePosition] Position " + position + " not currently populated. Scheduling a jump.");
|
||||
conversationViewModel.scheduleForNextMessageUpdate(() -> {
|
||||
list.scrollToPosition(position);
|
||||
getListAdapter().pulseHighlightItem(position);
|
||||
});
|
||||
} else {
|
||||
getListAdapter().pulseHighlightItem(position);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
|
||||
if (onMessageNotFound != null) {
|
||||
onMessageNotFound.run();
|
||||
}
|
||||
}
|
||||
conversationViewModel.onConversationDataAvailable(threadId, position);
|
||||
snapToTopDataObserver.buildScrollPosition(position)
|
||||
.withOnPerformScroll(((layoutManager, p) ->
|
||||
list.post(() -> {
|
||||
layoutManager.scrollToPosition(p);
|
||||
getListAdapter().pulseHighlightItem(position);
|
||||
})
|
||||
))
|
||||
.withOnInvalidPosition(() -> {
|
||||
if (onMessageNotFound != null) {
|
||||
onMessageNotFound.run();
|
||||
}
|
||||
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
|
||||
})
|
||||
.submit();
|
||||
}
|
||||
|
||||
private void maybeShowSwipeToReplyTooltip() {
|
||||
@@ -987,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 {
|
||||
@@ -1058,44 +1061,6 @@ public class ConversationFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private class DataObserver extends RecyclerView.AdapterDataObserver {
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (deferred.isDeferred()) {
|
||||
deferred.setDeferred(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
int firstVisibleItem = getListLayoutManager().findFirstVisibleItemPosition();
|
||||
|
||||
if (firstVisibleItem == 0) {
|
||||
View view = getListLayoutManager().findViewByPosition(0);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.getDrawingRect(rect);
|
||||
list.offsetDescendantRectToMyCoords(view, rect);
|
||||
|
||||
int bottom = rect.bottom;
|
||||
list.getDrawingRect(rect);
|
||||
|
||||
if (bottom <= rect.bottom) {
|
||||
getListLayoutManager().scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConversationFragmentItemClickListener implements ItemClickListener {
|
||||
|
||||
@Override
|
||||
@@ -1285,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
|
||||
@@ -1303,6 +1273,52 @@ public class ConversationFragment extends Fragment {
|
||||
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
|
||||
}
|
||||
|
||||
private final class ConversationSnapToTopDataObserver extends SnapToTopDataObserver {
|
||||
|
||||
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
|
||||
@Nullable ScrollRequestValidator scrollRequestValidator)
|
||||
{
|
||||
super(recyclerView, scrollRequestValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onItemRangeInserted(positionStart, itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
private final class ConversationScrollRequestValidator implements SnapToTopDataObserver.ScrollRequestValidator {
|
||||
|
||||
@Override
|
||||
public boolean isPositionStillValid(int position) {
|
||||
if (getListAdapter() == null) {
|
||||
return position >= 0;
|
||||
} else {
|
||||
return position >= 0 && position < getListAdapter().getItemCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemAtPositionLoaded(int position) {
|
||||
if (getListAdapter() == null) {
|
||||
return false;
|
||||
} else if (getListAdapter().hasFooter() && position == getListAdapter().getItemCount() - 1) {
|
||||
return true;
|
||||
} else {
|
||||
return getListAdapter().getItem(position) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
@@ -1449,33 +1465,4 @@ public class ConversationFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Deferred {
|
||||
|
||||
private Runnable deferred;
|
||||
private boolean isDeferred;
|
||||
|
||||
public void defer(@Nullable Runnable deferred) {
|
||||
this.deferred = deferred;
|
||||
executeIfNecessary();
|
||||
}
|
||||
|
||||
public void setDeferred(boolean isDeferred) {
|
||||
this.isDeferred = isDeferred;
|
||||
executeIfNecessary();
|
||||
}
|
||||
|
||||
public boolean isDeferred() {
|
||||
return isDeferred;
|
||||
}
|
||||
|
||||
private void executeIfNecessary() {
|
||||
if (deferred != null && !isDeferred) {
|
||||
Runnable local = deferred;
|
||||
|
||||
deferred = null;
|
||||
|
||||
local.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ import com.annimon.stream.Stream;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.ConfirmIdentityDialog;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.AlertView;
|
||||
@@ -77,7 +76,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
@@ -109,7 +107,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
@@ -1002,21 +999,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
|
||||
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
|
||||
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
groupSender.setText(recipient.getDisplayName(getContext()));
|
||||
groupSenderProfileName.setVisibility(View.GONE);
|
||||
} else {
|
||||
groupSender.setText(recipient.toShortString(context));
|
||||
|
||||
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
|
||||
groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
|
||||
groupSenderProfileName.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupSenderProfileName.setText(null);
|
||||
groupSenderProfileName.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
groupSender.setText(recipient.getDisplayName(getContext()));
|
||||
groupSenderProfileName.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1390,13 +1374,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
parent.onClick(v);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
Intent intent = new Intent(context, MessageDetailsActivity.class);
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
intent.putExtra(MessageDetailsActivity.IS_PUSH_GROUP_EXTRA, groupThread && messageRecord.isPush());
|
||||
intent.putExtra(MessageDetailsActivity.RECIPIENT_EXTRA, conversationRecipient.getId());
|
||||
context.startActivity(intent);
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
|
||||
handleApproveIdentity();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
|
||||
@@ -7,10 +7,10 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -35,23 +35,30 @@ class ConversationRepository {
|
||||
}
|
||||
|
||||
private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
|
||||
Pair<Long, Boolean> lastSeenAndHasSent = DatabaseFactory.getThreadDatabase(context).getLastSeenAndHasSent(threadId);
|
||||
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
|
||||
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
|
||||
|
||||
long lastSeen = lastSeenAndHasSent.first();
|
||||
boolean hasSent = lastSeenAndHasSent.second();
|
||||
int lastSeenPosition = 0;
|
||||
long lastSeen = metadata.getLastSeen();
|
||||
boolean hasSent = metadata.hasSent();
|
||||
int lastSeenPosition = 0;
|
||||
long lastScrolled = metadata.getLastScrolled();
|
||||
int lastScrolledPosition = 0;
|
||||
|
||||
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
||||
boolean hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
|
||||
|
||||
if (lastSeen > 0) {
|
||||
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionForLastSeen(threadId, lastSeen);
|
||||
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
|
||||
}
|
||||
|
||||
if (lastSeenPosition <= 0) {
|
||||
lastSeen = 0;
|
||||
}
|
||||
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition);
|
||||
if (lastSeen == 0 && lastScrolled > 0) {
|
||||
lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled);
|
||||
}
|
||||
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition, threadSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,16 +113,9 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
}
|
||||
|
||||
private void setRecipientTitle(Recipient recipient) {
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isLocalNumber()) setSelfTitle();
|
||||
else setIndividualRecipientTitle(recipient);
|
||||
} else {
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isLocalNumber()) setSelfTitle();
|
||||
else if (TextUtils.isEmpty(recipient.getName(getContext()))) setNonContactRecipientTitle(recipient);
|
||||
else setContactRecipientTitle(recipient);
|
||||
}
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isLocalNumber()) setSelfTitle();
|
||||
else setIndividualRecipientTitle(recipient);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@@ -138,25 +131,8 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
updateSubtitleVisibility();
|
||||
}
|
||||
|
||||
private void setContactRecipientTitle(Recipient recipient) {
|
||||
this.title.setText(recipient.getName(getContext()));
|
||||
|
||||
if (TextUtils.isEmpty(recipient.getCustomLabel())) {
|
||||
this.subtitle.setText(null);
|
||||
} else {
|
||||
this.subtitle.setText(recipient.getCustomLabel());
|
||||
}
|
||||
|
||||
updateSubtitleVisibility();
|
||||
}
|
||||
|
||||
private void setGroupRecipientTitle(Recipient recipient) {
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
this.title.setText(recipient.getDisplayName(getContext()));
|
||||
} else {
|
||||
this.title.setText(recipient.getName(getContext()));
|
||||
}
|
||||
|
||||
this.title.setText(recipient.getDisplayName(getContext()));
|
||||
this.subtitle.setText(Stream.of(recipient.getParticipants())
|
||||
.sorted((a, b) -> Boolean.compare(a.isLocalNumber(), b.isLocalNumber()))
|
||||
.map(r -> r.isLocalNumber() ? getResources().getString(R.string.ConversationTitleView_you)
|
||||
|
||||
@@ -3,11 +3,8 @@ package org.thoughtcrime.securesms.conversation;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -15,19 +12,17 @@ import androidx.paging.DataSource;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationDataSource.Invalidator;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
class ConversationViewModel extends ViewModel {
|
||||
|
||||
@@ -40,7 +35,6 @@ class ConversationViewModel extends ViewModel {
|
||||
private final MutableLiveData<Long> threadId;
|
||||
private final LiveData<PagedList<MessageRecord>> messages;
|
||||
private final LiveData<ConversationData> conversationMetadata;
|
||||
private final List<Runnable> onNextMessageLoad;
|
||||
private final Invalidator invalidator;
|
||||
|
||||
private int jumpToPosition;
|
||||
@@ -51,28 +45,35 @@ class ConversationViewModel extends ViewModel {
|
||||
this.conversationRepository = new ConversationRepository();
|
||||
this.recentMedia = new MutableLiveData<>();
|
||||
this.threadId = new MutableLiveData<>();
|
||||
this.onNextMessageLoad = new CopyOnWriteArrayList<>();
|
||||
this.invalidator = new Invalidator();
|
||||
|
||||
LiveData<ConversationData> conversationDataForRequestedThreadId = Transformations.switchMap(threadId, thread -> {
|
||||
return conversationRepository.getConversationData(thread, jumpToPosition);
|
||||
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
|
||||
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
|
||||
|
||||
jumpToPosition = -1;
|
||||
|
||||
return conversationData;
|
||||
});
|
||||
|
||||
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(conversationDataForRequestedThreadId, data -> {
|
||||
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator, this::onMessagesUpdated);
|
||||
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
|
||||
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(25)
|
||||
.setInitialLoadSizeHint(25)
|
||||
.build();
|
||||
|
||||
final int startPosition;
|
||||
if (jumpToPosition > 0) {
|
||||
startPosition = jumpToPosition;
|
||||
} else {
|
||||
if (data.shouldJumpToMessage()) {
|
||||
startPosition = data.getJumpToPosition();
|
||||
} else if (data.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
|
||||
startPosition = data.getLastSeenPosition();
|
||||
} else if (data.isMessageRequestAccepted()) {
|
||||
startPosition = data.getLastScrolledPosition();
|
||||
} else {
|
||||
startPosition = data.getThreadSize();
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting at position " + startPosition + " :: " + jumpToPosition + " :: " + data.getLastSeenPosition());
|
||||
Log.d(TAG, "Starting at position startPosition: " + startPosition + " jumpToPosition: " + jumpToPosition + " lastSeenPosition: " + data.getLastSeenPosition() + " lastScrolledPosition: " + data.getLastScrolledPosition());
|
||||
|
||||
return Transformations.map(new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationDataSource.EXECUTOR)
|
||||
.setInitialLoadKey(Math.max(startPosition, 0))
|
||||
@@ -82,13 +83,11 @@ class ConversationViewModel extends ViewModel {
|
||||
|
||||
this.messages = Transformations.map(messagesForThreadId, Pair::second);
|
||||
|
||||
LiveData<Long> threadIdForLoadedMessages = Transformations.distinctUntilChanged(Transformations.map(messagesForThreadId, Pair::first));
|
||||
LiveData<DistinctConversationDataByThreadId> distinctData = LiveDataUtil.combineLatest(messagesForThreadId,
|
||||
metadata,
|
||||
(m, data) -> new DistinctConversationDataByThreadId(data));
|
||||
|
||||
conversationMetadata = Transformations.switchMap(threadIdForLoadedMessages, m -> {
|
||||
LiveData<ConversationData> data = conversationRepository.getConversationData(m, jumpToPosition);
|
||||
jumpToPosition = -1;
|
||||
return data;
|
||||
});
|
||||
conversationMetadata = Transformations.map(Transformations.distinctUntilChanged(distinctData), DistinctConversationDataByThreadId::getConversationData);
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
@@ -122,24 +121,12 @@ class ConversationViewModel extends ViewModel {
|
||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeenPosition() : 0;
|
||||
}
|
||||
|
||||
void scheduleForNextMessageUpdate(@NonNull Runnable runnable) {
|
||||
onNextMessageLoad.add(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
invalidator.invalidate();
|
||||
}
|
||||
|
||||
private void onMessagesUpdated() {
|
||||
for (Runnable runnable : onNextMessageLoad) {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
onNextMessageLoad.clear();
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
@@ -147,4 +134,29 @@ class ConversationViewModel extends ViewModel {
|
||||
return modelClass.cast(new ConversationViewModel());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DistinctConversationDataByThreadId {
|
||||
private final ConversationData conversationData;
|
||||
|
||||
private DistinctConversationDataByThreadId(@NonNull ConversationData conversationData) {
|
||||
this.conversationData = conversationData;
|
||||
}
|
||||
|
||||
public @NonNull ConversationData getConversationData() {
|
||||
return conversationData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DistinctConversationDataByThreadId that = (DistinctConversationDataByThreadId) o;
|
||||
return Objects.equals(conversationData.getThreadId(), that.conversationData.getThreadId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(conversationData.getThreadId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Wrapper class for helping show a list of recipients that had recent safety number changes.
|
||||
*
|
||||
* Also provides helper methods for behavior used in multiple spots.
|
||||
*/
|
||||
final class ChangedRecipient {
|
||||
private final Recipient recipient;
|
||||
private final IdentityRecord record;
|
||||
|
||||
ChangedRecipient(@NonNull Recipient recipient, @NonNull IdentityRecord record) {
|
||||
this.recipient = recipient;
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@NonNull IdentityRecord getIdentityRecord() {
|
||||
return record;
|
||||
}
|
||||
|
||||
boolean isUnverified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.UNVERIFIED;
|
||||
}
|
||||
|
||||
boolean isVerified() {
|
||||
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, SafetyNumberChangeAdapter.ViewHolder> {
|
||||
|
||||
private final Callbacks callbacks;
|
||||
|
||||
SafetyNumberChangeAdapter(@NonNull Callbacks callbacks) {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.safety_number_change_recipient, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final ChangedRecipient changedRecipient = getItem(position);
|
||||
holder.bind(changedRecipient);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final AvatarImageView avatar;
|
||||
final FromTextView name;
|
||||
final TextView subtitle;
|
||||
final View viewButton;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
avatar = itemView.findViewById(R.id.safety_number_change_recipient_avatar);
|
||||
name = itemView.findViewById(R.id.safety_number_change_recipient_name);
|
||||
subtitle = itemView.findViewById(R.id.safety_number_change_recipient_subtitle);
|
||||
viewButton = itemView.findViewById(R.id.safety_number_change_recipient_view);
|
||||
}
|
||||
|
||||
void bind(@NonNull ChangedRecipient changedRecipient) {
|
||||
avatar.setRecipient(changedRecipient.getRecipient());
|
||||
name.setText(changedRecipient.getRecipient());
|
||||
|
||||
if (changedRecipient.isUnverified() || changedRecipient.isVerified()) {
|
||||
subtitle.setText(R.string.safety_number_change_dialog__previous_verified);
|
||||
|
||||
Drawable check = ContextCompat.getDrawable(itemView.getContext(), R.drawable.check);
|
||||
if (check != null) {
|
||||
check.setBounds(0, 0, ViewUtil.dpToPx(12), ViewUtil.dpToPx(12));
|
||||
subtitle.setCompoundDrawables(check, null, null, null);
|
||||
}
|
||||
} else if (changedRecipient.getRecipient().hasAUserSetDisplayName(itemView.getContext())) {
|
||||
subtitle.setText(changedRecipient.getRecipient().getE164().or(""));
|
||||
subtitle.setCompoundDrawables(null, null, null, null);
|
||||
} else {
|
||||
subtitle.setText("");
|
||||
}
|
||||
subtitle.setVisibility(TextUtils.isEmpty(subtitle.getText()) ? View.GONE : View.VISIBLE);
|
||||
|
||||
viewButton.setOnClickListener(view -> callbacks.onViewIdentityRecord(changedRecipient.getIdentityRecord()));
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks {
|
||||
|
||||
private static final String RECIPIENT_IDS_EXTRA = "recipient_ids";
|
||||
private static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
|
||||
private SafetyNumberChangeViewModel viewModel;
|
||||
private SafetyNumberChangeAdapter adapter;
|
||||
private View dialogView;
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
List<String> ids = Stream.of(identityRecords)
|
||||
.map(record -> record.getRecipientId().serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(Context context, MessageRecord messageRecord) {
|
||||
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
|
||||
.map(mismatch -> mismatch.getRecipientId(context).serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
arguments.putLong(MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private SafetyNumberChangeDialog() { }
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
List<RecipientId> recipientIds = Stream.of(getArguments().getStringArray(RECIPIENT_IDS_EXTRA)).map(RecipientId::from).toList();
|
||||
long messageId = getArguments().getLong(MESSAGE_ID_EXTRA, -1);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new SafetyNumberChangeViewModel.Factory(recipientIds, (messageId != -1) ? messageId : null)).get(SafetyNumberChangeViewModel.class);
|
||||
viewModel.getChangedRecipients().observe(getViewLifecycleOwner(), adapter::submitList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme());
|
||||
|
||||
configureView(dialogView);
|
||||
|
||||
builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
dialogView = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void configureView(View view) {
|
||||
RecyclerView list = view.findViewById(R.id.safety_number_change_dialog_list);
|
||||
adapter = new SafetyNumberChangeAdapter(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setItemAnimator(null);
|
||||
list.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
}
|
||||
|
||||
private void handleSendAnyway(DialogInterface dialogInterface, int which) {
|
||||
Activity activity = getActivity();
|
||||
Callback callback;
|
||||
if (activity instanceof Callback) {
|
||||
callback = (Callback) activity;
|
||||
} else {
|
||||
callback = null;
|
||||
}
|
||||
|
||||
LiveData<TrustAndVerifyResult> trustOrVerifyResultLiveData = viewModel.trustOrVerifyChangedRecipients();
|
||||
|
||||
Observer<TrustAndVerifyResult> observer = new Observer<TrustAndVerifyResult>() {
|
||||
@Override
|
||||
public void onChanged(TrustAndVerifyResult result) {
|
||||
if (callback != null) {
|
||||
switch (result) {
|
||||
case TRUST_AND_VERIFY:
|
||||
callback.onSendAnywayAfterSafetyNumberChange();
|
||||
break;
|
||||
case TRUST_VERIFY_AND_RESEND:
|
||||
callback.onMessageResentAfterSafetyNumberChange();
|
||||
break;
|
||||
}
|
||||
}
|
||||
trustOrVerifyResultLiveData.removeObserver(this);
|
||||
}
|
||||
};
|
||||
|
||||
trustOrVerifyResultLiveData.observeForever(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onSendAnywayAfterSafetyNumberChange();
|
||||
void onMessageResentAfterSafetyNumberChange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
final class SafetyNumberChangeRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
SafetyNumberChangeRepository(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull LiveData<SafetyNumberChangeState> getSafetyNumberChangeState(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MutableLiveData<SafetyNumberChangeState> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getSafetyNumberChangeStateInternal(recipientIds, messageId)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsInternal(changedRecipients)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipientsAndResend(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
|
||||
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsAndResendInternal(changedRecipients, messageRecord)));
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull SafetyNumberChangeState getSafetyNumberChangeStateInternal(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
MessageRecord messageRecord = null;
|
||||
if (messageId != null) {
|
||||
messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
|
||||
|
||||
List<ChangedRecipient> changedRecipients = Stream.of(DatabaseFactory.getIdentityDatabase(context).getIdentities(recipients).getIdentityRecords())
|
||||
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
|
||||
.toList();
|
||||
|
||||
return new SafetyNumberChangeState(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
IdentityRecord identityRecord = changedRecipient.getIdentityRecord();
|
||||
|
||||
if (changedRecipient.isUnverified()) {
|
||||
identityDatabase.setVerified(identityRecord.getRecipientId(),
|
||||
identityRecord.getIdentityKey(),
|
||||
IdentityDatabase.VerifiedStatus.DEFAULT);
|
||||
} else {
|
||||
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_AND_VERIFY;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private TrustAndVerifyResult trustOrVerifyChangedRecipientsAndResendInternal(@NonNull List<ChangedRecipient> changedRecipients,
|
||||
@NonNull MessageRecord messageRecord) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
identityKeyStore.saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
processOutgoingMessageRecord(changedRecipients, messageRecord);
|
||||
}
|
||||
|
||||
return TrustAndVerifyResult.TRUST_VERIFY_AND_RESEND;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void processOutgoingMessageRecord(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
for (ChangedRecipient changedRecipient : changedRecipients) {
|
||||
RecipientId id = changedRecipient.getRecipient().getId();
|
||||
IdentityKey identityKey = changedRecipient.getIdentityRecord().getIdentityKey();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(context, messageRecord, id);
|
||||
} else {
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
|
||||
|
||||
MessageSender.resend(context, messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class SafetyNumberChangeState {
|
||||
|
||||
private final List<ChangedRecipient> changedRecipients;
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
SafetyNumberChangeState(List<ChangedRecipient> changedRecipients, @Nullable MessageRecord messageRecord) {
|
||||
this.changedRecipients = changedRecipients;
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
@NonNull List<ChangedRecipient> getChangedRecipients() {
|
||||
return changedRecipients;
|
||||
}
|
||||
|
||||
@Nullable MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository.SafetyNumberChangeState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SafetyNumberChangeViewModel extends ViewModel {
|
||||
|
||||
private final SafetyNumberChangeRepository safetyNumberChangeRepository;
|
||||
private final LiveData<SafetyNumberChangeState> safetyNumberChangeState;
|
||||
|
||||
private SafetyNumberChangeViewModel(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId, SafetyNumberChangeRepository safetyNumberChangeRepository) {
|
||||
this.safetyNumberChangeRepository = safetyNumberChangeRepository;
|
||||
safetyNumberChangeState = this.safetyNumberChangeRepository.getSafetyNumberChangeState(recipientIds, messageId);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<ChangedRecipient>> getChangedRecipients() {
|
||||
return Transformations.map(safetyNumberChangeState, SafetyNumberChangeState::getChangedRecipients);
|
||||
}
|
||||
|
||||
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients() {
|
||||
SafetyNumberChangeState state = Objects.requireNonNull(safetyNumberChangeState.getValue());
|
||||
if (state.getMessageRecord() != null) {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipientsAndResend(state.getChangedRecipients(), state.getMessageRecord());
|
||||
} else {
|
||||
return safetyNumberChangeRepository.trustOrVerifyChangedRecipients(state.getChangedRecipients());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
private final List<RecipientId> recipientIds;
|
||||
private final Long messageId;
|
||||
|
||||
public Factory(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
|
||||
this.recipientIds = recipientIds;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
SafetyNumberChangeRepository repo = new SafetyNumberChangeRepository(ApplicationDependencies.getApplication());
|
||||
return Objects.requireNonNull(modelClass.cast(new SafetyNumberChangeViewModel(recipientIds, messageId, repo)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
public enum TrustAndVerifyResult {
|
||||
TRUST_AND_VERIFY,
|
||||
TRUST_VERIFY_AND_RESEND,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -1,184 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationListItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A CursorAdapter for building a list of conversation threads.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationListAdapter.ViewHolder> {
|
||||
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1;
|
||||
private static final int MESSAGE_TYPE_THREAD = 2;
|
||||
private static final int MESSAGE_TYPE_INBOX_ZERO = 3;
|
||||
private static final int TYPE_THREAD = 1;
|
||||
private static final int TYPE_ACTION = 2;
|
||||
private static final int TYPE_PLACEHOLDER = 3;
|
||||
|
||||
private final @NonNull ThreadDatabase threadDatabase;
|
||||
private final @NonNull GlideRequests glideRequests;
|
||||
private final @NonNull Locale locale;
|
||||
private final @NonNull LayoutInflater inflater;
|
||||
private final @Nullable ItemClickListener clickListener;
|
||||
private final @NonNull MessageDigest digest;
|
||||
private enum Payload {
|
||||
TYPING_INDICATOR,
|
||||
SELECTION
|
||||
}
|
||||
|
||||
private final Map<Long, ThreadRecord> batchSet = Collections.synchronizedMap(new HashMap<>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
private final GlideRequests glideRequests;
|
||||
private final OnConversationClickListener onConversationClickListener;
|
||||
private final Map<Long, Conversation> batchSet = Collections.synchronizedMap(new HashMap<>());
|
||||
private boolean batchMode = false;
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
private int archived;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
|
||||
{
|
||||
super(itemView);
|
||||
}
|
||||
protected ConversationListAdapter(@NonNull GlideRequests glideRequests, @NonNull OnConversationClickListener onConversationClickListener) {
|
||||
super(new ConversationDiffCallback());
|
||||
|
||||
public BindableConversationListItem getItem() {
|
||||
return (BindableConversationListItem)itemView;
|
||||
}
|
||||
this.glideRequests = glideRequests;
|
||||
this.onConversationClickListener = onConversationClickListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(@NonNull Cursor cursor) {
|
||||
ThreadRecord record = getThreadRecord(cursor);
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == TYPE_ACTION) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_action, parent, false));
|
||||
|
||||
return Conversions.byteArrayToLong(digest.digest(record.getRecipient().getId().serialize().getBytes()));
|
||||
}
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
@Override
|
||||
protected long getFastAccessItemId(int position) {
|
||||
return super.getFastAccessItemId(position);
|
||||
}
|
||||
|
||||
ConversationListAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener)
|
||||
{
|
||||
super(context, cursor);
|
||||
try {
|
||||
this.glideRequests = glideRequests;
|
||||
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
this.locale = locale;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.clickListener = clickListener;
|
||||
this.digest = MessageDigest.getInstance("SHA1");
|
||||
setHasStableIds(true);
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
throw new AssertionError("SHA-1 missing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == MESSAGE_TYPE_SWITCH_ARCHIVE) {
|
||||
ConversationListItemAction action = (ConversationListItemAction) inflater.inflate(R.layout.conversation_list_item_action,
|
||||
parent, false);
|
||||
|
||||
action.setOnClickListener(v -> {
|
||||
if (clickListener != null) clickListener.onSwitchToArchive();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onConversationClickListener.onShowArchiveClick();
|
||||
}
|
||||
});
|
||||
|
||||
return new ViewHolder(action);
|
||||
} else if (viewType == MESSAGE_TYPE_INBOX_ZERO) {
|
||||
return new ViewHolder((ConversationListItemInboxZero)inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false));
|
||||
return holder;
|
||||
} else if (viewType == TYPE_THREAD) {
|
||||
ConversationViewHolder holder = new ConversationViewHolder(CachedInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onConversationClickListener.onConversationClick(getItem(position));
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
int position = holder.getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
return onConversationClickListener.onConversationLongClick(getItem(position));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return holder;
|
||||
} else if (viewType == TYPE_PLACEHOLDER) {
|
||||
View v = new FrameLayout(parent.getContext());
|
||||
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
||||
return new PlaceholderViewHolder(v);
|
||||
} else {
|
||||
final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view,
|
||||
parent, false);
|
||||
|
||||
item.setOnClickListener(view -> {
|
||||
if (clickListener != null) clickListener.onItemClick(item);
|
||||
});
|
||||
|
||||
item.setOnLongClickListener(view -> {
|
||||
if (clickListener != null) clickListener.onItemLongClick(item);
|
||||
return true;
|
||||
});
|
||||
|
||||
return new ViewHolder(item);
|
||||
throw new IllegalStateException("Unknown type! " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemViewRecycled(ViewHolder holder) {
|
||||
holder.getItem().unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet.keySet(), batchMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(@NonNull Cursor cursor) {
|
||||
ThreadRecord threadRecord = getThreadRecord(cursor);
|
||||
|
||||
if (threadRecord.getDistributionType() == ThreadDatabase.DistributionTypes.ARCHIVE) {
|
||||
return MESSAGE_TYPE_SWITCH_ARCHIVE;
|
||||
} else if (threadRecord.getDistributionType() == ThreadDatabase.DistributionTypes.INBOX_ZERO) {
|
||||
return MESSAGE_TYPE_INBOX_ZERO;
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
return MESSAGE_TYPE_THREAD;
|
||||
for (Object payloadObject : payloads) {
|
||||
if (payloadObject instanceof Payload) {
|
||||
Payload payload = (Payload) payloadObject;
|
||||
|
||||
if (payload == Payload.SELECTION) {
|
||||
((ConversationViewHolder) holder).getConversationListItem().setBatchMode(batchMode);
|
||||
} else {
|
||||
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTypingThreads(@NonNull Set<Long> threadsIds) {
|
||||
typingSet.clear();
|
||||
typingSet.addAll(threadsIds);
|
||||
notifyDataSetChanged();
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder.getItemViewType() == TYPE_ACTION) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
|
||||
casted.getConversationListItem().bind(new ThreadRecord.Builder(100)
|
||||
.setBody("")
|
||||
.setDate(100)
|
||||
.setRecipient(Recipient.UNKNOWN)
|
||||
.setCount(archived)
|
||||
.build(),
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
} else if (holder.getItemViewType() == TYPE_THREAD) {
|
||||
ConversationViewHolder casted = (ConversationViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
|
||||
casted.getConversationListItem().bind(conversation.getThreadRecord(),
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
getBatchSelectionIds(),
|
||||
batchMode);
|
||||
}
|
||||
}
|
||||
|
||||
private ThreadRecord getThreadRecord(@NonNull Cursor cursor) {
|
||||
return threadDatabase.readerFor(cursor).getCurrent();
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((ConversationViewHolder) holder).getConversationListItem().unbind();
|
||||
}
|
||||
}
|
||||
|
||||
void toggleThreadInBatchSet(@NonNull ThreadRecord thread) {
|
||||
if (batchSet.containsKey(thread.getThreadId())) {
|
||||
batchSet.remove(thread.getThreadId());
|
||||
} else if (thread.getThreadId() != -1) {
|
||||
batchSet.put(thread.getThreadId(), thread);
|
||||
void setTypingThreads(@NonNull Set<Long> typingThreadSet) {
|
||||
this.typingSet.clear();
|
||||
this.typingSet.addAll(typingThreadSet);
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.TYPING_INDICATOR);
|
||||
}
|
||||
|
||||
void toggleConversationInBatchSet(@NonNull Conversation conversation) {
|
||||
if (batchSet.containsKey(conversation.getThreadRecord().getThreadId())) {
|
||||
batchSet.remove(conversation.getThreadRecord().getThreadId());
|
||||
} else if (conversation.getThreadRecord().getThreadId() != -1) {
|
||||
batchSet.put(conversation.getThreadRecord().getThreadId(), conversation);
|
||||
}
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
Collection<Conversation> getBatchSelection() {
|
||||
return batchSet.values();
|
||||
}
|
||||
|
||||
void updateArchived(int archived) {
|
||||
int oldArchived = this.archived;
|
||||
|
||||
this.archived = archived;
|
||||
|
||||
if (oldArchived != archived) {
|
||||
if (archived == 0) {
|
||||
notifyItemRemoved(getItemCount());
|
||||
} else if (oldArchived == 0) {
|
||||
notifyItemInserted(getItemCount() - 1);
|
||||
} else {
|
||||
notifyItemChanged(getItemCount() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return (archived > 0 ? 1 : 0) + super.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (archived > 0 && position == getItemCount() - 1) {
|
||||
return TYPE_ACTION;
|
||||
} else if (getItem(position) == null) {
|
||||
return TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
return TYPE_THREAD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +212,15 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
return batchSet.keySet();
|
||||
}
|
||||
|
||||
@NonNull Set<ThreadRecord> getBatchSelection() {
|
||||
return new HashSet<>(batchSet.values());
|
||||
void selectAllThreads() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
void initializeBatchMode(boolean toggle) {
|
||||
@@ -196,23 +229,48 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
|
||||
}
|
||||
|
||||
private void unselectAllThreads() {
|
||||
this.batchSet.clear();
|
||||
this.notifyDataSetChanged();
|
||||
batchSet.clear();
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
void selectAllThreads() {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
ThreadRecord record = getThreadRecord(getCursorAtPositionOrThrow(i));
|
||||
if (record.getThreadId() != -1) {
|
||||
batchSet.put(record.getThreadId(), record);
|
||||
}
|
||||
static final class ConversationViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final BindableConversationListItem conversationListItem;
|
||||
|
||||
ConversationViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
conversationListItem = (BindableConversationListItem) itemView;
|
||||
}
|
||||
|
||||
public BindableConversationListItem getConversationListItem() {
|
||||
return conversationListItem;
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
void onItemClick(ConversationListItem item);
|
||||
void onItemLongClick(ConversationListItem item);
|
||||
void onSwitchToArchive();
|
||||
private static final class ConversationDiffCallback extends DiffUtil.ItemCallback<Conversation> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Conversation oldItem, @NonNull Conversation newItem) {
|
||||
return oldItem.getThreadRecord().getThreadId() == newItem.getThreadRecord().getThreadId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Conversation oldItem, @NonNull Conversation newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||
PlaceholderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
interface OnConversationClickListener {
|
||||
void onConversationClick(Conversation conversation);
|
||||
boolean onConversationLongClick(Conversation conversation);
|
||||
void onShowArchiveClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,9 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.MenuRes;
|
||||
@@ -35,22 +30,17 @@ import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
|
||||
|
||||
public class ConversationListArchiveFragment extends ConversationListFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>, ActionMode.Callback, ItemClickListener
|
||||
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
|
||||
{
|
||||
private RecyclerView list;
|
||||
private View emptyState;
|
||||
@@ -71,10 +61,10 @@ public class ConversationListArchiveFragment extends ConversationListFragment
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
list = view.findViewById(R.id.list);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
emptyState = view.findViewById(R.id.empty_state);
|
||||
list = view.findViewById(R.id.list);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
emptyState = view.findViewById(R.id.empty_state);
|
||||
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
Toolbar toolbar = view.findViewById(R.id.toolbar_basic);
|
||||
@@ -86,16 +76,14 @@ public class ConversationListArchiveFragment extends ConversationListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new ConversationListLoader(getActivity(), null, true);
|
||||
protected void onPostSubmitList() {
|
||||
list.setVisibility(View.VISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> arg0, Cursor cursor) {
|
||||
super.onLoadFinished(arg0, cursor);
|
||||
|
||||
list.setVisibility(View.VISIBLE);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
protected boolean isArchived() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
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.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;
|
||||
|
||||
protected ConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
ContentObserver contentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
THROTTLER.publish(() -> {
|
||||
invalidate();
|
||||
context.getContentResolver().unregisterContentObserver(this);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
invalidator.observe(() -> {
|
||||
invalidate();
|
||||
context.getContentResolver().unregisterContentObserver(contentObserver);
|
||||
});
|
||||
|
||||
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
|
||||
}
|
||||
|
||||
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
|
||||
else return new ArchivedConversationListDataSource(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Conversation> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
|
||||
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));
|
||||
effectiveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
|
||||
}
|
||||
|
||||
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Conversation> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<Conversation> conversations = new ArrayList<>(params.loadSize);
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
|
||||
ThreadRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
conversations.add(new Conversation(record));
|
||||
}
|
||||
}
|
||||
|
||||
callback.onResult(conversations);
|
||||
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
protected abstract int getTotalCount();
|
||||
protected abstract Cursor getCursor(long offset, long limit);
|
||||
|
||||
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
|
||||
|
||||
ArchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
super(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getArchivedConversationListCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getArchivedConversationList(offset, limit);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnarchivedConversationListDataSource extends ConversationListDataSource {
|
||||
|
||||
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
|
||||
super(context, invalidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getUnarchivedConversationListCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getConversationList(offset, limit);
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, Conversation> {
|
||||
|
||||
private final Context context;
|
||||
private final Invalidator invalidator;
|
||||
private final boolean isArchived;
|
||||
|
||||
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
|
||||
this.context = context;
|
||||
this.invalidator = invalidator;
|
||||
this.isArchived = isArchived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DataSource<Integer, Conversation> create() {
|
||||
return ConversationListDataSource.create(context, invalidator, isArchived);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,13 +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.database.Cursor;
|
||||
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;
|
||||
@@ -55,12 +53,11 @@ 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;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -91,13 +88,12 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
@@ -119,6 +115,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -126,28 +123,25 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
public class ConversationListFragment extends MainFragment implements LoaderManager.LoaderCallbacks<Cursor>,
|
||||
ActionMode.Callback,
|
||||
ItemClickListener,
|
||||
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
|
||||
ConversationListAdapter.OnConversationClickListener,
|
||||
ConversationListSearchAdapter.EventListener,
|
||||
MainNavigator.BackHandler,
|
||||
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);
|
||||
|
||||
@@ -157,23 +151,25 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
R.drawable.empty_inbox_4,
|
||||
R.drawable.empty_inbox_5 };
|
||||
|
||||
private ActionMode actionMode;
|
||||
private RecyclerView list;
|
||||
private ReminderView reminderView;
|
||||
private View emptyState;
|
||||
private ImageView emptyImage;
|
||||
private TextView searchEmptyState;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private View toolbarShadow;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private ConversationListSearchAdapter searchAdapter;
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
private ActionMode actionMode;
|
||||
private RecyclerView list;
|
||||
private ReminderView reminderView;
|
||||
private View emptyState;
|
||||
private ImageView emptyImage;
|
||||
private TextView searchEmptyState;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private View toolbarShadow;
|
||||
private ConversationListViewModel viewModel;
|
||||
private RecyclerView.Adapter activeAdapter;
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private ConversationListSearchAdapter searchAdapter;
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
|
||||
public static ConversationListFragment newInstance() {
|
||||
return new ConversationListFragment();
|
||||
@@ -214,10 +210,12 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
reminderView.setOnDismissListener(this::updateReminders);
|
||||
|
||||
list.setHasFixedSize(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
||||
list.setItemAnimator(new DeleteItemAnimator());
|
||||
list.addOnScrollListener(new ScrollListener());
|
||||
|
||||
snapToTopDataObserver = new SnapToTopDataObserver(list, null);
|
||||
|
||||
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
|
||||
|
||||
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
|
||||
@@ -247,7 +245,6 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
super.onResume();
|
||||
|
||||
updateReminders();
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(requireContext())) {
|
||||
@@ -257,9 +254,12 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
SimpleTask.run(getLifecycle(), Recipient::self, this::initializeProfileIcon);
|
||||
|
||||
if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
|
||||
activeAdapter = defaultAdapter;
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
list.setAdapter(defaultAdapter);
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
|
||||
if (activeAdapter != null) {
|
||||
activeAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +314,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
private boolean closeSearchIfOpen() {
|
||||
if (searchToolbar.isVisible() || activeAdapter == searchAdapter) {
|
||||
activeAdapter = defaultAdapter;
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
list.setAdapter(defaultAdapter);
|
||||
setAdapter(defaultAdapter);
|
||||
searchToolbar.collapse();
|
||||
return true;
|
||||
}
|
||||
@@ -330,20 +329,9 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +344,11 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowArchiveClick() {
|
||||
getNavigator().goToArchiveList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactClicked(@NonNull Recipient contact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
@@ -440,16 +433,14 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
if (trimmed.length() > 0) {
|
||||
if (activeAdapter != searchAdapter) {
|
||||
activeAdapter = searchAdapter;
|
||||
list.setAdapter(searchAdapter);
|
||||
setAdapter(searchAdapter);
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
list.addItemDecoration(searchAdapterDecoration);
|
||||
}
|
||||
} else {
|
||||
if (activeAdapter != defaultAdapter) {
|
||||
activeAdapter = defaultAdapter;
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
list.setAdapter(defaultAdapter);
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,19 +448,36 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
@Override
|
||||
public void onSearchClosed() {
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
list.setAdapter(defaultAdapter);
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeListAdapters() {
|
||||
defaultAdapter = new ConversationListAdapter (requireContext(), GlideApp.with(this), Locale.getDefault(), null, this);
|
||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault () );
|
||||
defaultAdapter = new ConversationListAdapter(GlideApp.with(this), this);
|
||||
searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault());
|
||||
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
|
||||
activeAdapter = defaultAdapter;
|
||||
|
||||
list.setAdapter(defaultAdapter);
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
setAdapter(defaultAdapter);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void setAdapter(@NonNull RecyclerView.Adapter adapter) {
|
||||
RecyclerView.Adapter oldAdapter = activeAdapter;
|
||||
|
||||
activeAdapter = adapter;
|
||||
|
||||
if (oldAdapter == activeAdapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
|
||||
if (adapter == defaultAdapter) {
|
||||
defaultAdapter.registerAdapterDataObserver(snapToTopDataObserver);
|
||||
} else {
|
||||
defaultAdapter.unregisterAdapterDataObserver(snapToTopDataObserver);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeTypingObserver() {
|
||||
@@ -482,11 +490,16 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
});
|
||||
}
|
||||
|
||||
protected boolean isArchived() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory()).get(ConversationListViewModel.class);
|
||||
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
|
||||
|
||||
viewModel.getSearchResult().observe(this, this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(this, this::onMegaphoneChanged);
|
||||
viewModel.getConversationList().observe(this, this::onSubmitList);
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
@@ -733,14 +746,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new ConversationListLoader(getActivity(), null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> arg0, Cursor cursor) {
|
||||
if (cursor == null || cursor.getCount() <= 0) {
|
||||
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
|
||||
if (conversationList.isEmpty()) {
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
|
||||
@@ -753,45 +760,39 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
cameraFab.stopPulse();
|
||||
}
|
||||
|
||||
defaultAdapter.changeCursor(cursor);
|
||||
defaultAdapter.submitList(conversationList.getConversations());
|
||||
defaultAdapter.updateArchived(conversationList.getArchivedCount());
|
||||
|
||||
onPostSubmitList();
|
||||
}
|
||||
|
||||
protected void onPostSubmitList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> arg0) {
|
||||
defaultAdapter.changeCursor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(ConversationListItem item) {
|
||||
public void onConversationClick(Conversation conversation) {
|
||||
if (actionMode == null) {
|
||||
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType());
|
||||
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
|
||||
} else {
|
||||
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
|
||||
adapter.toggleThreadInBatchSet(item.getThread());
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
|
||||
if (adapter.getBatchSelectionIds().size() == 0) {
|
||||
if (defaultAdapter.getBatchSelectionIds().size() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
|
||||
setCorrectMenuVisibility(actionMode.getMenu());
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(ConversationListItem item) {
|
||||
public boolean onConversationLongClick(Conversation conversation) {
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this);
|
||||
|
||||
defaultAdapter.initializeBatchMode(true);
|
||||
defaultAdapter.toggleThreadInBatchSet(item.getThread());
|
||||
defaultAdapter.notifyDataSetChanged();
|
||||
}
|
||||
defaultAdapter.toggleConversationInBatchSet(conversation);
|
||||
|
||||
@Override
|
||||
public void onSwitchToArchive() {
|
||||
getNavigator().goToArchiveList();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -870,7 +871,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
}
|
||||
|
||||
private void setCorrectMenuVisibility(@NonNull Menu menu) {
|
||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(thread -> !thread.isRead());
|
||||
boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead());
|
||||
|
||||
if (hasUnread) {
|
||||
menu.findItem(R.id.menu_mark_as_unread).setVisible(false);
|
||||
@@ -954,11 +955,10 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -976,7 +976,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
}
|
||||
|
||||
@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)
|
||||
@@ -984,28 +984,32 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,28 +174,19 @@ public class ConversationListItem extends RelativeLayout
|
||||
this.fromView.setText(recipient.get(), thread.isRead());
|
||||
}
|
||||
|
||||
if (typingThreads.contains(threadId)) {
|
||||
this.subjectView.setVisibility(INVISIBLE);
|
||||
updateTypingIndicator(typingThreads);
|
||||
|
||||
this.typingView.setVisibility(VISIBLE);
|
||||
this.typingView.startAnimation();
|
||||
} else {
|
||||
this.typingView.setVisibility(GONE);
|
||||
this.typingView.stopAnimation();
|
||||
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
|
||||
|
||||
this.subjectView.setVisibility(VISIBLE);
|
||||
this.subjectView.setText(getTrimmedSnippet(getThreadDisplayBody(getContext(), thread)));
|
||||
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
|
||||
groupAddedBy.observeForever(groupAddedByObserver);
|
||||
}
|
||||
|
||||
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
|
||||
if (thread.getGroupAddedBy() != null) {
|
||||
groupAddedBy = Recipient.live(thread.getGroupAddedBy());
|
||||
groupAddedBy.observeForever(groupAddedByObserver);
|
||||
}
|
||||
|
||||
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
|
||||
this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)
|
||||
: ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color));
|
||||
|
||||
if (thread.getDate() > 0) {
|
||||
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
|
||||
dateView.setText(date);
|
||||
@@ -291,11 +282,27 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void setBatchMode(boolean batchMode) {
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
this.batchMode = batchMode;
|
||||
setSelected(batchMode && selectedThreads.contains(thread.getThreadId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
|
||||
if (typingThreads.contains(threadId)) {
|
||||
this.subjectView.setVisibility(INVISIBLE);
|
||||
|
||||
this.typingView.setVisibility(VISIBLE);
|
||||
this.typingView.startAnimation();
|
||||
} else {
|
||||
this.typingView.setVisibility(GONE);
|
||||
this.typingView.stopAnimation();
|
||||
|
||||
this.subjectView.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient.get();
|
||||
}
|
||||
@@ -421,7 +428,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
} else if (SmsDatabase.Types.isMissedCall(thread.getType())) {
|
||||
return emphasisAdded(context.getString(org.thoughtcrime.securesms.R.string.ThreadRecord_missed_call));
|
||||
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().toShortString(context)));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, thread.getRecipient().getDisplayName(context)));
|
||||
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
|
||||
int seconds = (int)(thread.getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
@@ -433,7 +440,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
|
||||
} else {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().toShortString(context)));
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, thread.getRecipient().getDisplayName(context)));
|
||||
}
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
|
||||
@@ -55,4 +55,14 @@ public class ConversationListItemAction extends LinearLayout implements Bindable
|
||||
public void unbind() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,14 @@ public class ConversationListItemInboxZero extends LinearLayout implements Binda
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBatchMode(boolean batchMode) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
@@ -20,36 +25,67 @@ import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<Integer> archivedCount;
|
||||
private final LiveData<ConversationList> conversationList;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer debouncer;
|
||||
private final ContentObserver observer;
|
||||
private final Invalidator invalidator;
|
||||
|
||||
private String lastQuery;
|
||||
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository) {
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||
this.application = application;
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.archivedCount = new MutableLiveData<>();
|
||||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.debouncer = new Debouncer(300);
|
||||
this.invalidator = new Invalidator();
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
if (!TextUtils.isEmpty(getLastQuery())) {
|
||||
searchRepository.query(getLastQuery(), searchResult::postValue);
|
||||
}
|
||||
|
||||
if (!isArchived) {
|
||||
updateArchivedCount();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
|
||||
PagedList.Config config = new PagedList.Config.Builder()
|
||||
.setPageSize(15)
|
||||
.setInitialLoadSizeHint(30)
|
||||
.setEnablePlaceholders(true)
|
||||
.build();
|
||||
|
||||
LiveData<PagedList<Conversation>> conversationList = new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
|
||||
.setInitialLoadKey(0)
|
||||
.build();
|
||||
|
||||
if (isArchived) {
|
||||
this.archivedCount.setValue(0);
|
||||
} else {
|
||||
updateArchivedCount();
|
||||
}
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, observer);
|
||||
|
||||
this.conversationList = LiveDataUtil.combineLatest(conversationList, this.archivedCount, ConversationList::new);
|
||||
}
|
||||
|
||||
@NonNull LiveData<SearchResult> getSearchResult() {
|
||||
@@ -60,6 +96,10 @@ class ConversationListViewModel extends ViewModel {
|
||||
return megaphone;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ConversationList> getConversationList() {
|
||||
return conversationList;
|
||||
}
|
||||
|
||||
void onVisible() {
|
||||
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
||||
}
|
||||
@@ -95,15 +135,51 @@ class ConversationListViewModel extends ViewModel {
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
invalidator.invalidate();
|
||||
debouncer.clear();
|
||||
application.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
private void updateArchivedCount() {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
archivedCount.postValue(DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount());
|
||||
});
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final boolean isArchived;
|
||||
|
||||
public Factory(boolean isArchived) {
|
||||
this.isArchived = isArchived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository()));
|
||||
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
|
||||
}
|
||||
}
|
||||
|
||||
final static class ConversationList {
|
||||
private final PagedList<Conversation> conversations;
|
||||
private final int archivedCount;
|
||||
|
||||
ConversationList(PagedList<Conversation> conversations, int archivedCount) {
|
||||
this.conversations = conversations;
|
||||
this.archivedCount = archivedCount;
|
||||
}
|
||||
|
||||
PagedList<Conversation> getConversations() {
|
||||
return conversations;
|
||||
}
|
||||
|
||||
int getArchivedCount() {
|
||||
return archivedCount;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return conversations.isEmpty() && archivedCount == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.conversationlist.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
|
||||
public class Conversation {
|
||||
private final ThreadRecord threadRecord;
|
||||
|
||||
public Conversation(@NonNull ThreadRecord threadRecord) {
|
||||
this.threadRecord = threadRecord;
|
||||
}
|
||||
|
||||
public @NonNull ThreadRecord getThreadRecord() {
|
||||
return threadRecord;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return threadRecord.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class AttachmentDatabase extends Database {
|
||||
static final String WIDTH = "width";
|
||||
static final String HEIGHT = "height";
|
||||
static final String CAPTION = "caption";
|
||||
private static final String DATA_HASH = "data_hash";
|
||||
static final String DATA_HASH = "data_hash";
|
||||
static final String VISUAL_HASH = "blur_hash";
|
||||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
@@ -496,14 +496,20 @@ public class AttachmentDatabase extends Database {
|
||||
database.beginTransaction();
|
||||
try {
|
||||
for (AttachmentId weakReference : removableWeakReferences) {
|
||||
Log.i(TAG, String.format("[deleteAttachmentOnDisk] Deleting weak reference for %s %s", data, weakReference));
|
||||
deletedCount += database.delete(TABLE_NAME, PART_ID_WHERE, weakReference.toStrings());
|
||||
Log.i(TAG, String.format("[deleteAttachmentOnDisk] Clearing weak reference for %s %s", data, weakReference));
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull(DATA);
|
||||
values.putNull(DATA_RANDOM);
|
||||
values.putNull(DATA_HASH);
|
||||
values.putNull(THUMBNAIL);
|
||||
values.putNull(THUMBNAIL_RANDOM);
|
||||
deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings());
|
||||
}
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
String logMessage = String.format(Locale.US, "[deleteAttachmentOnDisk] Deleted %d/%d weak references for %s", deletedCount, removableWeakReferences.size(), data);
|
||||
String logMessage = String.format(Locale.US, "[deleteAttachmentOnDisk] Cleared %d/%d weak references for %s", deletedCount, removableWeakReferences.size(), data);
|
||||
if (deletedCount != removableWeakReferences.size()) {
|
||||
Log.w(TAG, logMessage);
|
||||
} else {
|
||||
|
||||
@@ -215,13 +215,15 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getPushGroupNamesContainingMember(RecipientId recipientId) {
|
||||
@WorkerThread
|
||||
public List<String> getPushGroupNamesContainingMember(@NonNull RecipientId recipientId) {
|
||||
return Stream.of(getPushGroupsContainingMember(recipientId))
|
||||
.map(GroupRecord::getTitle)
|
||||
.map(groupRecord -> Recipient.resolved(groupRecord.getRecipientId()).getDisplayName(context))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<GroupRecord> getPushGroupsContainingMember(RecipientId recipientId) {
|
||||
@WorkerThread
|
||||
public @NonNull List<GroupRecord> getPushGroupsContainingMember(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
|
||||
String query = MEMBERS + " LIKE ? AND " + MMS + " = ?";
|
||||
@@ -691,6 +693,10 @@ public final class GroupDatabase extends Database {
|
||||
return mms;
|
||||
}
|
||||
|
||||
public boolean isV1Group() {
|
||||
return !mms && !isV2Group();
|
||||
}
|
||||
|
||||
public boolean isV2Group() {
|
||||
return v2GroupProperties != null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
@@ -89,6 +91,10 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
|
||||
if (Stream.of(fullSpecs).map(FullSpec::getJobSpec).allMatch(JobSpec::isMemoryOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
@@ -149,32 +155,38 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
public synchronized void updateJobs(@NonNull List<JobSpec> jobs) {
|
||||
if (Stream.of(jobs).allMatch(JobSpec::isMemoryOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (JobSpec job : jobs) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
values.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
values.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
values.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
values.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
values.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
Stream.of(jobs)
|
||||
.filterNot(JobSpec::isMemoryOnly)
|
||||
.forEach(job -> {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
values.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
values.put(Jobs.QUEUE_KEY, job.getQueueKey());
|
||||
values.put(Jobs.CREATE_TIME, job.getCreateTime());
|
||||
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
|
||||
values.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
values.put(Jobs.IS_RUNNING, job.isRunning() ? 1 : 0);
|
||||
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ job.getId() };
|
||||
String query = Jobs.JOB_SPEC_ID + " = ?";
|
||||
String[] args = new String[]{ job.getId() };
|
||||
|
||||
db.update(Jobs.TABLE_NAME, values, query, args);
|
||||
}
|
||||
db.update(Jobs.TABLE_NAME, values, query, args);
|
||||
});
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@@ -228,6 +240,10 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
|
||||
if (job.isMemoryOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
|
||||
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
|
||||
@@ -247,21 +263,25 @@ public class JobDatabase extends Database {
|
||||
}
|
||||
|
||||
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
|
||||
for (ConstraintSpec constraintSpec : constraints) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
Stream.of(constraints)
|
||||
.filterNot(ConstraintSpec::isMemoryOnly)
|
||||
.forEach(constraintSpec -> {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
|
||||
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
|
||||
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
});
|
||||
}
|
||||
|
||||
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
|
||||
for (DependencySpec dependencySpec : dependencies) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
Stream.of(dependencies)
|
||||
.filterNot(DependencySpec::isMemoryOnly)
|
||||
.forEach(dependencySpec -> {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
|
||||
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
|
||||
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
|
||||
@@ -277,16 +297,19 @@ public class JobDatabase extends Database {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_INPUT_DATA)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1);
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1,
|
||||
false);
|
||||
}
|
||||
|
||||
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)),
|
||||
false);
|
||||
}
|
||||
|
||||
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
|
||||
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class MediaDatabase extends Database {
|
||||
+ " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND "
|
||||
+ MmsDatabase.VIEW_ONCE + " = 0 AND "
|
||||
+ AttachmentDatabase.DATA + " IS NOT NULL AND "
|
||||
+ AttachmentDatabase.QUOTE + " = 0 AND "
|
||||
+ "(" + AttachmentDatabase.QUOTE + " = 0 OR (" + AttachmentDatabase.QUOTE + " = 1 AND " + AttachmentDatabase.DATA_HASH + " IS NULL)) AND "
|
||||
+ AttachmentDatabase.STICKER_PACK_ID + " IS NULL ";
|
||||
|
||||
private static final String UNIQUE_MEDIA_QUERY = "SELECT "
|
||||
|
||||
@@ -153,13 +153,15 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
}
|
||||
|
||||
public void setAllReactionsSeen() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
String query = REACTIONS_UNREAD + " != ?";
|
||||
String[] args = new String[] { "0" };
|
||||
|
||||
values.put(REACTIONS_UNREAD, 0);
|
||||
values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
|
||||
|
||||
db.update(getTableName(), values, null, null);
|
||||
db.update(getTableName(), values, query, args);
|
||||
}
|
||||
|
||||
public void addReaction(long messageId, @NonNull ReactionRecord reaction) {
|
||||
|
||||
@@ -599,11 +599,13 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)";
|
||||
|
||||
for (long id : ids) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{String.valueOf(id)});
|
||||
db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)});
|
||||
|
||||
if (threadId < 0) {
|
||||
threadId = getThreadIdForMessage(id);
|
||||
|
||||
@@ -124,9 +124,9 @@ public class MmsSmsDatabase extends Database {
|
||||
return new Pair<>(id, latestQuit);
|
||||
}
|
||||
|
||||
public int getMessagePositionForLastSeen(long threadId, long lastSeen) {
|
||||
public int getMessagePositionOnOrAfterTimestamp(long threadId, long timestamp) {
|
||||
String[] projection = new String[] { "COUNT(*)" };
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + lastSeen;
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " >= " + timestamp;
|
||||
|
||||
try (Cursor cursor = queryTables(projection, selection, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
@@ -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);
|
||||
|
||||
@@ -3,10 +3,8 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
@@ -16,31 +14,38 @@ import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PushDatabase extends Database {
|
||||
|
||||
private static final String TAG = PushDatabase.class.getSimpleName();
|
||||
|
||||
private static final String TABLE_NAME = "push";
|
||||
public static final String ID = "_id";
|
||||
public static final String TYPE = "type";
|
||||
public static final String SOURCE_E164 = "source";
|
||||
public static final String SOURCE_UUID = "source_uuid";
|
||||
public static final String DEVICE_ID = "device_id";
|
||||
public static final String LEGACY_MSG = "body";
|
||||
public static final String CONTENT = "content";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
public static final String SERVER_TIMESTAMP = "server_timestamp";
|
||||
public static final String SERVER_GUID = "server_guid";
|
||||
private static final String TABLE_NAME = "push";
|
||||
public static final String ID = "_id";
|
||||
public static final String TYPE = "type";
|
||||
public static final String SOURCE_E164 = "source";
|
||||
public static final String SOURCE_UUID = "source_uuid";
|
||||
public static final String DEVICE_ID = "device_id";
|
||||
public static final String LEGACY_MSG = "body";
|
||||
public static final String CONTENT = "content";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
public static final String SERVER_RECEIVED_TIMESTAMP = "server_timestamp";
|
||||
public static final String SERVER_DELIVERED_TIMESTAMP = "server_delivered_timestamp";
|
||||
public static final String SERVER_GUID = "server_guid";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
TYPE + " INTEGER, " + SOURCE_E164 + " TEXT, " + SOURCE_UUID + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER, " +
|
||||
SERVER_TIMESTAMP + " INTEGER DEFAULT 0, " + SERVER_GUID + " TEXT DEFAULT NULL);";
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
TYPE + " INTEGER, " +
|
||||
SOURCE_E164 + " TEXT, " +
|
||||
SOURCE_UUID + " TEXT, " +
|
||||
DEVICE_ID + " INTEGER, " +
|
||||
LEGACY_MSG + " TEXT, " +
|
||||
CONTENT + " TEXT, " +
|
||||
TIMESTAMP + " INTEGER, " +
|
||||
SERVER_RECEIVED_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
SERVER_DELIVERED_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
SERVER_GUID + " TEXT DEFAULT NULL);";
|
||||
|
||||
public PushDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
@@ -60,7 +65,8 @@ public class PushDatabase extends Database {
|
||||
values.put(LEGACY_MSG, envelope.hasLegacyMessage() ? Base64.encodeBytes(envelope.getLegacyMessage()) : "");
|
||||
values.put(CONTENT, envelope.hasContent() ? Base64.encodeBytes(envelope.getContent()) : "");
|
||||
values.put(TIMESTAMP, envelope.getTimestamp());
|
||||
values.put(SERVER_TIMESTAMP, envelope.getServerTimestamp());
|
||||
values.put(SERVER_RECEIVED_TIMESTAMP, envelope.getServerReceivedTimestamp());
|
||||
values.put(SERVER_DELIVERED_TIMESTAMP, envelope.getServerDeliveredTimestamp());
|
||||
values.put(SERVER_GUID, envelope.getUuid());
|
||||
|
||||
return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
@@ -87,7 +93,8 @@ public class PushDatabase extends Database {
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)),
|
||||
Util.isEmpty(legacyMessage) ? null : Base64.decode(legacyMessage),
|
||||
Util.isEmpty(content) ? null : Base64.decode(content),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -154,15 +161,16 @@ public class PushDatabase extends Database {
|
||||
if (cursor == null || !cursor.moveToNext())
|
||||
return null;
|
||||
|
||||
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
||||
String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID));
|
||||
String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164));
|
||||
int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID));
|
||||
String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG));
|
||||
String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
long serverTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP));
|
||||
String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID));
|
||||
int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
|
||||
String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID));
|
||||
String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164));
|
||||
int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID));
|
||||
String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG));
|
||||
String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
long serverReceivedTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP));
|
||||
long serverDeliveredTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP));
|
||||
String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID));
|
||||
|
||||
return new SignalServiceEnvelope(type,
|
||||
SignalServiceAddress.fromRaw(sourceUuid, sourceE164),
|
||||
@@ -170,7 +178,8 @@ public class PushDatabase extends Database {
|
||||
timestamp,
|
||||
legacyMessage != null ? Base64.decode(legacyMessage) : null,
|
||||
content != null ? Base64.decode(content) : null,
|
||||
serverTimestamp,
|
||||
serverReceivedTimestamp,
|
||||
serverDeliveredTimestamp,
|
||||
serverGuid);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
@@ -60,6 +61,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -102,6 +104,7 @@ public class RecipientDatabase extends Database {
|
||||
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
|
||||
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
|
||||
private static final String PROFILE_SHARING = "profile_sharing";
|
||||
private static final String LAST_PROFILE_FETCH = "last_profile_fetch";
|
||||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
private static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String UUID_CAPABILITY = "uuid_supported";
|
||||
@@ -123,7 +126,8 @@ public class RecipientDatabase extends Database {
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH,
|
||||
NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION,
|
||||
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
|
||||
@@ -166,6 +170,10 @@ public class RecipientDatabase extends Database {
|
||||
public static VibrateState fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
|
||||
public static VibrateState fromBoolean(boolean enabled) {
|
||||
return enabled ? ENABLED : DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RegisteredState {
|
||||
@@ -295,6 +303,7 @@ public class RecipientDatabase extends Database {
|
||||
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
|
||||
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
|
||||
LAST_PROFILE_FETCH + " INTEGER DEFAULT 0, " +
|
||||
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
|
||||
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
|
||||
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
@@ -842,6 +851,7 @@ public class RecipientDatabase extends Database {
|
||||
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
|
||||
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
|
||||
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
@@ -908,7 +918,7 @@ public class RecipientDatabase extends Database {
|
||||
systemDisplayName, systemContactPhoto,
|
||||
systemPhoneLabel, systemContactUri,
|
||||
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar,
|
||||
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing,
|
||||
AvatarHelper.hasAvatar(context, RecipientId.from(id)), profileSharing, lastProfileFetch,
|
||||
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection,
|
||||
Recipient.Capability.deserialize(uuidCapabilityValue),
|
||||
@@ -1587,6 +1597,53 @@ public class RecipientDatabase extends Database {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param lastInteractionThreshold Only include contacts that have been interacted with since this time.
|
||||
* @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time.
|
||||
* @param limit Only return at most this many contact.
|
||||
*/
|
||||
public List<RecipientId> getRecipientsForRoutineProfileFetch(long lastInteractionThreshold, long lastProfileFetchThreshold, int limit) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Set<Recipient> recipientsWithinInteractionThreshold = new LinkedHashSet<>();
|
||||
|
||||
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false))) {
|
||||
ThreadRecord record;
|
||||
|
||||
while ((record = reader.getNext()) != null && record.getDate() > lastInteractionThreshold) {
|
||||
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
recipientsWithinInteractionThreshold.addAll(recipient.getParticipants());
|
||||
} else {
|
||||
recipientsWithinInteractionThreshold.add(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Stream.of(recipientsWithinInteractionThreshold)
|
||||
.filterNot(Recipient::isLocalNumber)
|
||||
.filter(r -> r.getLastProfileFetchTime() < lastProfileFetchThreshold)
|
||||
.limit(limit)
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void markProfilesFetched(@NonNull Collection<RecipientId> ids, long time) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(LAST_PROFILE_FETCH, time);
|
||||
|
||||
for (RecipientId id : ids) {
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { id.serialize() });
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
|
||||
List<String> blockedE164 = Stream.of(blocked)
|
||||
.filter(b -> b.getNumber().isPresent())
|
||||
@@ -1885,6 +1942,7 @@ public class RecipientDatabase extends Database {
|
||||
private final String signalProfileAvatar;
|
||||
private final boolean hasProfileImage;
|
||||
private final boolean profileSharing;
|
||||
private final long lastProfileFetch;
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
@@ -1923,6 +1981,7 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String signalProfileAvatar,
|
||||
boolean hasProfileImage,
|
||||
boolean profileSharing,
|
||||
long lastProfileFetch,
|
||||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection,
|
||||
@@ -1961,6 +2020,7 @@ public class RecipientDatabase extends Database {
|
||||
this.signalProfileAvatar = signalProfileAvatar;
|
||||
this.hasProfileImage = hasProfileImage;
|
||||
this.profileSharing = profileSharing;
|
||||
this.lastProfileFetch = lastProfileFetch;
|
||||
this.notificationChannel = notificationChannel;
|
||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
@@ -2091,6 +2151,10 @@ public class RecipientDatabase extends Database {
|
||||
return profileSharing;
|
||||
}
|
||||
|
||||
public long getLastProfileFetch() {
|
||||
return lastProfileFetch;
|
||||
}
|
||||
|
||||
public @Nullable String getNotificationChannel() {
|
||||
return notificationChannel;
|
||||
}
|
||||
|
||||
@@ -366,11 +366,13 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)";
|
||||
|
||||
for (long id : ids) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(EXPIRE_STARTED, startedAtTimestamp);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{String.valueOf(id)});
|
||||
db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)});
|
||||
|
||||
if (threadId < 0) {
|
||||
threadId = getThreadIdForMessage(id);
|
||||
|
||||
@@ -90,6 +90,7 @@ public class ThreadDatabase extends Database {
|
||||
public static final String EXPIRES_IN = "expires_in";
|
||||
public static final String LAST_SEEN = "last_seen";
|
||||
public static final String HAS_SENT = "has_sent";
|
||||
private static final String LAST_SCROLLED = "last_scrolled";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
DATE + " INTEGER DEFAULT 0, " +
|
||||
@@ -111,7 +112,8 @@ public class ThreadDatabase extends Database {
|
||||
LAST_SEEN + " INTEGER DEFAULT 0, " +
|
||||
HAS_SENT + " INTEGER DEFAULT 0, " +
|
||||
READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
UNREAD_COUNT + " INTEGER DEFAULT 0);";
|
||||
UNREAD_COUNT + " INTEGER DEFAULT 0, " +
|
||||
LAST_SCROLLED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
|
||||
@@ -120,7 +122,7 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
private static final String[] THREAD_PROJECTION = {
|
||||
ID, DATE, MESSAGE_COUNT, RECIPIENT_ID, SNIPPET, SNIPPET_CHARSET, READ, UNREAD_COUNT, TYPE, ERROR, SNIPPET_TYPE,
|
||||
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT
|
||||
SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, ARCHIVED, STATUS, DELIVERY_RECEIPT_COUNT, EXPIRES_IN, LAST_SEEN, READ_RECEIPT_COUNT, LAST_SCROLLED
|
||||
};
|
||||
|
||||
private static final List<String> TYPED_THREAD_PROJECTION = Stream.of(THREAD_PROJECTION)
|
||||
@@ -550,9 +552,21 @@ public class ThreadDatabase extends Database {
|
||||
return positions;
|
||||
}
|
||||
|
||||
public Cursor getConversationList(long offset, long limit) {
|
||||
return getConversationList("0", offset, limit);
|
||||
}
|
||||
|
||||
public Cursor getArchivedConversationList(long offset, long limit) {
|
||||
return getConversationList("1", offset, limit);
|
||||
}
|
||||
|
||||
private Cursor getConversationList(String archived) {
|
||||
return getConversationList(archived, 0, 0);
|
||||
}
|
||||
|
||||
private Cursor getConversationList(@NonNull String archived, long offset, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", 0);
|
||||
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", offset, limit);
|
||||
Cursor cursor = db.rawQuery(query, new String[]{archived});
|
||||
|
||||
setNotifyConversationListListeners(cursor);
|
||||
@@ -560,20 +574,24 @@ public class ThreadDatabase extends Database {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public int getUnarchivedConversationListCount() {
|
||||
return getConversationListCount(false);
|
||||
}
|
||||
|
||||
public int getArchivedConversationListCount() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
return getConversationListCount(true);
|
||||
}
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[] {"COUNT(*)"}, ARCHIVED + " = ?",
|
||||
new String[] {"1"}, null, null, null);
|
||||
private int getConversationListCount(boolean archived) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] columns = new String[] { "COUNT(*)" };
|
||||
String query = ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0";
|
||||
String[] args = new String[] { archived ? "1" : "0" };
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -618,18 +636,26 @@ public class ThreadDatabase extends Database {
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public Pair<Long, Boolean> getLastSeenAndHasSent(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||
public void setLastScrolled(long threadId, long lastScrolledTimestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
||||
try {
|
||||
contentValues.put(LAST_SCROLLED, lastScrolledTimestamp);
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
|
||||
}
|
||||
|
||||
public ConversationMetadata getConversationMetadata(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN, HAS_SENT, LAST_SCROLLED}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return new Pair<>(cursor.getLong(0), cursor.getLong(1) == 1);
|
||||
return new ConversationMetadata(cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(HAS_SENT)) == 1,
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SCROLLED)));
|
||||
}
|
||||
|
||||
return new Pair<>(-1L, false);
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
return new ConversationMetadata(-1L, false, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +870,11 @@ public class ThreadDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull String createQuery(@NonNull String where, int limit) {
|
||||
private @NonNull String createQuery(@NonNull String where, long limit) {
|
||||
return createQuery(where, 0, limit);
|
||||
}
|
||||
|
||||
private @NonNull String createQuery(@NonNull String where, long offset, long limit) {
|
||||
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
|
||||
String query =
|
||||
"SELECT " + projection + " FROM " + TABLE_NAME +
|
||||
@@ -859,6 +889,10 @@ public class ThreadDatabase extends Database {
|
||||
query += " LIMIT " + limit;
|
||||
}
|
||||
|
||||
if (offset > 0) {
|
||||
query += " OFFSET " + offset;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -916,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)))
|
||||
@@ -1060,4 +1094,28 @@ public class ThreadDatabase extends Database {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConversationMetadata {
|
||||
private final long lastSeen;
|
||||
private final boolean hasSent;
|
||||
private final long lastScrolled;
|
||||
|
||||
public ConversationMetadata(long lastSeen, boolean hasSent, long lastScrolled) {
|
||||
this.lastSeen = lastSeen;
|
||||
this.hasSent = hasSent;
|
||||
this.lastScrolled = lastScrolled;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public boolean hasSent() {
|
||||
return hasSent;
|
||||
}
|
||||
|
||||
public long getLastScrolled() {
|
||||
return lastScrolled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +134,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int SERVER_TIMESTAMP = 59;
|
||||
private static final int REMOTE_DELETE = 60;
|
||||
private static final int COLOR_MIGRATION = 61;
|
||||
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 = 61;
|
||||
private static final int DATABASE_VERSION = 65;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -906,6 +910,51 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < LAST_SCROLLED) {
|
||||
db.execSQL("ALTER TABLE thread ADD COLUMN last_scrolled INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < LAST_PROFILE_FETCH) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN last_profile_fetch INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < SERVER_DELIVERED_TIMESTAMP) {
|
||||
db.execSQL("ALTER TABLE push ADD COLUMN server_delivered_timestamp INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < QUOTE_CLEANUP) {
|
||||
String query = "SELECT _data " +
|
||||
"FROM (SELECT _data, MIN(quote) AS all_quotes " +
|
||||
"FROM part " +
|
||||
"WHERE _data NOT NULL AND data_hash NOT NULL " +
|
||||
"GROUP BY _data) " +
|
||||
"WHERE all_quotes = 1";
|
||||
|
||||
int count = 0;
|
||||
|
||||
try (Cursor cursor = db.rawQuery(query, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow("_data"));
|
||||
|
||||
if (new File(data).delete()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.putNull("_data");
|
||||
values.putNull("data_random");
|
||||
values.putNull("thumbnail");
|
||||
values.putNull("thumbnail_random");
|
||||
values.putNull("data_hash");
|
||||
db.update("part", values, "_data = ?", new String[] { data });
|
||||
|
||||
count++;
|
||||
} else {
|
||||
Log.w(TAG, "[QuoteCleanup] Failed to delete " + data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "[QuoteCleanup] Cleaned up " + count + " quotes.");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.thoughtcrime.securesms.database.identity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -24,6 +27,10 @@ public final class IdentityRecordList {
|
||||
identityRecords.addAll(identityRecordList.identityRecords);
|
||||
}
|
||||
|
||||
public List<IdentityRecord> getIdentityRecords() {
|
||||
return Collections.unmodifiableList(identityRecords);
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
for (IdentityRecord identityRecord : identityRecords) {
|
||||
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationListLoader extends AbstractCursorLoader {
|
||||
|
||||
private final String filter;
|
||||
private final boolean archived;
|
||||
|
||||
public ConversationListLoader(Context context, String filter, boolean archived) {
|
||||
super(context);
|
||||
this.filter = filter;
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
if (filter != null && filter.trim().length() != 0) return getFilteredConversationList(filter);
|
||||
else if (!archived) return getUnarchivedConversationList();
|
||||
else return getArchivedConversationList();
|
||||
}
|
||||
|
||||
private Cursor getUnarchivedConversationList() {
|
||||
List<Cursor> cursorList = new LinkedList<>();
|
||||
cursorList.add(DatabaseFactory.getThreadDatabase(context).getConversationList());
|
||||
|
||||
int archivedCount = DatabaseFactory.getThreadDatabase(context)
|
||||
.getArchivedConversationListCount();
|
||||
|
||||
if (archivedCount > 0) {
|
||||
MatrixCursor switchToArchiveCursor = new MatrixCursor(new String[] {
|
||||
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
|
||||
ThreadDatabase.RECIPIENT_ID, ThreadDatabase.SNIPPET, ThreadDatabase.READ, ThreadDatabase.UNREAD_COUNT,
|
||||
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
|
||||
ThreadDatabase.SNIPPET_CONTENT_TYPE, ThreadDatabase.SNIPPET_EXTRAS,
|
||||
ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS, ThreadDatabase.DELIVERY_RECEIPT_COUNT,
|
||||
ThreadDatabase.EXPIRES_IN, ThreadDatabase.LAST_SEEN, ThreadDatabase.READ_RECEIPT_COUNT}, 1);
|
||||
|
||||
|
||||
if (cursorList.get(0).getCount() <= 0) {
|
||||
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
|
||||
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.INBOX_ZERO,
|
||||
0, null, null, null, 0, -1, 0, 0, 0, -1});
|
||||
}
|
||||
|
||||
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
|
||||
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.ARCHIVE,
|
||||
0, null, null, null, 0, -1, 0, 0, 0, -1});
|
||||
|
||||
cursorList.add(switchToArchiveCursor);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursorList.toArray(new Cursor[0]));
|
||||
}
|
||||
|
||||
private Cursor getArchivedConversationList() {
|
||||
return DatabaseFactory.getThreadDatabase(context).getArchivedConversationList();
|
||||
}
|
||||
|
||||
private Cursor getFilteredConversationList(String filter) {
|
||||
List<String> numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter);
|
||||
List<RecipientId> recipientIds = new LinkedList<>();
|
||||
|
||||
for (String number : numbers) {
|
||||
recipientIds.add(Recipient.external(context, number).getId());
|
||||
}
|
||||
|
||||
return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(recipientIds);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,10 @@ public abstract class DisplayRecord {
|
||||
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||
}
|
||||
|
||||
public boolean isSent() {
|
||||
return MmsSmsColumns.Types.isSentType(type);
|
||||
}
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||
}
|
||||
|
||||
@@ -116,32 +116,32 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
} else if (isGroupQuit() && isOutgoing()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
|
||||
} else if (isGroupQuit()) {
|
||||
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString(context)));
|
||||
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIncomingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().toShortString(context)));
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isOutgoingCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
|
||||
} else if (isMissedCall()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
|
||||
} else if (isJoined()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().toShortString(context)));
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isExpirationTimerUpdate()) {
|
||||
int seconds = (int)(getExpiresIn() / 1000);
|
||||
if (seconds <= 0) {
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().toShortString(context)));
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().getDisplayName(context)));
|
||||
}
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(context), time));
|
||||
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().getDisplayName(context), time));
|
||||
} else if (isIdentityUpdate()) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString(context)));
|
||||
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityVerified()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString(context)));
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (isIdentityDefault()) {
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString(context)));
|
||||
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().getDisplayName(context)));
|
||||
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().getDisplayName(context)));
|
||||
}
|
||||
|
||||
return new SpannableString(getBody());
|
||||
@@ -178,7 +178,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a UUID by it's corresponding recipient's {@link Recipient#toShortString}.
|
||||
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
|
||||
*/
|
||||
private static class ShortStringDescriptionStrategy implements GroupsV2UpdateMessageProducer.DescribeMemberStrategy {
|
||||
|
||||
@@ -193,7 +193,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
return context.getString(R.string.MessageRecord_unknown);
|
||||
}
|
||||
return Recipient.resolved(RecipientId.from(uuid, null)).toShortString(context);
|
||||
return Recipient.resolved(RecipientId.from(uuid, null)).getDisplayName(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -84,7 +84,7 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
} else if (isEndSession() && isOutgoing()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
|
||||
} else if (isEndSession()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().toShortString(context)));
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().getDisplayName(context)));
|
||||
} else if (SmsDatabase.Types.isUnsupportedMessageType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version));
|
||||
} else if (SmsDatabase.Types.isInvalidMessageType(type)) {
|
||||
|
||||
@@ -187,6 +187,53 @@ public final class ThreadRecord {
|
||||
else return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ThreadRecord that = (ThreadRecord) o;
|
||||
return threadId == that.threadId &&
|
||||
type == that.type &&
|
||||
date == that.date &&
|
||||
deliveryStatus == that.deliveryStatus &&
|
||||
deliveryReceiptCount == that.deliveryReceiptCount &&
|
||||
readReceiptCount == that.readReceiptCount &&
|
||||
count == that.count &&
|
||||
unreadCount == that.unreadCount &&
|
||||
forcedUnread == that.forcedUnread &&
|
||||
distributionType == that.distributionType &&
|
||||
archived == that.archived &&
|
||||
expiresIn == that.expiresIn &&
|
||||
lastSeen == that.lastSeen &&
|
||||
body.equals(that.body) &&
|
||||
recipient.equals(that.recipient) &&
|
||||
Objects.equals(snippetUri, that.snippetUri) &&
|
||||
Objects.equals(contentType, that.contentType) &&
|
||||
Objects.equals(extra, that.extra);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(threadId,
|
||||
body,
|
||||
recipient,
|
||||
type,
|
||||
date,
|
||||
deliveryStatus,
|
||||
deliveryReceiptCount,
|
||||
readReceiptCount,
|
||||
snippetUri,
|
||||
contentType,
|
||||
extra,
|
||||
count,
|
||||
unreadCount,
|
||||
forcedUnread,
|
||||
distributionType,
|
||||
archived,
|
||||
expiresIn,
|
||||
lastSeen);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private long threadId;
|
||||
private String body;
|
||||
|
||||
@@ -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;
|
||||
@@ -46,6 +55,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.ConnectivityListener;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ApplicationDependencies.Provider} that provides real app dependencies.
|
||||
@@ -91,7 +101,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
provideClientZkOperations().getProfileOperations(),
|
||||
SignalExecutors.UNBOUNDED);
|
||||
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,8 +143,10 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
.setJobFactories(JobManagerFactories.getJobFactories(context))
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(context))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(context))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context)))
|
||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context), SignalExecutors.newCachedSingleThreadExecutor("signal-fast-job-storage")))
|
||||
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushDecryptMessageJob.KEY, PushProcessMessageJob.KEY, MarkerJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY))
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
@@ -21,15 +20,13 @@ import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -37,7 +34,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class GiphyActivity extends PassphraseRequiredActionBarActivity
|
||||
public class GiphyActivity extends PassphraseRequiredActivity
|
||||
implements GiphyActivityToolbar.OnLayoutChangedListener,
|
||||
GiphyActivityToolbar.OnFilterChangedListener,
|
||||
GiphyAdapter.OnItemClickListener
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -17,6 +16,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.giph.net.GiphyLoader;
|
||||
@@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class GiphyFragment extends Fragment implements LoaderManager.LoaderCallbacks<List<GiphyImage>>, GiphyAdapter.OnItemClickListener {
|
||||
public abstract class GiphyFragment extends LoggingFragment implements LoaderManager.LoaderCallbacks<List<GiphyImage>>, GiphyAdapter.OnItemClickListener {
|
||||
|
||||
private static final String TAG = GiphyFragment.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements Resou
|
||||
public boolean handles(@NonNull File source, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
Log.i(TAG, "Checking item for encrypted Bitmap cache decoder: " + source.toString());
|
||||
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
@@ -41,12 +39,10 @@ public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements Resou
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
|
||||
public @Nullable Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
Log.i(TAG, "Encrypted Bitmap cache decoder running: " + source.toString());
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ public class EncryptedGifCacheDecoder extends EncryptedCoder implements Resource
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull File source, @NonNull Options options) {
|
||||
Log.i(TAG, "Checking item for encrypted GIF cache decoder: " + source.toString());
|
||||
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
@@ -39,9 +37,8 @@ public class EncryptedGifCacheDecoder extends EncryptedCoder implements Resource
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
public @Nullable Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
Log.i(TAG, "Encrypted GIF cache decoder running...");
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.decode(inputStream, width, height, options);
|
||||
|
||||
@@ -110,7 +110,7 @@ public final class GroupV1MessageProcessor {
|
||||
|
||||
Recipient sender = Recipient.externalPush(context, content.getSender());
|
||||
|
||||
if (FeatureFlags.messageRequests() && (sender.isSystemContact() || sender.isProfileSharing())) {
|
||||
if (sender.isSystemContact() || sender.isProfileSharing()) {
|
||||
Log.i(TAG, "Auto-enabling profile sharing because 'adder' is trusted. contact: " + sender.isSystemContact() + ", profileSharing: " + sender.isProfileSharing());
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.externalGroup(context, id).getId(), true);
|
||||
}
|
||||
@@ -245,7 +245,7 @@ public final class GroupV1MessageProcessor {
|
||||
} else {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
String body = Base64.encodeBytes(storage.toByteArray());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt());
|
||||
IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, storage, body);
|
||||
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -52,7 +53,13 @@ public final class LiveGroup {
|
||||
}
|
||||
|
||||
public LiveData<String> getTitle() {
|
||||
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getTitle);
|
||||
return LiveDataUtil.combineLatest(groupRecord, recipient, (groupRecord, recipient) -> {
|
||||
String title = groupRecord.getTitle();
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
return title;
|
||||
}
|
||||
return recipient.getDisplayName(ApplicationDependencies.getApplication());
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<Recipient> getGroupRecipient() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user