mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-17 00:15:40 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f9f62992f | ||
|
|
1938d6cae0 | ||
|
|
13e8c55781 | ||
|
|
6264f9b585 | ||
|
|
4482bfcabb | ||
|
|
015088a53f | ||
|
|
ef7d707432 | ||
|
|
d1f6a924fb | ||
|
|
f312757daf | ||
|
|
1d83729e6c | ||
|
|
6a45858b4a | ||
|
|
1b448c2bdf | ||
|
|
f6cd190245 | ||
|
|
23303e5407 | ||
|
|
b5237848e9 | ||
|
|
7cac0c9a7c | ||
|
|
9dbbe4675f | ||
|
|
95978f16e9 | ||
|
|
d055bba452 | ||
|
|
8ef809a02b | ||
|
|
458941f952 | ||
|
|
5852a508aa | ||
|
|
e2b4995fbb | ||
|
|
a3556d9f68 | ||
|
|
fe890a1a41 | ||
|
|
c06bb18249 | ||
|
|
9099969b41 | ||
|
|
6358f59f67 | ||
|
|
073034dd3c | ||
|
|
17fb815805 | ||
|
|
409e7c41b4 | ||
|
|
b9a1a5027c | ||
|
|
49535f6378 | ||
|
|
c058452605 | ||
|
|
b3511dba77 | ||
|
|
afbe27c55f | ||
|
|
41d227207d | ||
|
|
92b586c061 | ||
|
|
acbc17c909 | ||
|
|
15f17747ee | ||
|
|
781054fc9d | ||
|
|
b59769a30a | ||
|
|
26e0e09e24 | ||
|
|
3a2990a911 | ||
|
|
d8060b3041 | ||
|
|
f42ec5318f | ||
|
|
bd0d425cbf | ||
|
|
b3d5d7c33e | ||
|
|
1746869dc3 | ||
|
|
633f4cbbe5 | ||
|
|
0944e2f758 | ||
|
|
b49e4004ab | ||
|
|
68381f8b64 | ||
|
|
f180066058 | ||
|
|
6b7de2e85e | ||
|
|
c650a978e9 | ||
|
|
e05cadafe6 | ||
|
|
c6008a4f90 | ||
|
|
5624855eba | ||
|
|
799ff86fc0 | ||
|
|
798fc84e82 | ||
|
|
cc363a3c88 |
@@ -61,8 +61,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 775
|
||||
def canonicalVersionName = "5.3.2"
|
||||
def canonicalVersionCode = 781
|
||||
def canonicalVersionName = "5.3.8"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -162,10 +162,6 @@ android {
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (keystores['debug'] != null) {
|
||||
@@ -315,8 +311,6 @@ dependencies {
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
|
||||
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
|
||||
|
||||
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
@@ -340,11 +334,11 @@ dependencies {
|
||||
implementation project(':video')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation 'org.whispersystems:signal-client-android:0.2.2'
|
||||
implementation 'org.whispersystems:signal-client-android:0.1.5'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.8.10'
|
||||
implementation 'org.signal:ringrtc-android:2.9.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
||||
@@ -24,7 +24,7 @@ public final class Log {
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
e(tag, message, null);
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
@@ -21,7 +21,7 @@ import android.os.Message;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
|
||||
@@ -287,6 +287,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 90) {
|
||||
Log.i(TAG, "Detected a new install that doesn't have passphrases disabled -- assuming bad initialization.");
|
||||
AppInitialization.onRepairFirstEverAppLaunch(this);
|
||||
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 912) {
|
||||
Log.i(TAG, "Detected a not-recent install that doesn't have passphrases disabled -- disabling now.");
|
||||
TextSecurePreferences.setPasswordDisabled(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ public class InsetAwareConstraintLayout extends ConstraintLayout {
|
||||
protected boolean fitSystemWindows(Rect insets) {
|
||||
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
|
||||
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
|
||||
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
|
||||
Guideline parentEndGuideline = findViewById(R.id.parent_end_guideline);
|
||||
|
||||
if (statusBarGuideline != null) {
|
||||
statusBarGuideline.setGuidelineBegin(insets.top);
|
||||
@@ -42,6 +44,14 @@ public class InsetAwareConstraintLayout extends ConstraintLayout {
|
||||
navigationBarGuideline.setGuidelineEnd(insets.bottom);
|
||||
}
|
||||
|
||||
if (parentStartGuideline != null) {
|
||||
parentStartGuideline.setGuidelineBegin(insets.left);
|
||||
}
|
||||
|
||||
if (parentEndGuideline != null) {
|
||||
parentEndGuideline.setGuidelineEnd(insets.right);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ public class ContactRepository {
|
||||
static final String ABOUT_COLUMN = "about";
|
||||
|
||||
static final int NORMAL_TYPE = 0;
|
||||
static final int PUSH_TYPE = 1;
|
||||
static final int NEW_PHONE_TYPE = 2;
|
||||
static final int NEW_USERNAME_TYPE = 3;
|
||||
static final int RECENT_TYPE = 4;
|
||||
static final int DIVIDER_TYPE = 5;
|
||||
static final int PUSH_TYPE = 1 << 0;
|
||||
static final int NEW_PHONE_TYPE = 1 << 2;
|
||||
static final int NEW_USERNAME_TYPE = 1 << 3;
|
||||
static final int RECENT_TYPE = 1 << 4;
|
||||
static final int DIVIDER_TYPE = 1 << 5;
|
||||
|
||||
/** Maps the recipient results to the legacy contact column names */
|
||||
private static final List<Pair<String, ValueMapper>> SEARCH_CURSOR_MAPPERS = new ArrayList<Pair<String, ValueMapper>>() {{
|
||||
@@ -87,13 +87,13 @@ public class ContactRepository {
|
||||
String aboutEmoji = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT_EMOJI);
|
||||
String about = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT);
|
||||
|
||||
if (aboutEmoji != null) {
|
||||
if (about != null) {
|
||||
if (!Util.isEmpty(aboutEmoji)) {
|
||||
if (!Util.isEmpty(about)) {
|
||||
return aboutEmoji + " " + about;
|
||||
} else {
|
||||
return aboutEmoji;
|
||||
}
|
||||
} else if (about != null) {
|
||||
} else if (!Util.isEmpty(about)) {
|
||||
return about;
|
||||
} else {
|
||||
return "";
|
||||
|
||||
@@ -215,9 +215,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
String label = CursorUtil.requireString(cursor, ContactRepository.LABEL_COLUMN);
|
||||
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
|
||||
numberType, label).toString();
|
||||
boolean isPush = (contactType & ContactRepository.PUSH_TYPE) > 0;
|
||||
|
||||
int color = (contactType == ContactRepository.PUSH_TYPE) ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
|
||||
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
|
||||
int color = isPush ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
|
||||
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
|
||||
|
||||
boolean currentContact = currentContacts.contains(id);
|
||||
|
||||
@@ -314,7 +315,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
private @NonNull String getHeaderString(int position) {
|
||||
int contactType = getContactType(position);
|
||||
|
||||
if (contactType == ContactRepository.RECENT_TYPE || contactType == ContactRepository.DIVIDER_TYPE) {
|
||||
if ((contactType & ContactRepository.RECENT_TYPE) > 0 || contactType == ContactRepository.DIVIDER_TYPE) {
|
||||
return " ";
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
||||
|
||||
@@ -135,8 +136,11 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
|
||||
addContactsSection(cursorList);
|
||||
addGroupsSection(cursorList);
|
||||
addNewNumberSection(cursorList);
|
||||
addUsernameSearchSection(cursorList);
|
||||
|
||||
if (!hideNewNumberOrUsername(mode)) {
|
||||
addNewNumberSection(cursorList);
|
||||
addUsernameSearchSection(cursorList);
|
||||
}
|
||||
|
||||
return cursorList;
|
||||
}
|
||||
@@ -275,7 +279,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext());
|
||||
|
||||
MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX);
|
||||
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode))) {
|
||||
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode), !smsEnabled(mode))) {
|
||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations);
|
||||
ThreadRecord threadRecord;
|
||||
while ((threadRecord = reader.getNext()) != null) {
|
||||
@@ -287,7 +291,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
stringId,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
|
||||
"",
|
||||
ContactRepository.RECENT_TYPE,
|
||||
ContactRepository.RECENT_TYPE | (recipient.isRegistered() && !recipient.isForceSmsSelection() ? ContactRepository.PUSH_TYPE : 0),
|
||||
recipient.getCombinedAboutAndEmoji() });
|
||||
}
|
||||
}
|
||||
@@ -429,6 +433,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_GROUPS_V1);
|
||||
}
|
||||
|
||||
private static boolean hideNewNumberOrUsername(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_NEW);
|
||||
}
|
||||
|
||||
private static boolean flagSet(int mode, int flag) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,6 @@ import org.thoughtcrime.securesms.util.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
@@ -344,26 +343,26 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private static final int SMS_DEFAULT = 11;
|
||||
private static final int MEDIA_SENDER = 12;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
protected ComposeText composeText;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private ImageButton attachButton;
|
||||
protected ConversationTitleView titleView;
|
||||
private TextView charactersLeft;
|
||||
private ConversationFragment fragment;
|
||||
private Button unblockButton;
|
||||
private Button makeDefaultSmsButton;
|
||||
private Button registerButton;
|
||||
private InputAwareLayout container;
|
||||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
private MessageRequestsBottomView messageRequestBottomView;
|
||||
private ConversationReactionOverlay reactionOverlay;
|
||||
private GlideRequests glideRequests;
|
||||
protected ComposeText composeText;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private ImageButton attachButton;
|
||||
protected ConversationTitleView titleView;
|
||||
private TextView charactersLeft;
|
||||
private ConversationFragment fragment;
|
||||
private Button unblockButton;
|
||||
private Button makeDefaultSmsButton;
|
||||
private Button registerButton;
|
||||
private InputAwareLayout container;
|
||||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
private MessageRequestsBottomView messageRequestBottomView;
|
||||
private ConversationReactionDelegate reactionDelegate;
|
||||
|
||||
private AttachmentManager attachmentManager;
|
||||
private AudioRecorder audioRecorder;
|
||||
@@ -429,7 +428,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
setContentView(R.layout.conversation_activity);
|
||||
|
||||
getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
|
||||
WindowUtil.setLightNavigationBar(getWindow());
|
||||
WindowUtil.setLightNavigationBarFromTheme(this);
|
||||
|
||||
fragment = initFragment(R.id.fragment_content, new ConversationFragment(), dynamicLanguage.getCurrentLocale());
|
||||
|
||||
@@ -594,8 +593,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
container.hideAttachedInput(true);
|
||||
}
|
||||
|
||||
if (reactionOverlay != null && reactionOverlay.isShowing()) {
|
||||
reactionOverlay.hide();
|
||||
if (reactionDelegate.isShowing()) {
|
||||
reactionDelegate.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +607,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
return reactionOverlay.applyTouchEvent(ev) || super.dispatchTouchEvent(ev);
|
||||
return reactionDelegate.applyTouchEvent(ev) || super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1030,8 +1029,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
Log.d(TAG, "onBackPressed()");
|
||||
if (reactionOverlay.isShowing()) {
|
||||
reactionOverlay.hide();
|
||||
if (reactionDelegate.isShowing()) {
|
||||
reactionDelegate.hide();
|
||||
} else if (container.isInputOpen()) {
|
||||
container.hideCurrentInput(composeText);
|
||||
} else {
|
||||
@@ -1919,7 +1918,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
panelParent = findViewById(R.id.conversation_activity_panel_parent);
|
||||
searchNav = findViewById(R.id.conversation_search_nav);
|
||||
messageRequestBottomView = findViewById(R.id.conversation_activity_message_request_bottom_bar);
|
||||
reactionOverlay = findViewById(R.id.conversation_reaction_scrubber);
|
||||
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||
wallpaper = findViewById(R.id.conversation_wallpaper);
|
||||
wallpaperDim = findViewById(R.id.conversation_wallpaper_dim);
|
||||
@@ -1927,6 +1925,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
|
||||
|
||||
Stub<ConversationReactionOverlay> reactionOverlayStub = ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub);
|
||||
reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
|
||||
|
||||
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
@@ -1984,7 +1986,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
|
||||
|
||||
reactionOverlay.setOnReactionSelectedListener(this);
|
||||
reactionDelegate.setOnReactionSelectedListener(this);
|
||||
|
||||
joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient()));
|
||||
}
|
||||
@@ -2212,7 +2214,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
public void onReactionSelected(MessageRecord messageRecord, String emoji) {
|
||||
final Context context = getApplicationContext();
|
||||
|
||||
reactionOverlay.hide();
|
||||
reactionDelegate.hide();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
|
||||
@@ -2238,14 +2240,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (oldRecord != null && hasAddedCustomEmoji) {
|
||||
final Context context = getApplicationContext();
|
||||
|
||||
reactionOverlay.hide();
|
||||
reactionDelegate.hide();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context,
|
||||
messageRecord.getId(),
|
||||
messageRecord.isMms(),
|
||||
oldRecord));
|
||||
} else {
|
||||
reactionOverlay.hideAllButMask();
|
||||
reactionDelegate.hideAllButMask();
|
||||
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage)
|
||||
.show(getSupportFragmentManager(), "BOTTOM");
|
||||
@@ -2254,7 +2256,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onReactWithAnyEmojiDialogDismissed() {
|
||||
reactionOverlay.hideMask();
|
||||
reactionDelegate.hideMask();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2950,6 +2952,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public void onRecorderLocked() {
|
||||
updateToggleButtonState();
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3014,6 +3017,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
vibrator.vibrate(50);
|
||||
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
|
||||
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
|
||||
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
|
||||
@@ -3126,7 +3130,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void onReactionsDialogDismissed() {
|
||||
reactionOverlay.hideMask();
|
||||
reactionDelegate.hideMask();
|
||||
}
|
||||
|
||||
// Listeners
|
||||
@@ -3357,14 +3361,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||
{
|
||||
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionOverlay.setOnHideListener(onHideListener);
|
||||
reactionOverlay.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
|
||||
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionDelegate.setOnHideListener(onHideListener);
|
||||
reactionDelegate.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListVerticalTranslationChanged(float translationY) {
|
||||
reactionOverlay.setListVerticalTranslation(translationY);
|
||||
reactionDelegate.setListVerticalTranslation(translationY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3384,22 +3388,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public void handleReactionDetails(@NonNull View maskTarget) {
|
||||
reactionOverlay.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
if (!reactionDelegate.isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
//noinspection CodeBlock2Expr
|
||||
return DatabaseFactory.getMmsSmsDatabase(this)
|
||||
.checkMessageExists(reactionOverlay.getMessageRecord());
|
||||
.checkMessageExists(reactionDelegate.getMessageRecord());
|
||||
}, messageExists -> {
|
||||
if (!messageExists) {
|
||||
reactionOverlay.hide();
|
||||
reactionDelegate.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -363,8 +363,10 @@ public class ConversationAdapter
|
||||
|
||||
if (hasWallpaper) {
|
||||
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
|
||||
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.transparent_black_80));
|
||||
} else {
|
||||
viewHolder.clearBackground();
|
||||
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.core_grey_45));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,10 +607,12 @@ public class ConversationAdapter
|
||||
|
||||
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView textView;
|
||||
View divider;
|
||||
|
||||
StickyHeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
divider = itemView.findViewById(R.id.last_seen_divider);
|
||||
}
|
||||
|
||||
StickyHeaderViewHolder(TextView textView) {
|
||||
@@ -628,6 +632,12 @@ public class ConversationAdapter
|
||||
textView.setBackgroundResource(resId);
|
||||
}
|
||||
|
||||
public void setDividerColor(@ColorInt int color) {
|
||||
if (divider != null) {
|
||||
divider.setBackgroundColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearBackground() {
|
||||
textView.setBackground(null);
|
||||
}
|
||||
|
||||
@@ -1178,7 +1178,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (groupSender != null) {
|
||||
int stickerAuthorColor = ContextCompat.getColor(context, R.color.signal_text_primary);
|
||||
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord, hasWallpaper)) {
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord, false)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
} else if (!hasWallpaper && hasNoBubble(messageRecord)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
/**
|
||||
* Delegate class that mimics the ConversationReactionOverlay public API
|
||||
*
|
||||
* This allows us to properly stub out the ConversationReactionOverlay View class while still
|
||||
* respecting listeners and other positional information that can be set BEFORE we want to actually
|
||||
* resolve the view.
|
||||
*/
|
||||
final class ConversationReactionDelegate {
|
||||
|
||||
private final Stub<ConversationReactionOverlay> overlayStub;
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private ConversationReactionOverlay.OnHideListener onHideListener;
|
||||
private float translationY;
|
||||
|
||||
ConversationReactionDelegate(@NonNull Stub<ConversationReactionOverlay> overlayStub) {
|
||||
this.overlayStub = overlayStub;
|
||||
}
|
||||
|
||||
boolean isShowing() {
|
||||
return overlayStub.resolved() && overlayStub.get().isShowing();
|
||||
}
|
||||
|
||||
void show(@NonNull Activity activity,
|
||||
@NonNull View maskTarget,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
int maskPaddingBottom)
|
||||
{
|
||||
resolveOverlay().show(activity, maskTarget, conversationRecipient, messageRecord, maskPaddingBottom, lastSeenDownPoint);
|
||||
}
|
||||
|
||||
void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||
resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
overlayStub.get().hide();
|
||||
}
|
||||
|
||||
void hideAllButMask() {
|
||||
overlayStub.get().hideAllButMask();
|
||||
}
|
||||
|
||||
void hideMask() {
|
||||
overlayStub.get().hideMask();
|
||||
}
|
||||
|
||||
void setOnReactionSelectedListener(@NonNull ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener) {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setOnReactionSelectedListener(onReactionSelectedListener);
|
||||
}
|
||||
}
|
||||
|
||||
void setOnToolbarItemClickedListener(@NonNull Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
}
|
||||
}
|
||||
|
||||
void setOnHideListener(@NonNull ConversationReactionOverlay.OnHideListener onHideListener) {
|
||||
this.onHideListener = onHideListener;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setOnHideListener(onHideListener);
|
||||
}
|
||||
}
|
||||
|
||||
void setListVerticalTranslation(float translationY) {
|
||||
this.translationY = translationY;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setListVerticalTranslation(translationY);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull MessageRecord getMessageRecord() {
|
||||
if (!overlayStub.resolved()) {
|
||||
throw new IllegalStateException("Cannot call getMessageRecord right now.");
|
||||
}
|
||||
|
||||
return overlayStub.get().getMessageRecord();
|
||||
}
|
||||
|
||||
boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
|
||||
if (!overlayStub.resolved() || !overlayStub.get().isShowing()) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
lastSeenDownPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return overlayStub.get().applyTouchEvent(motionEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ConversationReactionOverlay resolveOverlay() {
|
||||
ConversationReactionOverlay overlay = overlayStub.get();
|
||||
|
||||
overlay.setListVerticalTranslation(translationY);
|
||||
overlay.setOnHideListener(onHideListener);
|
||||
overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
overlay.setOnReactionSelectedListener(onReactionSelectedListener);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private final Boundary horizontalEmojiBoundary = new Boundary();
|
||||
private final Boundary verticalScrubBoundary = new Boundary();
|
||||
private final PointF deadzoneTouchPoint = new PointF();
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
@@ -149,7 +148,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
@NonNull View maskTarget,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
int maskPaddingBottom)
|
||||
int maskPaddingBottom,
|
||||
@NonNull PointF lastSeenDownPoint)
|
||||
{
|
||||
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
@@ -292,10 +292,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
|
||||
if (!isShowing()) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
lastSeenDownPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
}
|
||||
return false;
|
||||
throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber.");
|
||||
}
|
||||
|
||||
if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -30,10 +31,12 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
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.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -52,15 +55,15 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
private Set<ConversationMessage> batchSelected;
|
||||
|
||||
private TextView body;
|
||||
private MaterialButton actionButton;
|
||||
private View background;
|
||||
private ConversationMessage conversationMessage;
|
||||
private Recipient conversationRecipient;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private MessageRecord messageRecord;
|
||||
private LiveData<Spannable> displayBody;
|
||||
private EventListener eventListener;
|
||||
private TextView body;
|
||||
private MaterialButton actionButton;
|
||||
private View background;
|
||||
private ConversationMessage conversationMessage;
|
||||
private Recipient conversationRecipient;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private MessageRecord messageRecord;
|
||||
private LiveData<SpannableString> displayBody;
|
||||
private EventListener eventListener;
|
||||
|
||||
private final UpdateObserver updateObserver = new UpdateObserver();
|
||||
|
||||
@@ -101,7 +104,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
bind(lifecycleOwner, conversationMessage, nextMessageRecord, conversationRecipient, hasWallpaper);
|
||||
bind(lifecycleOwner, conversationMessage, previousMessageRecord, nextMessageRecord, conversationRecipient, hasWallpaper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,6 +119,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
private void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean hasWallpaper)
|
||||
@@ -133,12 +137,6 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
groupObserver.observe(lifecycleOwner, null);
|
||||
}
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.wallpaper_bubble_background_12);
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
|
||||
int textColor = ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color);
|
||||
if (ThemeUtil.isDarkTheme(getContext()) && hasWallpaper) {
|
||||
textColor = ContextCompat.getColor(getContext(), R.color.core_grey_15);
|
||||
@@ -152,17 +150,29 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor);
|
||||
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<SpannableString> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, textColor);
|
||||
LiveData<SpannableString> spannableMessage = loading(liveUpdateMessage);
|
||||
|
||||
observeDisplayBody(lifecycleOwner, spannableMessage);
|
||||
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient);
|
||||
|
||||
presentBackground(shouldCollapse(messageRecord, previousMessageRecord),
|
||||
shouldCollapse(messageRecord, nextMessageRecord),
|
||||
hasWallpaper);
|
||||
}
|
||||
|
||||
private static boolean shouldCollapse(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> candidate)
|
||||
{
|
||||
return candidate.isPresent() &&
|
||||
candidate.get().isUpdate() &&
|
||||
DateUtils.isSameDay(current.getTimestamp(), candidate.get().getTimestamp()) &&
|
||||
isSameType(current, candidate.get());
|
||||
}
|
||||
|
||||
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
|
||||
private @NonNull LiveData<Spannable> loading(@NonNull LiveData<Spannable> string) {
|
||||
private @NonNull LiveData<SpannableString> loading(@NonNull LiveData<SpannableString> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(getContext().getString(R.string.ConversationUpdateItem_loading))));
|
||||
}
|
||||
|
||||
@@ -198,7 +208,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<Spannable> displayBody) {
|
||||
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
|
||||
if (this.displayBody != displayBody) {
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.removeObserver(updateObserver);
|
||||
@@ -221,7 +231,10 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void present(ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Recipient conversationRecipient) {
|
||||
private void present(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
{
|
||||
if (batchSelected.contains(conversationMessage)) setSelected(true);
|
||||
else setSelected(false);
|
||||
|
||||
@@ -294,6 +307,78 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper) {
|
||||
int marginDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_margin);
|
||||
int marginCollapsed = 0;
|
||||
int paddingDefault = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_padding);
|
||||
int paddingCollapsed = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_update_vertical_padding_collapsed);
|
||||
|
||||
if (collapseAbove && collapseBelow) {
|
||||
ViewUtil.setTopMargin(background, marginCollapsed);
|
||||
ViewUtil.setBottomMargin(background, marginCollapsed);
|
||||
|
||||
ViewUtil.setPaddingTop(background, paddingCollapsed);
|
||||
ViewUtil.setPaddingBottom(background, paddingCollapsed);
|
||||
|
||||
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_middle);
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
} else if (collapseAbove) {
|
||||
ViewUtil.setTopMargin(background, marginCollapsed);
|
||||
ViewUtil.setBottomMargin(background, marginDefault);
|
||||
|
||||
ViewUtil.setPaddingTop(background, paddingCollapsed);
|
||||
ViewUtil.setPaddingBottom(background, paddingDefault);
|
||||
|
||||
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_bottom);
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
} else if (collapseBelow) {
|
||||
ViewUtil.setTopMargin(background, marginDefault);
|
||||
ViewUtil.setBottomMargin(background, marginCollapsed);
|
||||
|
||||
ViewUtil.setPaddingTop(background, paddingDefault);
|
||||
ViewUtil.setPaddingBottom(background, paddingCollapsed);
|
||||
|
||||
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_top);
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
} else {
|
||||
ViewUtil.setTopMargin(background, marginDefault);
|
||||
ViewUtil.setBottomMargin(background, marginDefault);
|
||||
|
||||
ViewUtil.setPaddingTop(background, paddingDefault);
|
||||
ViewUtil.setPaddingBottom(background, paddingDefault);
|
||||
|
||||
ViewUtil.updateLayoutParams(background, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (hasWallpaper) {
|
||||
background.setBackgroundResource(R.drawable.conversation_update_wallpaper_background_singular);
|
||||
} else {
|
||||
background.setBackground(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSameType(@NonNull MessageRecord current, @NonNull MessageRecord candidate) {
|
||||
return (current.isGroupUpdate() && candidate.isGroupUpdate()) ||
|
||||
(current.isProfileChange() && candidate.isProfileChange()) ||
|
||||
(current.isGroupCall() && candidate.isGroupCall()) ||
|
||||
(current.isExpirationTimerUpdate() && candidate.isExpirationTimerUpdate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(View.OnClickListener l) {
|
||||
super.setOnClickListener(new InternalClickListener(l));
|
||||
|
||||
@@ -99,10 +99,11 @@ final class MenuState {
|
||||
.shouldShowSaveAttachmentAction(!actionMessage &&
|
||||
!viewOnce &&
|
||||
messageRecord.isMms() &&
|
||||
!messageRecord.isMediaPending() &&
|
||||
!messageRecord.isMmsNotification() &&
|
||||
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
|
||||
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
|
||||
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
|
||||
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete && !messageRecord.isMediaPending())
|
||||
.shouldShowDetailsAction(!actionMessage)
|
||||
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest));
|
||||
}
|
||||
|
||||
@@ -1026,24 +1026,34 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
Snackbar.LENGTH_LONG,
|
||||
false)
|
||||
{
|
||||
private final ThreadDatabase threadDatabase= DatabaseFactory.getThreadDatabase(getActivity());
|
||||
|
||||
private List<Long> pinnedThreadIds;
|
||||
|
||||
@Override
|
||||
protected void executeAction(@Nullable Long parameter) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
|
||||
Context context = requireActivity();
|
||||
|
||||
pinnedThreadIds = threadDatabase.getPinnedThreadIds();
|
||||
threadDatabase.archiveConversation(threadId);
|
||||
|
||||
if (unreadCount > 0) {
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(getActivity()).setRead(threadId, false);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
MarkReadReceiver.process(getActivity(), messageIds);
|
||||
List<MarkedMessageInfo> messageIds = threadDatabase.setRead(threadId, false);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reverseAction(@Nullable Long parameter) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
|
||||
Context context = requireActivity();
|
||||
|
||||
threadDatabase.unarchiveConversation(threadId);
|
||||
threadDatabase.restorePins(pinnedThreadIds);
|
||||
|
||||
if (unreadCount > 0) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).incrementUnread(threadId, unreadCount);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(getActivity());
|
||||
threadDatabase.incrementUnread(threadId, unreadCount);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TextAppearanceSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
@@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
@@ -487,11 +489,47 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
|
||||
} else {
|
||||
return LiveDataUtil.just(new SpannableString(removeNewlines(thread.getBody())));
|
||||
String body = removeNewlines(thread.getBody());
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
RecipientId groupMessageSender = thread.getGroupMessageSender();
|
||||
if (!groupMessageSender.isUnknown()) {
|
||||
return describeGroupMessage(context, body, groupMessageSender);
|
||||
}
|
||||
}
|
||||
return LiveDataUtil.just(new SpannableString(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LiveData<SpannableString> describeGroupMessage(@NonNull Context context,
|
||||
@NonNull String body,
|
||||
@NonNull RecipientId groupMessageSender)
|
||||
{
|
||||
return whileLoadingShow(body, recipientToStringAsync(groupMessageSender,
|
||||
r -> createGroupMessageUpdateString(context, body, r)));
|
||||
}
|
||||
|
||||
private static SpannableString createGroupMessageUpdateString(@NonNull Context context,
|
||||
@NonNull String body,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
String sender = (recipient.isSelf() ? context.getString(R.string.MessageRecord_you)
|
||||
: recipient.getShortDisplayName(context)) + ": ";
|
||||
|
||||
SpannableString spannable = new SpannableString(sender + body);
|
||||
spannable.setSpan(new TextAppearanceSpan(context, R.style.Signal_Text_Preview_Medium),
|
||||
0,
|
||||
sender.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return spannable;
|
||||
}
|
||||
|
||||
/** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
|
||||
private static @NonNull LiveData<SpannableString> whileLoadingShow(@NonNull String loading, @NonNull LiveData<SpannableString> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(loading)));
|
||||
}
|
||||
|
||||
private static @NonNull String removeNewlines(@Nullable String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
@@ -512,7 +550,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description, defaultTint));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<Spannable> description) {
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<SpannableString> description) {
|
||||
return Transformations.map(description, sequence -> {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new StyleSpan(Typeface.ITALIC),
|
||||
|
||||
@@ -60,8 +60,8 @@ public class TextSecureSessionStore implements SessionStore {
|
||||
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
|
||||
|
||||
return sessionRecord != null &&
|
||||
sessionRecord.hasSenderChain() &&
|
||||
sessionRecord.getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
|
||||
sessionRecord.getSessionState().hasSenderChain() &&
|
||||
sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
private static final String NEXT_RUN_ATTEMPT_TIME = "next_run_attempt_time";
|
||||
private static final String RUN_ATTEMPT = "run_attempt";
|
||||
private static final String MAX_ATTEMPTS = "max_attempts";
|
||||
private static final String MAX_BACKOFF = "max_backoff";
|
||||
private static final String LIFESPAN = "lifespan";
|
||||
private static final String SERIALIZED_DATA = "serialized_data";
|
||||
private static final String SERIALIZED_INPUT_DATA = "serialized_input_data";
|
||||
@@ -54,7 +53,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
NEXT_RUN_ATTEMPT_TIME + " INTEGER, " +
|
||||
RUN_ATTEMPT + " INTEGER, " +
|
||||
MAX_ATTEMPTS + " INTEGER, " +
|
||||
MAX_BACKOFF + " INTEGER, " +
|
||||
LIFESPAN + " INTEGER, " +
|
||||
SERIALIZED_DATA + " TEXT, " +
|
||||
SERIALIZED_INPUT_DATA + " TEXT DEFAULT NULL, " +
|
||||
@@ -232,7 +230,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
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.LIFESPAN, job.getLifespan());
|
||||
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
@@ -308,7 +305,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
|
||||
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
|
||||
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
|
||||
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
|
||||
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
|
||||
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
|
||||
contentValues.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
|
||||
@@ -347,7 +343,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_INPUT_DATA)),
|
||||
@@ -399,7 +394,6 @@ public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
|
||||
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, CursorUtil.requireLong(cursor, "next_run_attempt_time"));
|
||||
values.put(Jobs.RUN_ATTEMPT, CursorUtil.requireInt(cursor, "run_attempt"));
|
||||
values.put(Jobs.MAX_ATTEMPTS, CursorUtil.requireInt(cursor, "max_attempts"));
|
||||
values.put(Jobs.MAX_BACKOFF, CursorUtil.requireLong(cursor, "max_backoff"));
|
||||
values.put(Jobs.LIFESPAN, CursorUtil.requireLong(cursor, "lifespan"));
|
||||
values.put(Jobs.SERIALIZED_DATA, CursorUtil.requireString(cursor, "serialized_data"));
|
||||
values.put(Jobs.SERIALIZED_INPUT_DATA, CursorUtil.requireString(cursor, "serialized_input_data"));
|
||||
|
||||
@@ -125,7 +125,7 @@ public class RecipientDatabase extends Database {
|
||||
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";
|
||||
static final String FORCE_SMS_SELECTION = "force_sms_selection";
|
||||
private static final String CAPABILITIES = "capabilities";
|
||||
private static final String STORAGE_SERVICE_ID = "storage_service_key";
|
||||
private static final String DIRTY = "dirty";
|
||||
|
||||
@@ -70,6 +70,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.Map;
|
||||
@@ -547,10 +548,10 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
public Cursor getRecentConversationList(int limit, boolean includeInactiveGroups, boolean hideV1Groups) {
|
||||
return getRecentConversationList(limit, includeInactiveGroups, false, hideV1Groups);
|
||||
return getRecentConversationList(limit, includeInactiveGroups, false, hideV1Groups, false);
|
||||
}
|
||||
|
||||
public Cursor getRecentConversationList(int limit, boolean includeInactiveGroups, boolean groupsOnly, boolean hideV1Groups) {
|
||||
public Cursor getRecentConversationList(int limit, boolean includeInactiveGroups, boolean groupsOnly, boolean hideV1Groups, boolean hideSms) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = !includeInactiveGroups ? MESSAGE_COUNT + " != 0 AND (" + GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " IS NULL OR " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1)"
|
||||
: MESSAGE_COUNT + " != 0";
|
||||
@@ -563,6 +564,11 @@ public class ThreadDatabase extends Database {
|
||||
query += " AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.GROUP_TYPE + " != " + RecipientDatabase.GroupType.SIGNAL_V1.getId();
|
||||
}
|
||||
|
||||
if (hideSms) {
|
||||
query += " AND (" + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.GROUP_ID + " NOT NULL OR " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.REGISTERED + " = " + RecipientDatabase.RegisteredState.REGISTERED.getId() + ")";
|
||||
query += " AND " + RecipientDatabase.TABLE_NAME + "." + RecipientDatabase.FORCE_SMS_SELECTION + " = 0";
|
||||
}
|
||||
|
||||
query += " AND " + ARCHIVED + " = 0";
|
||||
|
||||
return db.rawQuery(createQuery(query, 0, limit, true), null);
|
||||
@@ -792,13 +798,10 @@ public class ThreadDatabase extends Database {
|
||||
* @return Pinned recipients, in order from top to bottom.
|
||||
*/
|
||||
public @NonNull List<RecipientId> getPinnedRecipientIds() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{RECIPIENT_ID};
|
||||
String query = PINNED + " > ?";
|
||||
String[] args = SqlUtil.buildArgs(0);
|
||||
String[] projection = new String[]{ID, RECIPIENT_ID};
|
||||
List<RecipientId> pinned = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC")) {
|
||||
try (Cursor cursor = getPinned(projection)) {
|
||||
while (cursor.moveToNext()) {
|
||||
pinned.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
|
||||
}
|
||||
@@ -807,14 +810,64 @@ public class ThreadDatabase extends Database {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void pinConversations(@NonNull Set<Long> threadIds) {
|
||||
/**
|
||||
* @return Pinned thread ids, in order from top to bottom.
|
||||
*/
|
||||
public @NonNull List<Long> getPinnedThreadIds() {
|
||||
String[] projection = new String[]{ID};
|
||||
List<Long> pinned = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = getPinned(projection)) {
|
||||
while (cursor.moveToNext()) {
|
||||
pinned.add(CursorUtil.requireLong(cursor, ID));
|
||||
}
|
||||
}
|
||||
|
||||
return pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pinned recipients, in order from top to bottom.
|
||||
*/
|
||||
private @NonNull Cursor getPinned(String[] projection) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = PINNED + " > ?";
|
||||
String[] args = SqlUtil.buildArgs(0);
|
||||
|
||||
return db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC");
|
||||
}
|
||||
|
||||
public void restorePins(@NonNull Collection<Long> threadIds) {
|
||||
Log.d(TAG, "Restoring pinned threads " + StringUtil.join(threadIds, ","));
|
||||
pinConversations(threadIds, true);
|
||||
}
|
||||
|
||||
public void pinConversations(@NonNull Collection<Long> threadIds) {
|
||||
Log.d(TAG, "Pinning threads " + StringUtil.join(threadIds, ","));
|
||||
pinConversations(threadIds, false);
|
||||
}
|
||||
|
||||
private void pinConversations(@NonNull Collection<Long> threadIds, boolean clearFirst) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
threadIds = new LinkedHashSet<>(threadIds);
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
if (clearFirst) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PINNED, 0);
|
||||
String query = PINNED + " > ?";
|
||||
String[] args = SqlUtil.buildArgs(0);
|
||||
db.update(TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
int pinnedCount = getPinnedConversationListCount();
|
||||
|
||||
if (pinnedCount > 0 && clearFirst) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
for (long threadId : threadIds) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(PINNED, ++pinnedCount);
|
||||
@@ -1325,9 +1378,10 @@ public class ThreadDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable Extra getExtrasFor(MessageRecord record) {
|
||||
private @Nullable Extra getExtrasFor(@NonNull MessageRecord record) {
|
||||
boolean messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, record.getThreadId());
|
||||
RecipientId threadRecipientId = getRecipientIdForThreadId(record.getThreadId());
|
||||
RecipientId individualRecipient = record.getIndividualRecipient().getId();
|
||||
|
||||
if (!messageRequestAccepted && threadRecipientId != null) {
|
||||
Recipient resolved = Recipient.resolved(threadRecipientId);
|
||||
@@ -1338,35 +1392,42 @@ public class ThreadDatabase extends Database {
|
||||
RecipientId from = RecipientId.from(inviteAddState.getAddedOrInvitedBy(), null);
|
||||
if (inviteAddState.isInvited()) {
|
||||
Log.i(TAG, "GV2 invite message request from " + from);
|
||||
return Extra.forGroupV2invite(from);
|
||||
return Extra.forGroupV2invite(from, individualRecipient);
|
||||
} else {
|
||||
Log.i(TAG, "GV2 message request from " + from);
|
||||
return Extra.forGroupMessageRequest(from);
|
||||
return Extra.forGroupMessageRequest(from, individualRecipient);
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Falling back to unknown message request state for GV2 message");
|
||||
return Extra.forMessageRequest();
|
||||
return Extra.forMessageRequest(individualRecipient);
|
||||
} else {
|
||||
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
|
||||
|
||||
if (recipientId != null) {
|
||||
return Extra.forGroupMessageRequest(recipientId);
|
||||
return Extra.forGroupMessageRequest(recipientId, individualRecipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Extra.forMessageRequest();
|
||||
return Extra.forMessageRequest(individualRecipient);
|
||||
}
|
||||
|
||||
if (record.isRemoteDelete()) {
|
||||
return Extra.forRemoteDelete();
|
||||
return Extra.forRemoteDelete(individualRecipient);
|
||||
} else if (record.isViewOnce()) {
|
||||
return Extra.forViewOnce();
|
||||
return Extra.forViewOnce(individualRecipient);
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
|
||||
return Extra.forSticker(slide.getEmoji());
|
||||
return Extra.forSticker(slide.getEmoji(), individualRecipient);
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
|
||||
return Extra.forAlbum();
|
||||
return Extra.forAlbum(individualRecipient);
|
||||
}
|
||||
|
||||
if (threadRecipientId != null) {
|
||||
Recipient resolved = Recipient.resolved(threadRecipientId);
|
||||
if (resolved.isGroup()) {
|
||||
return Extra.forDefault(individualRecipient);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1537,6 +1598,7 @@ public class ThreadDatabase extends Database {
|
||||
@JsonProperty private final boolean isMessageRequestAccepted;
|
||||
@JsonProperty private final boolean isGv2Invite;
|
||||
@JsonProperty private final String groupAddedBy;
|
||||
@JsonProperty private final String individualRecipientId;
|
||||
|
||||
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
|
||||
@JsonProperty("isSticker") boolean isSticker,
|
||||
@@ -1545,7 +1607,8 @@ public class ThreadDatabase extends Database {
|
||||
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
|
||||
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
|
||||
@JsonProperty("isGv2Invite") boolean isGv2Invite,
|
||||
@JsonProperty("groupAddedBy") String groupAddedBy)
|
||||
@JsonProperty("groupAddedBy") String groupAddedBy,
|
||||
@JsonProperty("individualRecipientId") String individualRecipientId)
|
||||
{
|
||||
this.isRevealable = isRevealable;
|
||||
this.isSticker = isSticker;
|
||||
@@ -1555,34 +1618,39 @@ public class ThreadDatabase extends Database {
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
this.isGv2Invite = isGv2Invite;
|
||||
this.groupAddedBy = groupAddedBy;
|
||||
this.individualRecipientId = individualRecipientId;
|
||||
}
|
||||
|
||||
public static @NonNull Extra forViewOnce() {
|
||||
return new Extra(true, false, null, false, false, true, false, null);
|
||||
public static @NonNull Extra forViewOnce(@NonNull RecipientId individualRecipient) {
|
||||
return new Extra(true, false, null, false, false, true, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forSticker(@Nullable String emoji) {
|
||||
return new Extra(false, true, emoji, false, false, true, false, null);
|
||||
public static @NonNull Extra forSticker(@Nullable String emoji, @NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, true, emoji, false, false, true, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forAlbum() {
|
||||
return new Extra(false, false, null, true, false, true, false, null);
|
||||
public static @NonNull Extra forAlbum(@NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, true, false, true, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forRemoteDelete() {
|
||||
return new Extra(false, false, null, false, true, true, false, null);
|
||||
public static @NonNull Extra forRemoteDelete(@NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, false, true, true, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forMessageRequest() {
|
||||
return new Extra(false, false, null, false, false, false, false, null);
|
||||
public static @NonNull Extra forMessageRequest(@NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, false, false, false, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
|
||||
return new Extra(false, false, null, false, false, false, false, recipientId.serialize());
|
||||
public static @NonNull Extra forGroupMessageRequest(@NonNull RecipientId recipientId, @NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, false, false, false, false, recipientId.serialize(), individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forGroupV2invite(RecipientId recipientId) {
|
||||
return new Extra(false, false, null, false, false, false, true, recipientId.serialize());
|
||||
public static @NonNull Extra forGroupV2invite(@NonNull RecipientId recipientId, @NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, false, false, false, true, recipientId.serialize(), individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forDefault(@NonNull RecipientId individualRecipient) {
|
||||
return new Extra(false, false, null, false, false, true, false, null, individualRecipient.serialize());
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
@@ -1616,6 +1684,10 @@ public class ThreadDatabase extends Database {
|
||||
public @Nullable String getGroupAddedBy() {
|
||||
return groupAddedBy;
|
||||
}
|
||||
|
||||
public @Nullable String getIndividualRecipientId() {
|
||||
return individualRecipientId;
|
||||
}
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionState;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.File;
|
||||
@@ -63,7 +64,9 @@ class SessionStoreMigrationHelper {
|
||||
|
||||
if (versionMarker == SINGLE_STATE_VERSION) {
|
||||
Log.i(TAG, "Migrating single state version: " + sessionFile.getAbsolutePath());
|
||||
sessionRecord = SessionRecord.fromSingleSessionState(serialized);
|
||||
SessionState sessionState = new SessionState(serialized);
|
||||
|
||||
sessionRecord = new SessionRecord(sessionState);
|
||||
} else if (versionMarker >= ARCHIVE_STATES_VERSION) {
|
||||
Log.i(TAG, "Migrating session: " + sessionFile.getAbsolutePath());
|
||||
sessionRecord = new SessionRecord(serialized);
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class LiveUpdateMessage {
|
||||
* recreates the string asynchronously when they change.
|
||||
*/
|
||||
@AnyThread
|
||||
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) {
|
||||
public static LiveData<SpannableString> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) {
|
||||
if (updateDescription.isStringStatic()) {
|
||||
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint));
|
||||
}
|
||||
@@ -49,13 +49,13 @@ public final class LiveUpdateMessage {
|
||||
/**
|
||||
* Observes a single recipient and recreates the string asynchronously when they change.
|
||||
*/
|
||||
public static LiveData<Spannable> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, Spannable> createStringInBackground)
|
||||
public static LiveData<SpannableString> recipientToStringAsync(@NonNull RecipientId recipientId,
|
||||
@NonNull Function<Recipient, SpannableString> createStringInBackground)
|
||||
{
|
||||
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
|
||||
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveDataResolved(), createStringInBackground);
|
||||
}
|
||||
|
||||
private static @NonNull Spannable toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint) {
|
||||
private static @NonNull SpannableString toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string, @ColorInt int defaultTint) {
|
||||
boolean isDarkTheme = ThemeUtil.isDarkTheme(context);
|
||||
int drawableResource = updateDescription.getIconResource();
|
||||
int tint = isDarkTheme ? updateDescription.getDarkTint() : updateDescription.getLightTint();
|
||||
|
||||
@@ -199,18 +199,31 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
|
||||
public boolean isSelfCreatedGroup() {
|
||||
if (!isGroupUpdate() || !isGroupV2()) return false;
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = getDecryptedGroupV2Context();
|
||||
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
DecryptedGroupChange change = DecryptedGroupV2Context.parseFrom(decoded)
|
||||
.getChange();
|
||||
|
||||
return selfCreatedGroup(change);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
if (decryptedGroupV2Context == null) {
|
||||
return false;
|
||||
}
|
||||
DecryptedGroupChange change = decryptedGroupV2Context.getChange();
|
||||
|
||||
return selfCreatedGroup(change);
|
||||
}
|
||||
|
||||
private @Nullable DecryptedGroupV2Context getDecryptedGroupV2Context() {
|
||||
if (!isGroupUpdate() || !isGroupV2()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DecryptedGroupV2Context decryptedGroupV2Context;
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
decryptedGroupV2Context = null;
|
||||
}
|
||||
return decryptedGroupV2Context;
|
||||
}
|
||||
|
||||
private static boolean selfCreatedGroup(@NonNull DecryptedGroupChange change) {
|
||||
@@ -240,26 +253,25 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
|
||||
public @Nullable InviteAddState getGv2AddInviteState() {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
DecryptedGroup groupState = decryptedGroupV2Context.getGroupState();
|
||||
boolean invited = DecryptedGroupUtil.findPendingByUuid(groupState.getPendingMembersList(), Recipient.self().requireUuid()).isPresent();
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = getDecryptedGroupV2Context();
|
||||
|
||||
if (decryptedGroupV2Context.hasChange()) {
|
||||
UUID changeEditor = UuidUtil.fromByteStringOrNull(decryptedGroupV2Context.getChange().getEditor());
|
||||
|
||||
if (changeEditor != null) {
|
||||
return new InviteAddState(invited, changeEditor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "GV2 Message editor could not be determined");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
if (decryptedGroupV2Context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DecryptedGroup groupState = decryptedGroupV2Context.getGroupState();
|
||||
boolean invited = DecryptedGroupUtil.findPendingByUuid(groupState.getPendingMembersList(), Recipient.self().requireUuid()).isPresent();
|
||||
|
||||
if (decryptedGroupV2Context.hasChange()) {
|
||||
UUID changeEditor = UuidUtil.fromByteStringOrNull(decryptedGroupV2Context.getChange().getEditor());
|
||||
|
||||
if (changeEditor != null) {
|
||||
return new InviteAddState(invited, changeEditor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "GV2 Message editor could not be determined");
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull String getCallDateString(@NonNull Context context) {
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class ThreadRecord {
|
||||
private final long threadId;
|
||||
private final String body;
|
||||
private final Recipient recipient;
|
||||
private final Recipient sender;
|
||||
private final long type;
|
||||
private final long date;
|
||||
private final long deliveryStatus;
|
||||
@@ -61,6 +62,7 @@ public final class ThreadRecord {
|
||||
this.threadId = builder.threadId;
|
||||
this.body = builder.body;
|
||||
this.recipient = builder.recipient;
|
||||
this.sender = builder.sender;
|
||||
this.date = builder.date;
|
||||
this.type = builder.type;
|
||||
this.deliveryStatus = builder.deliveryStatus;
|
||||
@@ -184,6 +186,29 @@ public final class ThreadRecord {
|
||||
else return null;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getIndividualRecipientId() {
|
||||
if (extra != null && extra.getIndividualRecipientId() != null) {
|
||||
return RecipientId.from(extra.getIndividualRecipientId());
|
||||
} else {
|
||||
if (getRecipient().isGroup()) {
|
||||
return RecipientId.UNKNOWN;
|
||||
} else {
|
||||
return getRecipient().getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getGroupMessageSender() {
|
||||
RecipientId threadRecipientId = getRecipient().getId();
|
||||
RecipientId individualRecipientId = getIndividualRecipientId();
|
||||
|
||||
if (threadRecipientId.equals(individualRecipientId)) {
|
||||
return Recipient.self().getId();
|
||||
} else {
|
||||
return individualRecipientId;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isGv2Invite() {
|
||||
return extra != null && extra.isGv2Invite();
|
||||
}
|
||||
@@ -249,7 +274,8 @@ public final class ThreadRecord {
|
||||
public static class Builder {
|
||||
private long threadId;
|
||||
private String body;
|
||||
private Recipient recipient;
|
||||
private Recipient recipient = Recipient.UNKNOWN;
|
||||
private Recipient sender = Recipient.UNKNOWN;
|
||||
private long type;
|
||||
private long date;
|
||||
private long deliveryStatus;
|
||||
@@ -281,6 +307,11 @@ public final class ThreadRecord {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSender(@NonNull Recipient sender) {
|
||||
this.sender = sender;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setType(long type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
|
||||
@@ -20,7 +20,11 @@ public class ApngBufferCacheDecoder implements ResourceDecoder<ByteBuffer, APNGD
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
||||
return APNGParser.isAPNG(new ByteBufferReader(source));
|
||||
if (options.get(ApngOptions.ANIMATE)) {
|
||||
return APNGParser.isAPNG(new ByteBufferReader(source));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
20
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngOptions.java
vendored
Normal file
20
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngOptions.java
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import com.bumptech.glide.load.Option;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
|
||||
/**
|
||||
* Holds options that can be used to alter how APNGs are decoded in Glide.
|
||||
*/
|
||||
public final class ApngOptions {
|
||||
|
||||
private static final String KEY = "org.signal.skip_apng";
|
||||
|
||||
public static Option<Boolean> ANIMATE = Option.disk(KEY, true, (keyBytes, value, messageDigest) -> {
|
||||
messageDigest.update(keyBytes);
|
||||
messageDigest.update(Conversions.intToByteArray(value ? 1 : 0));
|
||||
});
|
||||
|
||||
private ApngOptions() {}
|
||||
}
|
||||
@@ -26,7 +26,11 @@ public class ApngStreamCacheDecoder implements ResourceDecoder<InputStream, APNG
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return APNGParser.isAPNG(new StreamReader(source));
|
||||
if (options.get(ApngOptions.ANIMATE)) {
|
||||
return APNGParser.isAPNG(new StreamReader(source));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -352,10 +352,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
||||
viewModel.getMentionSetting().observe(getViewLifecycleOwner(), value -> mentionsValue.setText(value));
|
||||
|
||||
viewModel.getCanLeaveGroup().observe(getViewLifecycleOwner(), canLeave -> leaveGroup.setVisibility(canLeave ? View.VISIBLE : View.GONE));
|
||||
viewModel.getCanBlockGroup().observe(getViewLifecycleOwner(), canBlock -> {
|
||||
blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE);
|
||||
unblockGroup.setVisibility(canBlock ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
viewModel.getCanBlockGroup().observe(getViewLifecycleOwner(), canBlock -> blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE));
|
||||
viewModel.getCanUnblockGroup().observe(getViewLifecycleOwner(), canUnblock -> unblockGroup.setVisibility(canUnblock ? View.VISIBLE : View.GONE));
|
||||
|
||||
viewModel.getGroupInfoMessage().observe(getViewLifecycleOwner(), message -> {
|
||||
switch (message) {
|
||||
|
||||
@@ -77,6 +77,7 @@ public class ManageGroupViewModel extends ViewModel {
|
||||
private final DefaultValueLiveData<CollapseState> memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
|
||||
private final LiveData<Boolean> canLeaveGroup;
|
||||
private final LiveData<Boolean> canBlockGroup;
|
||||
private final LiveData<Boolean> canUnblockGroup;
|
||||
private final LiveData<Boolean> showLegacyIndicator;
|
||||
private final LiveData<String> mentionSetting;
|
||||
private final LiveData<Boolean> groupLinkOn;
|
||||
@@ -119,7 +120,8 @@ public class ManageGroupViewModel extends ViewModel {
|
||||
this.hasCustomNotifications = Transformations.map(this.groupRecipient,
|
||||
recipient -> recipient.getNotificationChannel() != null || !NotificationChannels.supported());
|
||||
this.canLeaveGroup = liveGroup.isActive();
|
||||
this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> !recipient.isBlocked());
|
||||
this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> RecipientUtil.isBlockable(recipient) && !recipient.isBlocked());
|
||||
this.canUnblockGroup = Transformations.map(this.groupRecipient, Recipient::isBlocked);
|
||||
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
|
||||
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
|
||||
this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
|
||||
@@ -220,6 +222,10 @@ public class ManageGroupViewModel extends ViewModel {
|
||||
return canBlockGroup;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanUnblockGroup() {
|
||||
return canUnblockGroup;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanLeaveGroup() {
|
||||
return canLeaveGroup;
|
||||
}
|
||||
|
||||
@@ -150,18 +150,21 @@ public abstract class Job {
|
||||
|
||||
public static final class Result {
|
||||
|
||||
private static final Result SUCCESS_NO_DATA = new Result(ResultType.SUCCESS, null, null);
|
||||
private static final Result RETRY = new Result(ResultType.RETRY, null, null);
|
||||
private static final Result FAILURE = new Result(ResultType.FAILURE, null, null);
|
||||
private static final int INVALID_BACKOFF = -1;
|
||||
|
||||
private static final Result SUCCESS_NO_DATA = new Result(ResultType.SUCCESS, null, null, INVALID_BACKOFF);
|
||||
private static final Result FAILURE = new Result(ResultType.FAILURE, null, null, INVALID_BACKOFF);
|
||||
|
||||
private final ResultType resultType;
|
||||
private final RuntimeException runtimeException;
|
||||
private final Data outputData;
|
||||
private final long backoffInterval;
|
||||
|
||||
private Result(@NonNull ResultType resultType, @Nullable RuntimeException runtimeException, @Nullable Data outputData) {
|
||||
private Result(@NonNull ResultType resultType, @Nullable RuntimeException runtimeException, @Nullable Data outputData, long backoffInterval) {
|
||||
this.resultType = resultType;
|
||||
this.runtimeException = runtimeException;
|
||||
this.outputData = outputData;
|
||||
this.backoffInterval = backoffInterval;
|
||||
}
|
||||
|
||||
/** Job completed successfully. */
|
||||
@@ -171,12 +174,15 @@ public abstract class Job {
|
||||
|
||||
/** Job completed successfully and wants to provide some output data. */
|
||||
public static Result success(@Nullable Data outputData) {
|
||||
return new Result(ResultType.SUCCESS, null, outputData);
|
||||
return new Result(ResultType.SUCCESS, null, outputData, INVALID_BACKOFF);
|
||||
}
|
||||
|
||||
/** Job did not complete successfully, but it can be retried later. */
|
||||
public static Result retry() {
|
||||
return RETRY;
|
||||
/**
|
||||
* Job did not complete successfully, but it can be retried later.
|
||||
* @param backoffInterval How long to wait before retrying
|
||||
*/
|
||||
public static Result retry(long backoffInterval) {
|
||||
return new Result(ResultType.RETRY, null, null, backoffInterval);
|
||||
}
|
||||
|
||||
/** Job did not complete successfully and should not be tried again. Dependent jobs will also be failed.*/
|
||||
@@ -186,7 +192,7 @@ public abstract class Job {
|
||||
|
||||
/** Same as {@link #failure()}, except the app should also crash with the provided exception. */
|
||||
public static Result fatalFailure(@NonNull RuntimeException runtimeException) {
|
||||
return new Result(ResultType.FAILURE, runtimeException, null);
|
||||
return new Result(ResultType.FAILURE, runtimeException, null, INVALID_BACKOFF);
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
@@ -209,6 +215,10 @@ public abstract class Job {
|
||||
return outputData;
|
||||
}
|
||||
|
||||
long getBackoffInterval() {
|
||||
return backoffInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
switch (resultType) {
|
||||
@@ -241,7 +251,6 @@ public abstract class Job {
|
||||
private final long createTime;
|
||||
private final long lifespan;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final int maxInstancesForFactory;
|
||||
private final int maxInstancesForQueue;
|
||||
private final String queue;
|
||||
@@ -253,7 +262,6 @@ public abstract class Job {
|
||||
long createTime,
|
||||
long lifespan,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
int maxInstancesForFactory,
|
||||
int maxInstancesForQueue,
|
||||
@Nullable String queue,
|
||||
@@ -265,7 +273,6 @@ public abstract class Job {
|
||||
this.createTime = createTime;
|
||||
this.lifespan = lifespan;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.maxInstancesForFactory = maxInstancesForFactory;
|
||||
this.maxInstancesForQueue = maxInstancesForQueue;
|
||||
this.queue = queue;
|
||||
@@ -290,10 +297,6 @@ public abstract class Job {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
int getMaxInstancesForFactory() {
|
||||
return maxInstancesForFactory;
|
||||
}
|
||||
@@ -319,14 +322,13 @@ public abstract class Job {
|
||||
}
|
||||
|
||||
public Builder toBuilder() {
|
||||
return new Builder(id, createTime, maxBackoff, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
|
||||
return new Builder(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
|
||||
}
|
||||
|
||||
|
||||
public static final class Builder {
|
||||
private String id;
|
||||
private long createTime;
|
||||
private long maxBackoff;
|
||||
private long lifespan;
|
||||
private int maxAttempts;
|
||||
private int maxInstancesForFactory;
|
||||
@@ -341,12 +343,11 @@ public abstract class Job {
|
||||
}
|
||||
|
||||
Builder(@NonNull String id) {
|
||||
this(id, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(FeatureFlags.getDefaultMaxBackoffSeconds()), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false);
|
||||
this(id, System.currentTimeMillis(), IMMORTAL, 1, UNLIMITED, UNLIMITED, null, new LinkedList<>(), null, false);
|
||||
}
|
||||
|
||||
private Builder(@NonNull String id,
|
||||
long createTime,
|
||||
long maxBackoff,
|
||||
long lifespan,
|
||||
int maxAttempts,
|
||||
int maxInstancesForFactory,
|
||||
@@ -358,7 +359,6 @@ public abstract class Job {
|
||||
{
|
||||
this.id = id;
|
||||
this.createTime = createTime;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.lifespan = lifespan;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.maxInstancesForFactory = maxInstancesForFactory;
|
||||
@@ -391,15 +391,6 @@ public abstract class Job {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the longest amount of time to wait between retries. No guarantees that this will
|
||||
* be respected on API >= 26.
|
||||
*/
|
||||
public @NonNull Builder setMaxBackoff(long maxBackoff) {
|
||||
this.maxBackoff = maxBackoff;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of instances you'd want of this job at any given time, as
|
||||
* determined by the job's factory key. If enqueueing this job would put it over that limit,
|
||||
@@ -479,7 +470,7 @@ public abstract class Job {
|
||||
}
|
||||
|
||||
public @NonNull Parameters build() {
|
||||
return new Parameters(id, createTime, lifespan, maxAttempts, maxBackoff, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
|
||||
return new Parameters(id, createTime, lifespan, maxAttempts, maxInstancesForFactory, maxInstancesForQueue, queue, constraintKeys, inputData, memoryOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,13 @@ class JobController {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onRetry(@NonNull Job job) {
|
||||
synchronized void onRetry(@NonNull Job job, long backoffInterval) {
|
||||
if (backoffInterval <= 0) {
|
||||
throw new IllegalArgumentException("Invalid backoff interval! " + backoffInterval);
|
||||
}
|
||||
|
||||
int nextRunAttempt = job.getRunAttempt() + 1;
|
||||
long nextRunAttemptTime = calculateNextRunAttemptTime(System.currentTimeMillis(), nextRunAttempt, TimeUnit.SECONDS.toMillis(FeatureFlags.getDefaultMaxBackoffSeconds()));
|
||||
long nextRunAttemptTime = System.currentTimeMillis() + backoffInterval;
|
||||
String serializedData = dataSerializer.serialize(job.serialize());
|
||||
|
||||
jobStorage.updateJobAfterRetry(job.getId(), false, nextRunAttempt, nextRunAttemptTime, serializedData);
|
||||
@@ -355,7 +359,6 @@ class JobController {
|
||||
job.getNextRunAttemptTime(),
|
||||
job.getRunAttempt(),
|
||||
job.getParameters().getMaxAttempts(),
|
||||
job.getParameters().getMaxBackoff(),
|
||||
job.getParameters().getLifespan(),
|
||||
dataSerializer.serialize(job.serialize()),
|
||||
null,
|
||||
@@ -447,22 +450,10 @@ class JobController {
|
||||
.setMaxAttempts(jobSpec.getMaxAttempts())
|
||||
.setQueue(jobSpec.getQueueKey())
|
||||
.setConstraints(Stream.of(constraintSpecs).map(ConstraintSpec::getFactoryKey).toList())
|
||||
.setMaxBackoff(jobSpec.getMaxBackoff())
|
||||
.setInputData(jobSpec.getSerializedInputData() != null ? dataSerializer.deserialize(jobSpec.getSerializedInputData()) : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
private long calculateNextRunAttemptTime(long currentTime, int nextAttempt, long maxBackoff) {
|
||||
int boundedAttempt = Math.min(nextAttempt, 30);
|
||||
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
|
||||
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
|
||||
double jitter = 0.75 + (Math.random() * 0.5);
|
||||
|
||||
actualBackoff = (long) (actualBackoff * jitter);
|
||||
|
||||
return currentTime + actualBackoff;
|
||||
}
|
||||
|
||||
private @NonNull JobSpec mapToJobWithInputData(@NonNull JobSpec jobSpec, @NonNull Data inputData) {
|
||||
return new JobSpec(jobSpec.getId(),
|
||||
jobSpec.getFactoryKey(),
|
||||
@@ -471,7 +462,6 @@ class JobController {
|
||||
jobSpec.getNextRunAttemptTime(),
|
||||
jobSpec.getRunAttempt(),
|
||||
jobSpec.getMaxAttempts(),
|
||||
jobSpec.getMaxBackoff(),
|
||||
jobSpec.getLifespan(),
|
||||
jobSpec.getSerializedData(),
|
||||
dataSerializer.serialize(inputData),
|
||||
|
||||
@@ -69,7 +69,6 @@ public class JobMigrator {
|
||||
jobSpec.getNextRunAttemptTime(),
|
||||
jobSpec.getRunAttempt(),
|
||||
jobSpec.getMaxAttempts(),
|
||||
jobSpec.getMaxBackoff(),
|
||||
jobSpec.getLifespan(),
|
||||
dataSerializer.serialize(updatedJobData.getData()),
|
||||
jobSpec.getSerializedInputData(),
|
||||
|
||||
@@ -53,7 +53,7 @@ class JobRunner extends Thread {
|
||||
if (result.isSuccess()) {
|
||||
jobController.onSuccess(job, result.getOutputData());
|
||||
} else if (result.isRetry()) {
|
||||
jobController.onRetry(job);
|
||||
jobController.onRetry(job, result.getBackoffInterval());
|
||||
job.onRetry();
|
||||
} else if (result.isFailure()) {
|
||||
List<Job> dependents = jobController.onFailure(job);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
public final class BackoffUtil {
|
||||
|
||||
private BackoffUtil() {}
|
||||
|
||||
/**
|
||||
* Simple exponential backoff with random jitter.
|
||||
* @param pastAttemptCount The number of attempts that have already been made.
|
||||
*
|
||||
* @return The calculated backoff.
|
||||
*/
|
||||
public static long exponentialBackoff(int pastAttemptCount, long maxBackoff) {
|
||||
if (pastAttemptCount < 1) {
|
||||
throw new IllegalArgumentException("Bad attempt count! " + pastAttemptCount);
|
||||
}
|
||||
|
||||
int boundedAttempt = Math.min(pastAttemptCount, 30);
|
||||
long exponentialBackoff = (long) Math.pow(2, boundedAttempt) * 1000;
|
||||
long actualBackoff = Math.min(exponentialBackoff, maxBackoff);
|
||||
double jitter = 0.75 + (Math.random() * 0.5);
|
||||
|
||||
return (long) (actualBackoff * jitter);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ public final class JobSpec {
|
||||
private final long nextRunAttemptTime;
|
||||
private final int runAttempt;
|
||||
private final int maxAttempts;
|
||||
private final long maxBackoff;
|
||||
private final long lifespan;
|
||||
private final String serializedData;
|
||||
private final String serializedInputData;
|
||||
@@ -30,7 +29,6 @@ public final class JobSpec {
|
||||
long nextRunAttemptTime,
|
||||
int runAttempt,
|
||||
int maxAttempts,
|
||||
long maxBackoff,
|
||||
long lifespan,
|
||||
@NonNull String serializedData,
|
||||
@Nullable String serializedInputData,
|
||||
@@ -42,7 +40,6 @@ public final class JobSpec {
|
||||
this.queueKey = queueKey;
|
||||
this.createTime = createTime;
|
||||
this.nextRunAttemptTime = nextRunAttemptTime;
|
||||
this.maxBackoff = maxBackoff;
|
||||
this.runAttempt = runAttempt;
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.lifespan = lifespan;
|
||||
@@ -80,10 +77,6 @@ public final class JobSpec {
|
||||
return maxAttempts;
|
||||
}
|
||||
|
||||
public long getMaxBackoff() {
|
||||
return maxBackoff;
|
||||
}
|
||||
|
||||
public long getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
@@ -113,7 +106,6 @@ public final class JobSpec {
|
||||
nextRunAttemptTime == jobSpec.nextRunAttemptTime &&
|
||||
runAttempt == jobSpec.runAttempt &&
|
||||
maxAttempts == jobSpec.maxAttempts &&
|
||||
maxBackoff == jobSpec.maxBackoff &&
|
||||
lifespan == jobSpec.lifespan &&
|
||||
isRunning == jobSpec.isRunning &&
|
||||
memoryOnly == jobSpec.memoryOnly &&
|
||||
@@ -126,13 +118,13 @@ public final class JobSpec {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, serializedData, serializedInputData, isRunning, memoryOnly);
|
||||
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, lifespan, serializedData, serializedInputData, isRunning, memoryOnly);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | lifespan: %d | isRunning: %b | memoryOnly: %b",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, isRunning, memoryOnly);
|
||||
return String.format("id: JOB::%s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | lifespan: %d | isRunning: %b | memoryOnly: %b",
|
||||
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, lifespan, isRunning, memoryOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ final class WorkManagerDatabase extends SQLiteOpenHelper {
|
||||
0,
|
||||
0,
|
||||
Job.Parameters.UNLIMITED,
|
||||
TimeUnit.SECONDS.toMillis(30),
|
||||
TimeUnit.DAYS.toMillis(1),
|
||||
dataSerializer.serialize(DataMigrator.convert(data)),
|
||||
null,
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
public abstract class BaseJob extends Job {
|
||||
|
||||
@@ -35,7 +37,7 @@ public abstract class BaseJob extends Job {
|
||||
} catch (Exception e) {
|
||||
if (onShouldRetry(e)) {
|
||||
Log.i(TAG, JobLogger.format(this, "Encountered a retryable exception."), e);
|
||||
return Result.retry();
|
||||
return Result.retry(getNextRunAttemptBackoff(getRunAttempt() + 1, e));
|
||||
} else {
|
||||
Log.w(TAG, JobLogger.format(this, "Encountered a failing exception."), e);
|
||||
return Result.failure();
|
||||
@@ -47,6 +49,18 @@ public abstract class BaseJob extends Job {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return how long you'd like to wait until the next retry, given the attempt count and
|
||||
* exception that caused the retry. The attempt count is the number of attempts that have been
|
||||
* made already, so this value will be at least 1.
|
||||
*
|
||||
* There is a sane default implementation here that uses exponential backoff, but jobs can
|
||||
* override this behavior to define custom backoff behavior.
|
||||
*/
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
return BackoffUtil.exponentialBackoff(pastAttemptCount, FeatureFlags.getDefaultMaxBackoff());
|
||||
}
|
||||
|
||||
protected abstract void onRun() throws Exception;
|
||||
|
||||
protected abstract boolean onShouldRetry(@NonNull Exception e);
|
||||
|
||||
@@ -192,7 +192,6 @@ public class FastJobStorage implements JobStorage {
|
||||
existing.getNextRunAttemptTime(),
|
||||
existing.getRunAttempt(),
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
existing.getSerializedData(),
|
||||
existing.getSerializedInputData(),
|
||||
@@ -222,7 +221,6 @@ public class FastJobStorage implements JobStorage {
|
||||
nextRunAttemptTime,
|
||||
runAttempt,
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
serializedData,
|
||||
existing.getSerializedInputData(),
|
||||
@@ -248,7 +246,6 @@ public class FastJobStorage implements JobStorage {
|
||||
existing.getNextRunAttemptTime(),
|
||||
existing.getRunAttempt(),
|
||||
existing.getMaxAttempts(),
|
||||
existing.getMaxBackoff(),
|
||||
existing.getLifespan(),
|
||||
existing.getSerializedData(),
|
||||
existing.getSerializedInputData(),
|
||||
|
||||
@@ -91,6 +91,10 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
Recipient recipient = Recipient.resolved(threadRecipient);
|
||||
|
||||
if (!recipient.hasServiceIdentifier()) {
|
||||
Log.i(TAG, "Queued for recipient without service identifier");
|
||||
return;
|
||||
}
|
||||
|
||||
MessageRequestResponseMessage response;
|
||||
|
||||
|
||||
@@ -263,13 +263,6 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof IOException) return true;
|
||||
if (exception instanceof RetryLaterException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
|
||||
@@ -105,8 +105,7 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
|
||||
@Override
|
||||
public void onPushSend()
|
||||
throws RetryLaterException, MmsException, NoSuchMessageException,
|
||||
UndeliverableMessageException
|
||||
throws IOException, MmsException, NoSuchMessageException, UndeliverableMessageException
|
||||
{
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
@@ -175,12 +174,6 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof RetryLaterException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
@@ -188,8 +181,7 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
}
|
||||
|
||||
private boolean deliver(OutgoingMediaMessage message)
|
||||
throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,
|
||||
UndeliverableMessageException
|
||||
throws IOException, InsecureFallbackApprovalException, UntrustedIdentityException, UndeliverableMessageException
|
||||
{
|
||||
if (message.getRecipient() == null) {
|
||||
throw new UndeliverableMessageException("No destination address.");
|
||||
@@ -239,9 +231,6 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
throw new UndeliverableMessageException(e);
|
||||
} catch (ServerRejectedException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
} catch (IOException e) {
|
||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -38,9 +39,11 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -56,6 +59,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -113,6 +118,27 @@ public abstract class PushSendJob extends SendJob {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof ServerRejectedException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return exception instanceof IOException ||
|
||||
exception instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (exception instanceof NonSuccessfulResponseCodeException) {
|
||||
if (((NonSuccessfulResponseCodeException) exception).is5xx()) {
|
||||
return BackoffUtil.exponentialBackoff(pastAttemptCount, FeatureFlags.getServerErrorMaxBackoff());
|
||||
}
|
||||
}
|
||||
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
}
|
||||
|
||||
protected Optional<byte[]> getProfileKey(@NonNull Recipient recipient) {
|
||||
if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) {
|
||||
return Optional.absent();
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -29,10 +30,12 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PushTextSendJob extends PushSendJob {
|
||||
|
||||
@@ -69,7 +72,7 @@ public class PushTextSendJob extends PushSendJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend() throws NoSuchMessageException, RetryLaterException, UndeliverableMessageException {
|
||||
public void onPushSend() throws IOException, NoSuchMessageException, UndeliverableMessageException {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getSmsMessage(messageId);
|
||||
@@ -132,13 +135,6 @@ public class PushTextSendJob extends PushSendJob {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof RetryLaterException) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
@@ -152,7 +148,7 @@ public class PushTextSendJob extends PushSendJob {
|
||||
}
|
||||
|
||||
private boolean deliver(SmsMessageRecord message)
|
||||
throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, UndeliverableMessageException
|
||||
throws UntrustedIdentityException, InsecureFallbackApprovalException, UndeliverableMessageException, IOException
|
||||
{
|
||||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
@@ -187,9 +183,6 @@ public class PushTextSendJob extends PushSendJob {
|
||||
throw new InsecureFallbackApprovalException(e);
|
||||
} catch (ServerRejectedException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
} catch (IOException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
@@ -124,6 +125,9 @@ public class RefreshOwnProfileJob extends BaseJob {
|
||||
String plaintextAbout = ProfileUtil.decryptName(profileKey, encryptedAbout);
|
||||
String plaintextEmoji = ProfileUtil.decryptName(profileKey, encryptedEmoji);
|
||||
|
||||
Log.d(TAG, "Saving " + (!Util.isEmpty(plaintextAbout) ? "non-" : "") + "empty about.");
|
||||
Log.d(TAG, "Saving " + (!Util.isEmpty(plaintextEmoji) ? "non-" : "") + "empty emoji.");
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setAbout(Recipient.self().getId(), plaintextAbout, plaintextEmoji);
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.util.AppSignatureUtil;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
import org.thoughtcrime.securesms.util.CensorshipUtil;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -54,6 +55,7 @@ public class LogSectionSystemInfo implements LogSection {
|
||||
builder.append("ABIs : ").append(TextUtils.join(", ", getSupportedAbis())).append("\n");
|
||||
builder.append("Memory : ").append(getMemoryUsage()).append("\n");
|
||||
builder.append("Memclass : ").append(getMemoryClass(context)).append("\n");
|
||||
builder.append("MemInfo : ").append(getMemoryInfo(context)).append("\n");
|
||||
builder.append("OS Host : ").append(Build.HOST).append("\n");
|
||||
builder.append("Censored : ").append(CensorshipUtil.isCensored(context)).append("\n");
|
||||
builder.append("Play Services : ").append(getPlayServicesString(context)).append("\n");
|
||||
@@ -102,6 +104,12 @@ public class LogSectionSystemInfo implements LogSection {
|
||||
return activityManager.getMemoryClass() + lowMem;
|
||||
}
|
||||
|
||||
private static @NonNull String getMemoryInfo(Context context) {
|
||||
ActivityManager.MemoryInfo info = DeviceProperties.getMemoryInfo(context);
|
||||
return String.format(Locale.US, "availMem: %d mb, totalMem: %d mb, threshold: %d mb, lowMemory: %b",
|
||||
ByteUnit.BYTES.toMegabytes(info.availMem), ByteUnit.BYTES.toMegabytes(info.totalMem), ByteUnit.BYTES.toMegabytes(info.threshold), info.lowMemory);
|
||||
}
|
||||
|
||||
private static @NonNull Iterable<String> getSupportedAbis() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
return Arrays.asList(Build.SUPPORTED_ABIS);
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
/**
|
||||
* A base class for jobs that are intended to be used in {@link ApplicationMigrations}. Some
|
||||
@@ -45,7 +47,7 @@ abstract class MigrationJob extends Job {
|
||||
} catch (Exception e) {
|
||||
if (shouldRetry(e)) {
|
||||
Log.w(TAG, JobLogger.format(this, "Encountered a retryable exception."), e);
|
||||
return Result.retry();
|
||||
return Result.retry(BackoffUtil.exponentialBackoff(getRunAttempt(), FeatureFlags.getDefaultMaxBackoff()));
|
||||
} else {
|
||||
Log.w(TAG, JobLogger.format(this, "Encountered a non-runtime fatal exception."), e);
|
||||
throw new FailedMigrationError(e);
|
||||
|
||||
@@ -202,6 +202,7 @@ public class EditAboutFragment extends Fragment implements ManageProfileActivity
|
||||
private void onPresetSelected(@NonNull AboutPreset preset) {
|
||||
onEmojiSelectedInternal(preset.getEmoji());
|
||||
bodyView.setText(requireContext().getString(preset.getBodyRes()));
|
||||
bodyView.setSelection(bodyView.length(), bodyView.length());
|
||||
}
|
||||
|
||||
private final class PresetAdapter extends ListAdapter<AboutPreset, PresetViewHolder> {
|
||||
|
||||
@@ -11,10 +11,8 @@ import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileFragmentDirections;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -22,7 +20,7 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
/**
|
||||
* Activity that manages the local user's profile, as accessed via the settings.
|
||||
*/
|
||||
public class ManageProfileActivity extends BaseActivity implements ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
public class ManageProfileActivity extends PassphraseRequiredActivity implements ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
@@ -39,9 +37,7 @@ public class ManageProfileActivity extends BaseActivity implements ReactWithAnyE
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.manage_profile_activity);
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class LiveRecipient {
|
||||
private final Context context;
|
||||
private final MutableLiveData<Recipient> liveData;
|
||||
private final LiveData<Recipient> observableLiveData;
|
||||
private final LiveData<Recipient> observableLiveDataResolved;
|
||||
private final Set<RecipientForeverObserver> observers;
|
||||
private final Observer<Recipient> foreverObserver;
|
||||
private final AtomicReference<Recipient> recipient;
|
||||
@@ -41,9 +42,9 @@ public final class LiveRecipient {
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final MutableLiveData<Object> refreshForceNotify;
|
||||
|
||||
LiveRecipient(@NonNull Context context, @NonNull MutableLiveData<Recipient> liveData, @NonNull Recipient defaultRecipient) {
|
||||
LiveRecipient(@NonNull Context context, @NonNull Recipient defaultRecipient) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.liveData = liveData;
|
||||
this.liveData = new MutableLiveData<>(defaultRecipient);
|
||||
this.recipient = new AtomicReference<>(defaultRecipient);
|
||||
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
@@ -53,10 +54,11 @@ public final class LiveRecipient {
|
||||
o.onRecipientChanged(recipient);
|
||||
}
|
||||
};
|
||||
this.refreshForceNotify = new MutableLiveData<>(System.currentTimeMillis());
|
||||
this.refreshForceNotify = new MutableLiveData<>(new Object());
|
||||
this.observableLiveData = LiveDataUtil.combineLatest(LiveDataUtil.distinctUntilChanged(liveData, Recipient::hasSameContent),
|
||||
refreshForceNotify,
|
||||
(recipient, force) -> recipient);
|
||||
this.observableLiveDataResolved = LiveDataUtil.filter(this.observableLiveData, r -> !r.isResolving());
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
@@ -183,6 +185,10 @@ public final class LiveRecipient {
|
||||
return observableLiveData;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Recipient> getLiveDataResolved() {
|
||||
return observableLiveDataResolved;
|
||||
}
|
||||
|
||||
private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) {
|
||||
RecipientSettings settings = recipientDatabase.getRecipientSettings(id);
|
||||
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)
|
||||
|
||||
@@ -6,9 +6,6 @@ import android.content.Context;
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -22,7 +19,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -50,7 +46,7 @@ public final class LiveRecipientCache {
|
||||
this.context = context.getApplicationContext();
|
||||
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
this.recipients = new LRUCache<>(CACHE_MAX);
|
||||
this.unknown = new LiveRecipient(context, new MutableLiveData<>(), Recipient.UNKNOWN);
|
||||
this.unknown = new LiveRecipient(context, Recipient.UNKNOWN);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@@ -60,7 +56,7 @@ public final class LiveRecipientCache {
|
||||
LiveRecipient live = recipients.get(id);
|
||||
|
||||
if (live == null) {
|
||||
final LiveRecipient newLive = new LiveRecipient(context, new MutableLiveData<>(), new Recipient(id));
|
||||
final LiveRecipient newLive = new LiveRecipient(context, new Recipient(id));
|
||||
|
||||
recipients.put(id, newLive);
|
||||
|
||||
@@ -93,7 +89,7 @@ public final class LiveRecipientCache {
|
||||
boolean needsResolve = false;
|
||||
|
||||
if (live == null) {
|
||||
live = new LiveRecipient(context, new MutableLiveData<>(), recipient);
|
||||
live = new LiveRecipient(context, recipient);
|
||||
recipients.put(recipient.getId(), live);
|
||||
needsResolve = recipient.isResolving();
|
||||
} else if (live.get().isResolving() || !recipient.isResolving()) {
|
||||
|
||||
@@ -111,7 +111,7 @@ public class RecipientUtil {
|
||||
|
||||
public static boolean isBlockable(@NonNull Recipient recipient) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
return resolved.isPushGroup() || resolved.hasServiceIdentifier();
|
||||
return !resolved.isMmsGroup();
|
||||
}
|
||||
|
||||
public static List<Recipient> getEligibleForSending(@NonNull List<Recipient> recipients) {
|
||||
@@ -177,7 +177,10 @@ public class RecipientUtil {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId()));
|
||||
|
||||
if (recipient.hasServiceIdentifier()) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,6 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -261,10 +260,11 @@ public class ManageRecipientFragment extends LoggingFragment {
|
||||
});
|
||||
}
|
||||
|
||||
viewModel.getCanBlock().observe(getViewLifecycleOwner(), canBlock -> {
|
||||
block.setVisibility(canBlock ? View.VISIBLE : View.GONE);
|
||||
unblock.setVisibility(canBlock ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
viewModel.getCanBlock().observe(getViewLifecycleOwner(),
|
||||
canBlock -> block.setVisibility(canBlock ? View.VISIBLE : View.GONE));
|
||||
|
||||
viewModel.getCanUnblock().observe(getViewLifecycleOwner(),
|
||||
canUnblock -> unblock.setVisibility(canUnblock ? View.VISIBLE : View.GONE));
|
||||
|
||||
messageButton.setOnClickListener(v -> {
|
||||
if (fromConversation) {
|
||||
|
||||
@@ -63,6 +63,7 @@ public final class ManageRecipientViewModel extends ViewModel {
|
||||
private final LiveData<Boolean> canCollapseMemberList;
|
||||
private final DefaultValueLiveData<CollapseState> groupListCollapseState;
|
||||
private final LiveData<Boolean> canBlock;
|
||||
private final LiveData<Boolean> canUnblock;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> visibleSharedGroups;
|
||||
private final LiveData<String> sharedGroupsCountSummary;
|
||||
private final LiveData<Boolean> canAddToAGroup;
|
||||
@@ -79,7 +80,8 @@ public final class ManageRecipientViewModel extends ViewModel {
|
||||
this.disappearingMessageTimer = Transformations.map(this.recipient, r -> ExpirationUtil.getExpirationDisplayValue(context, r.getExpireMessages()));
|
||||
this.muteState = Transformations.map(this.recipient, r -> new MuteState(r.getMuteUntil(), r.isMuted()));
|
||||
this.hasCustomNotifications = Transformations.map(this.recipient, r -> r.getNotificationChannel() != null || !NotificationChannels.supported());
|
||||
this.canBlock = Transformations.map(this.recipient, r -> !r.isBlocked());
|
||||
this.canBlock = Transformations.map(this.recipient, r -> RecipientUtil.isBlockable(r) && !r.isBlocked());
|
||||
this.canUnblock = Transformations.map(this.recipient, Recipient::isBlocked);
|
||||
this.internalDetails = Transformations.map(this.recipient, this::populateInternalDetails);
|
||||
|
||||
manageRecipientRepository.getThreadId(this::onThreadIdLoaded);
|
||||
@@ -181,6 +183,10 @@ public final class ManageRecipientViewModel extends ViewModel {
|
||||
return canBlock;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanUnblock() {
|
||||
return canUnblock;
|
||||
}
|
||||
|
||||
void handleExpirationSelection(@NonNull Context context) {
|
||||
withRecipient(recipient ->
|
||||
ExpirationDialog.show(context,
|
||||
@@ -285,13 +291,13 @@ public final class ManageRecipientViewModel extends ViewModel {
|
||||
|
||||
String profileKeyBase64 = recipient.getProfileKey() != null ? Base64.encodeBytes(recipient.getProfileKey()) : "None";
|
||||
String profileKeyHex = recipient.getProfileKey() != null ? Hex.toStringCondensed(recipient.getProfileKey()) : "None";
|
||||
return String.format("-- Profile Name --\n%s\n\n" +
|
||||
return String.format("-- Profile Name --\n[%s] [%s]\n\n" +
|
||||
"-- Profile Sharing --\n%s\n\n" +
|
||||
"-- Profile Key (Base64) --\n%s\n\n" +
|
||||
"-- Profile Key (Hex) --\n%s\n\n" +
|
||||
"-- UUID --\n%s\n\n" +
|
||||
"-- RecipientId --\n%s",
|
||||
recipient.getProfileName().toString(),
|
||||
recipient.getProfileName().getGivenName(), recipient.getProfileName().getFamilyName(),
|
||||
recipient.isProfileSharing(),
|
||||
profileKeyBase64,
|
||||
profileKeyHex,
|
||||
|
||||
@@ -148,11 +148,13 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
int trimmedLength = pin.replace(" ", "").length();
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedLength < MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
enableAndFocusPinEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.IceCandidate;
|
||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||
|
||||
/**
|
||||
@@ -17,29 +16,28 @@ import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||
*/
|
||||
public class IceCandidateParcel implements Parcelable {
|
||||
|
||||
@NonNull private final IceCandidate iceCandidate;
|
||||
@NonNull private final byte[] iceCandidate;
|
||||
|
||||
public IceCandidateParcel(@NonNull IceCandidate iceCandidate) {
|
||||
public IceCandidateParcel(@NonNull byte[] iceCandidate) {
|
||||
this.iceCandidate = iceCandidate;
|
||||
}
|
||||
|
||||
public IceCandidateParcel(@NonNull IceUpdateMessage iceUpdateMessage) {
|
||||
this.iceCandidate = new IceCandidate(iceUpdateMessage.getOpaque(), iceUpdateMessage.getSdp());
|
||||
this.iceCandidate = iceUpdateMessage.getOpaque();
|
||||
}
|
||||
|
||||
private IceCandidateParcel(@NonNull Parcel in) {
|
||||
this.iceCandidate = new IceCandidate(in.createByteArray(),
|
||||
in.readString());
|
||||
this.iceCandidate = in.createByteArray();
|
||||
}
|
||||
|
||||
public @NonNull IceCandidate getIceCandidate() {
|
||||
public @NonNull byte[] getIceCandidate() {
|
||||
return iceCandidate;
|
||||
}
|
||||
|
||||
public @NonNull IceUpdateMessage getIceUpdateMessage(@NonNull CallId callId) {
|
||||
return new IceUpdateMessage(callId.longValue(),
|
||||
iceCandidate.getOpaque(),
|
||||
iceCandidate.getSdp());
|
||||
iceCandidate,
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,8 +47,7 @@ public class IceCandidateParcel implements Parcelable {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
dest.writeByteArray(iceCandidate.getOpaque());
|
||||
dest.writeString(iceCandidate.getSdp());
|
||||
dest.writeByteArray(iceCandidate);
|
||||
}
|
||||
|
||||
public static final Creator<IceCandidateParcel> CREATOR = new Creator<IceCandidateParcel>() {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Detects faces with the built in Android face detection.
|
||||
*/
|
||||
final class AndroidFaceDetector implements FaceDetector {
|
||||
|
||||
private static final String TAG = Log.tag(AndroidFaceDetector.class);
|
||||
|
||||
private static final int MAX_FACES = 20;
|
||||
|
||||
@Override
|
||||
public List<Face> detect(@NonNull Bitmap source) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Log.d(TAG, String.format(Locale.US, "Bitmap format is %dx%d %s", source.getWidth(), source.getHeight(), source.getConfig()));
|
||||
|
||||
boolean createBitmap = source.getConfig() != Bitmap.Config.RGB_565 || source.getWidth() % 2 != 0;
|
||||
Bitmap bitmap;
|
||||
|
||||
if (createBitmap) {
|
||||
Log.d(TAG, "Changing colour format to 565, with even width");
|
||||
bitmap = Bitmap.createBitmap(source.getWidth() & ~0x1, source.getHeight(), Bitmap.Config.RGB_565);
|
||||
new Canvas(bitmap).drawBitmap(source, 0, 0, null);
|
||||
} else {
|
||||
bitmap = source;
|
||||
}
|
||||
|
||||
try {
|
||||
android.media.FaceDetector faceDetector = new android.media.FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES);
|
||||
android.media.FaceDetector.Face[] faces = new android.media.FaceDetector.Face[MAX_FACES];
|
||||
int foundFaces = faceDetector.findFaces(bitmap, faces);
|
||||
|
||||
Log.d(TAG, String.format(Locale.US, "Found %d faces", foundFaces));
|
||||
|
||||
return Stream.of(faces)
|
||||
.limit(foundFaces)
|
||||
.map(AndroidFaceDetector::faceToFace)
|
||||
.toList();
|
||||
} finally {
|
||||
if (createBitmap) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
|
||||
Log.d(TAG, "Finished in " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
private static Face faceToFace(@NonNull android.media.FaceDetector.Face face) {
|
||||
PointF point = new PointF();
|
||||
face.getMidPoint(point);
|
||||
|
||||
float halfWidth = face.eyesDistance() * 1.4f;
|
||||
float yOffset = face.eyesDistance() * 0.4f;
|
||||
RectF bounds = new RectF(point.x - halfWidth, point.y - halfWidth + yOffset, point.x + halfWidth, point.y + halfWidth + yOffset);
|
||||
|
||||
return new DefaultFace(bounds, face.confidence());
|
||||
}
|
||||
|
||||
private static class DefaultFace implements Face {
|
||||
private final RectF bounds;
|
||||
private final float certainty;
|
||||
|
||||
public DefaultFace(@NonNull RectF bounds, float confidence) {
|
||||
this.bounds = bounds;
|
||||
this.certainty = confidence;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RectF getBounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends FaceDetector> getDetectorClass() {
|
||||
return AndroidFaceDetector.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getConfidence() {
|
||||
return certainty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,18 @@ package org.thoughtcrime.securesms.scribbles;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface FaceDetector {
|
||||
List<RectF> detect(Bitmap bitmap);
|
||||
List<Face> detect(@NonNull Bitmap bitmap);
|
||||
|
||||
interface Face {
|
||||
RectF getBounds();
|
||||
|
||||
Class<? extends FaceDetector> getDetectorClass();
|
||||
|
||||
float getConfidence();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.firebase.ml.vision.FirebaseVision;
|
||||
import com.google.firebase.ml.vision.common.FirebaseVisionImage;
|
||||
import com.google.firebase.ml.vision.face.FirebaseVisionFace;
|
||||
import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetector;
|
||||
import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class FirebaseFaceDetector implements FaceDetector {
|
||||
|
||||
private static final String TAG = Log.tag(FirebaseFaceDetector.class);
|
||||
|
||||
private static final long MAX_SIZE = 1000 * 1000;
|
||||
|
||||
@Override
|
||||
public List<RectF> detect(Bitmap source) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
int performanceMode = getPerformanceMode(source);
|
||||
Log.d(TAG, "Using performance mode " + performanceMode + " (API " + Build.VERSION.SDK_INT + ", " + source.getWidth() + "x" + source.getHeight() + ")");
|
||||
|
||||
FirebaseVisionFaceDetectorOptions options = new FirebaseVisionFaceDetectorOptions.Builder()
|
||||
.setPerformanceMode(performanceMode)
|
||||
.setMinFaceSize(0.05f)
|
||||
.setContourMode(FirebaseVisionFaceDetectorOptions.NO_CONTOURS)
|
||||
.setLandmarkMode(FirebaseVisionFaceDetectorOptions.NO_LANDMARKS)
|
||||
.setClassificationMode(FirebaseVisionFaceDetectorOptions.NO_CLASSIFICATIONS)
|
||||
.build();
|
||||
|
||||
FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(source);
|
||||
List<RectF> output = new ArrayList<>();
|
||||
|
||||
try (FirebaseVisionFaceDetector detector = FirebaseVision.getInstance().getVisionFaceDetector(options)) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
detector.detectInImage(image)
|
||||
.addOnSuccessListener(firebaseVisionFaces -> {
|
||||
output.addAll(Stream.of(firebaseVisionFaces)
|
||||
.map(FirebaseVisionFace::getBoundingBox)
|
||||
.map(r -> new RectF(r.left, r.top, r.right, r.bottom))
|
||||
.toList());
|
||||
latch.countDown();
|
||||
})
|
||||
.addOnFailureListener(e -> latch.countDown());
|
||||
|
||||
latch.await(15, TimeUnit.SECONDS);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to close!", e);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Finished in " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static int getPerformanceMode(Bitmap source) {
|
||||
if (Build.VERSION.SDK_INT < 28) {
|
||||
return FirebaseVisionFaceDetectorOptions.FAST;
|
||||
}
|
||||
|
||||
return source.getWidth() * source.getHeight() < MAX_SIZE ? FirebaseVisionFaceDetectorOptions.ACCURATE
|
||||
: FirebaseVisionFaceDetectorOptions.FAST;
|
||||
}
|
||||
}
|
||||
@@ -375,7 +375,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
if (mainImage.getRenderer() != null) {
|
||||
Bitmap bitmap = ((UriGlideRenderer) mainImage.getRenderer()).getBitmap();
|
||||
if (bitmap != null) {
|
||||
FaceDetector detector = new FirebaseFaceDetector();
|
||||
FaceDetector detector = new AndroidFaceDetector();
|
||||
|
||||
Point size = model.getOutputSizeMaxWidth(1000);
|
||||
Bitmap render = model.render(ApplicationDependencies.getApplication(), size);
|
||||
@@ -486,7 +486,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
|
||||
private void renderFaceBlurs(@NonNull FaceDetectionResult result) {
|
||||
List<RectF> faces = result.rects;
|
||||
List<FaceDetector.Face> faces = result.faces;
|
||||
|
||||
if (faces.isEmpty()) {
|
||||
cachedFaceDetection = null;
|
||||
@@ -497,12 +497,12 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
|
||||
Matrix faceMatrix = new Matrix();
|
||||
|
||||
for (RectF face : faces) {
|
||||
FaceBlurRenderer faceBlurRenderer = new FaceBlurRenderer();
|
||||
for (FaceDetector.Face face : faces) {
|
||||
Renderer faceBlurRenderer = new FaceBlurRenderer();
|
||||
EditorElement element = new EditorElement(faceBlurRenderer, EditorModel.Z_MASK);
|
||||
Matrix localMatrix = element.getLocalMatrix();
|
||||
|
||||
faceMatrix.setRectToRect(Bounds.FULL_BOUNDS, face, Matrix.ScaleToFit.FILL);
|
||||
faceMatrix.setRectToRect(Bounds.FULL_BOUNDS, face.getBounds(), Matrix.ScaleToFit.FILL);
|
||||
|
||||
localMatrix.set(result.position);
|
||||
localMatrix.preConcat(faceMatrix);
|
||||
@@ -574,11 +574,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
|
||||
private static class FaceDetectionResult {
|
||||
private final List<RectF> rects;
|
||||
private final Matrix position;
|
||||
private final List<FaceDetector.Face> faces;
|
||||
private final Matrix position;
|
||||
|
||||
private FaceDetectionResult(@NonNull List<RectF> rects, @NonNull Point imageSize, @NonNull Matrix position) {
|
||||
this.rects = rects;
|
||||
private FaceDetectionResult(@NonNull List<FaceDetector.Face> faces, @NonNull Point imageSize, @NonNull Matrix position) {
|
||||
this.faces = faces;
|
||||
this.position = new Matrix(position);
|
||||
|
||||
Matrix imageProjectionMatrix = new Matrix();
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.CallManager.CallEvent;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.HttpHeader;
|
||||
import org.signal.ringrtc.IceCandidate;
|
||||
import org.signal.ringrtc.Remote;
|
||||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
@@ -930,7 +929,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendOffer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @Nullable byte[] opaque, @Nullable String sdp, @NonNull CallManager.CallMediaType callMediaType) {
|
||||
public void onSendOffer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull byte[] opaque, @NonNull CallManager.CallMediaType callMediaType) {
|
||||
Log.i(TAG, "onSendOffer: id: " + callId.format(remoteDevice) + " type: " + callMediaType.name());
|
||||
|
||||
if (remote instanceof RemotePeer) {
|
||||
@@ -944,7 +943,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_OFFER_OPAQUE, opaque)
|
||||
.putExtra(EXTRA_OFFER_SDP, sdp)
|
||||
.putExtra(EXTRA_OFFER_TYPE, offerType);
|
||||
|
||||
startService(intent);
|
||||
@@ -954,7 +952,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendAnswer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @Nullable byte[] opaque, @Nullable String sdp) {
|
||||
public void onSendAnswer(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull byte[] opaque) {
|
||||
Log.i(TAG, "onSendAnswer: id: " + callId.format(remoteDevice));
|
||||
|
||||
if (remote instanceof RemotePeer) {
|
||||
@@ -966,8 +964,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
|
||||
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
|
||||
.putExtra(EXTRA_BROADCAST, broadcast)
|
||||
.putExtra(EXTRA_ANSWER_OPAQUE, opaque)
|
||||
.putExtra(EXTRA_ANSWER_SDP, sdp);
|
||||
.putExtra(EXTRA_ANSWER_OPAQUE, opaque);
|
||||
|
||||
startService(intent);
|
||||
} else {
|
||||
@@ -976,7 +973,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendIceCandidates(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull List<IceCandidate> iceCandidates) {
|
||||
public void onSendIceCandidates(@NonNull CallId callId, @Nullable Remote remote, @NonNull Integer remoteDevice, @NonNull Boolean broadcast, @NonNull List<byte[]> iceCandidates) {
|
||||
Log.i(TAG, "onSendIceCandidates: id: " + callId.format(remoteDevice));
|
||||
|
||||
if (remote instanceof RemotePeer) {
|
||||
@@ -984,7 +981,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
|
||||
ArrayList<IceCandidateParcel> iceCandidateParcels = new ArrayList<>(iceCandidates.size());
|
||||
for (IceCandidate iceCandidate : iceCandidates) {
|
||||
for (byte[] iceCandidate : iceCandidates) {
|
||||
iceCandidateParcels.add(new IceCandidateParcel(iceCandidate));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.IceCandidate;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@@ -96,7 +95,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
{
|
||||
Log.i(tag, "handleReceivedIceCandidates(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()) + ", count: " + iceCandidateParcels.size());
|
||||
|
||||
LinkedList<IceCandidate> iceCandidates = new LinkedList<>();
|
||||
LinkedList<byte[]> iceCandidates = new LinkedList<>();
|
||||
for (IceCandidateParcel parcel : iceCandidateParcels) {
|
||||
iceCandidates.add(parcel.getIceCandidate());
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
callManager.setCommunicationMode();
|
||||
callManager.setAudioEnable(currentState.getLocalDeviceState().isMicrophoneEnabled());
|
||||
callManager.setVideoEnable(currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
callManager.setLowBandwidthMode(NetworkUtil.useLowBandwidthCalling(context));
|
||||
callManager.updateBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "Enabling audio/video failed: ", e);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
@@ -75,7 +76,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
|
||||
try {
|
||||
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
|
||||
groupCall.setBandwidthMode(NetworkUtil.useLowBandwidthCalling(context) ? GroupCall.BandwidthMode.LOW : GroupCall.BandwidthMode.NORMAL);
|
||||
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
|
||||
} catch (CallException e) {
|
||||
Log.e(tag, e);
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.PeekInfo;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
@@ -52,7 +53,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
||||
try {
|
||||
groupCall.setOutgoingAudioMuted(true);
|
||||
groupCall.setOutgoingVideoMuted(true);
|
||||
groupCall.setBandwidthMode(NetworkUtil.useLowBandwidthCalling(context) ? GroupCall.BandwidthMode.LOW : GroupCall.BandwidthMode.NORMAL);
|
||||
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
|
||||
|
||||
Log.i(TAG, "Connecting to group call: " + currentState.getCallInfoState().getCallRecipient().getId());
|
||||
groupCall.connect();
|
||||
@@ -151,7 +152,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
||||
groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera());
|
||||
groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled());
|
||||
groupCall.setBandwidthMode(NetworkUtil.useLowBandwidthCalling(context) ? GroupCall.BandwidthMode.LOW : GroupCall.BandwidthMode.NORMAL);
|
||||
groupCall.setBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
|
||||
|
||||
groupCall.join();
|
||||
} catch (CallException e) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
@@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
||||
import org.webrtc.PeerConnection;
|
||||
@@ -91,6 +93,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
videoState.requireCamera(),
|
||||
iceServers,
|
||||
hideIp,
|
||||
NetworkUtil.getCallingBandwidthMode(context),
|
||||
false);
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "Unable to proceed with call: ", e);
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
@@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
|
||||
import org.webrtc.PeerConnection;
|
||||
@@ -117,6 +119,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
videoState.requireCamera(),
|
||||
iceServers,
|
||||
isAlwaysTurn,
|
||||
NetworkUtil.getCallingBandwidthMode(context),
|
||||
currentState.getCallSetupState().isEnableVideoOnCreate());
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "Unable to proceed with call: ", e);
|
||||
@@ -147,11 +150,15 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
{
|
||||
Log.i(TAG, "handleReceivedAnswer(): id: " + callMetadata.getCallId().format(callMetadata.getRemoteDevice()));
|
||||
|
||||
if (answerMetadata.getOpaque() == null) {
|
||||
return callFailure(currentState, "receivedAnswer() failed: answerMetadata did not contain opaque", null);
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] remoteIdentityKey = WebRtcUtil.getPublicKeyBytes(receivedAnswerMetadata.getRemoteIdentityKey());
|
||||
byte[] localIdentityKey = WebRtcUtil.getPublicKeyBytes(IdentityKeyUtil.getIdentityKey(context).serialize());
|
||||
|
||||
webRtcInteractor.getCallManager().receivedAnswer(callMetadata.getCallId(), callMetadata.getRemoteDevice(), answerMetadata.getOpaque(), answerMetadata.getSdp(), receivedAnswerMetadata.isMultiRing(), remoteIdentityKey, localIdentityKey);
|
||||
webRtcInteractor.getCallManager().receivedAnswer(callMetadata.getCallId(), callMetadata.getRemoteDevice(), answerMetadata.getOpaque(), receivedAnswerMetadata.isMultiRing(), remoteIdentityKey, localIdentityKey);
|
||||
} catch (CallException | InvalidKeyException e) {
|
||||
return callFailure(currentState, "receivedAnswer() failed: ", e);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
@@ -342,6 +343,13 @@ public abstract class WebRtcActionProcessor {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
if (offerMetadata.getOpaque() == null) {
|
||||
Log.w(tag, "Opaque data is required.");
|
||||
currentState = currentState.getActionProcessor().handleSendHangup(currentState, callMetadata, WebRtcData.HangupMetadata.fromType(HangupMessage.Type.NORMAL), true);
|
||||
webRtcInteractor.insertMissedCall(callMetadata.getRemotePeer(), true, receivedOfferMetadata.getServerReceivedTimestamp(), offerMetadata.getOfferType() == OfferMessage.Type.VIDEO_CALL);
|
||||
return currentState;
|
||||
}
|
||||
|
||||
Log.i(tag, "add remotePeer callId: " + callMetadata.getRemotePeer().getCallId() + " key: " + callMetadata.getRemotePeer().hashCode());
|
||||
|
||||
callMetadata.getRemotePeer().setCallStartTimestamp(receivedOfferMetadata.getServerReceivedTimestamp());
|
||||
@@ -365,7 +373,6 @@ public abstract class WebRtcActionProcessor {
|
||||
callMetadata.getRemotePeer(),
|
||||
callMetadata.getRemoteDevice(),
|
||||
offerMetadata.getOpaque(),
|
||||
offerMetadata.getSdp(),
|
||||
messageAgeSec,
|
||||
WebRtcUtil.getCallMediaTypeFromOfferType(offerMetadata.getOfferType()),
|
||||
1,
|
||||
@@ -611,7 +618,7 @@ public abstract class WebRtcActionProcessor {
|
||||
|
||||
protected @NonNull WebRtcServiceState handleBandwidthModeUpdate(@NonNull WebRtcServiceState currentState) {
|
||||
try {
|
||||
webRtcInteractor.getCallManager().setLowBandwidthMode(NetworkUtil.useLowBandwidthCalling(context));
|
||||
webRtcInteractor.getCallManager().updateBandwidthMode(NetworkUtil.getCallingBandwidthMode(context));
|
||||
} catch (CallException e) {
|
||||
Log.i(tag, "handleBandwidthModeUpdate: could not update bandwidth mode.");
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ public final class MultiShareSender {
|
||||
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
if (multiShareArgs.getStickerLocator() != null) {
|
||||
slideDeck.addSlide(buildStickerSlide(context, multiShareArgs.getStickerLocator()));
|
||||
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
|
||||
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
||||
for (Media media : multiShareArgs.getMedia()) {
|
||||
slideDeck.addSlide(SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight()));
|
||||
@@ -186,13 +186,6 @@ public final class MultiShareSender {
|
||||
return slideDeck;
|
||||
}
|
||||
|
||||
private static @NonNull StickerSlide buildStickerSlide(@NonNull Context context, @NonNull StickerLocator stickerLocator) {
|
||||
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false);
|
||||
|
||||
return new StickerSlide(context, stickerRecord.getUri(), stickerRecord.getSize(), stickerLocator, stickerRecord.getContentType());
|
||||
}
|
||||
|
||||
public static final class MultiShareSendResultCollection {
|
||||
private final List<MultiShareSendResult> results;
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
private ImageView searchAction;
|
||||
private View shareConfirm;
|
||||
private ShareSelectionAdapter adapter;
|
||||
private boolean disallowMultiShare;
|
||||
|
||||
private ShareViewModel viewModel;
|
||||
|
||||
@@ -173,7 +174,12 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return viewModel.onContactSelected(new ShareContact(recipientId, number));
|
||||
if (disallowMultiShare) {
|
||||
Toast.makeText(this, R.string.ShareActivity__sharing_to_multiple_chats_is, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
} else {
|
||||
return viewModel.onContactSelected(new ShareContact(recipientId, number));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +209,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
private void initializeIntent() {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF | DisplayMode.FLAG_HIDE_NEW;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this) && viewModel.isExternalShare()) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
@@ -269,6 +275,42 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
animateInSelection();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getSmsShareRestriction().observe(this, smsShareRestriction -> {
|
||||
final int displayMode;
|
||||
|
||||
switch (smsShareRestriction) {
|
||||
case NO_RESTRICTIONS:
|
||||
disallowMultiShare = false;
|
||||
displayMode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (displayMode == -1) {
|
||||
Log.w(TAG, "DisplayMode not set yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this) && viewModel.isExternalShare() && (displayMode & DisplayMode.FLAG_SMS) == 0) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode | DisplayMode.FLAG_SMS);
|
||||
contactsFragment.setQueryFilter(null);
|
||||
}
|
||||
break;
|
||||
case DISALLOW_SMS_CONTACTS:
|
||||
disallowMultiShare = false;
|
||||
displayMode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (displayMode == -1) {
|
||||
Log.w(TAG, "DisplayMode not set yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode & ~DisplayMode.FLAG_SMS);
|
||||
contactsFragment.setQueryFilter(null);
|
||||
break;
|
||||
case DISALLOW_MULTI_SHARE:
|
||||
disallowMultiShare = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
@@ -16,8 +17,10 @@ import com.annimon.stream.Stream;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -33,15 +36,17 @@ public class ShareViewModel extends ViewModel {
|
||||
private final ShareRepository shareRepository;
|
||||
private final MutableLiveData<Optional<ShareData>> shareData;
|
||||
private final MutableLiveData<Set<ShareContact>> selectedContacts;
|
||||
private final LiveData<SmsShareRestriction> smsShareRestriction;
|
||||
|
||||
private boolean mediaUsed;
|
||||
private boolean externalShare;
|
||||
|
||||
private ShareViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.selectedContacts = new DefaultValueLiveData<>(Collections.emptySet());
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.selectedContacts = new DefaultValueLiveData<>(Collections.emptySet());
|
||||
this.smsShareRestriction = Transformations.map(selectedContacts, this::updateShareRestriction);
|
||||
}
|
||||
|
||||
void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
|
||||
@@ -90,6 +95,10 @@ public class ShareViewModel extends ViewModel {
|
||||
.toList());
|
||||
}
|
||||
|
||||
@NonNull LiveData<SmsShareRestriction> getSmsShareRestriction() {
|
||||
return Transformations.distinctUntilChanged(smsShareRestriction);
|
||||
}
|
||||
|
||||
void onNonExternalShare() {
|
||||
externalShare = false;
|
||||
}
|
||||
@@ -116,6 +125,23 @@ public class ShareViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull SmsShareRestriction updateShareRestriction(@NonNull Set<ShareContact> shareContacts) {
|
||||
if (shareContacts.isEmpty()) {
|
||||
return SmsShareRestriction.NO_RESTRICTIONS;
|
||||
} else if (shareContacts.size() == 1) {
|
||||
ShareContact shareContact = shareContacts.iterator().next();
|
||||
Recipient recipient = Recipient.live(shareContact.getRecipientId().get()).get();
|
||||
|
||||
if (!recipient.isRegistered() || recipient.isForceSmsSelection()) {
|
||||
return SmsShareRestriction.DISALLOW_MULTI_SHARE;
|
||||
} else {
|
||||
return SmsShareRestriction.DISALLOW_SMS_CONTACTS;
|
||||
}
|
||||
} else {
|
||||
return SmsShareRestriction.DISALLOW_SMS_CONTACTS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
@@ -123,4 +149,10 @@ public class ShareViewModel extends ViewModel {
|
||||
return modelClass.cast(new ShareViewModel());
|
||||
}
|
||||
}
|
||||
|
||||
enum SmsShareRestriction {
|
||||
NO_RESTRICTIONS,
|
||||
DISALLOW_SMS_CONTACTS,
|
||||
DISALLOW_MULTI_SHARE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.components.SelectionAwareEmojiEditText;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareDialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
@@ -80,6 +82,16 @@ public class ShareInterstitialActivity extends PassphraseRequiredActivity {
|
||||
LinkPreviewViewModel.Factory linkPreviewViewModelFactory = new LinkPreviewViewModel.Factory(linkPreviewRepository);
|
||||
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, linkPreviewViewModelFactory).get(LinkPreviewViewModel.class);
|
||||
|
||||
boolean hasSms = Stream.of(args.getShareContactAndThreads())
|
||||
.anyMatch(c -> {
|
||||
Recipient recipient = Recipient.resolved(c.getRecipientId());
|
||||
return !recipient.isRegistered() || recipient.isForceSmsSelection();
|
||||
});
|
||||
|
||||
if (hasSms) {
|
||||
linkPreviewViewModel.onTransportChanged(hasSms);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull MultiShareArgs args) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -29,13 +30,15 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerRecord> stickers;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private int stickerSize;
|
||||
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.stickers = new ArrayList<>();
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
this.stickers = new ArrayList<>();
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
@@ -52,7 +55,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerKeyboardPageViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize);
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i), stickerSize, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,7 +96,8 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
||||
public void bind(@NonNull GlideRequests glideRequests,
|
||||
@Nullable EventListener eventListener,
|
||||
@NonNull StickerRecord sticker,
|
||||
@Px int size)
|
||||
@Px int size,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
currentSticker = sticker;
|
||||
|
||||
@@ -102,6 +106,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
||||
itemView.requestLayout();
|
||||
|
||||
glideRequests.load(new DecryptableUri(sticker.getUri()))
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(image);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,7 @@ public final class StickerKeyboardPageFragment extends Fragment implements Stick
|
||||
GlideRequests glideRequests = GlideApp.with(this);
|
||||
|
||||
this.list = view.findViewById(R.id.sticker_keyboard_list);
|
||||
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this);
|
||||
this.adapter = new StickerKeyboardPageAdapter(glideRequests, this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()));
|
||||
this.layoutManager = new GridLayoutManager(requireContext(), 2);
|
||||
this.listTouchListener = new StickerRolloverTouchListener(requireContext(), glideRequests, eventListener, this);
|
||||
this.packId = getArguments().getString(KEY_PACK_ID);
|
||||
|
||||
@@ -19,10 +19,12 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardPageFragment.EventListener;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardRepository.PackListResult;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -147,7 +149,7 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
||||
startingIndex = !result.hasRecents() && result.getPacks().size() > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks()), null, this, null, startingIndex);
|
||||
presenter.present(this, pagerAdapter, new IconProvider(context, result.getPacks(), DeviceProperties.shouldAllowApngStickerAnimation(context)), null, this, null, startingIndex);
|
||||
|
||||
if (isSoloProvider && result.getPacks().isEmpty()) {
|
||||
context.startActivity(StickerManagementActivity.getIntent(context));
|
||||
@@ -238,10 +240,12 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
||||
|
||||
private final Context context;
|
||||
private final List<StickerPackRecord> packs;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs) {
|
||||
this.context = context;
|
||||
this.packs = packs;
|
||||
private IconProvider(@NonNull Context context, List<StickerPackRecord> packs, boolean allowApngAnimation) {
|
||||
this.context = context;
|
||||
this.packs = packs;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -253,6 +257,7 @@ public final class StickerKeyboardProvider implements MediaKeyboardProvider,
|
||||
Uri uri = packs.get(index - 1).getCover().getUri();
|
||||
|
||||
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
/**
|
||||
@@ -93,7 +94,7 @@ public final class StickerManagementActivity extends PassphraseRequiredActivity
|
||||
|
||||
private void initView() {
|
||||
this.list = findViewById(R.id.sticker_management_list);
|
||||
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this);
|
||||
this.adapter = new StickerManagementAdapter(GlideApp.with(this), this, DeviceProperties.shouldAllowApngStickerAnimation(this));
|
||||
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(adapter);
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.adapter.SectionedRecyclerViewAdapter;
|
||||
@@ -38,6 +39,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
private final List<StickerSection> sections = new ArrayList<StickerSection>(3) {{
|
||||
StickerSection yourStickers = new StickerSection(TAG_YOUR_STICKERS,
|
||||
@@ -55,9 +57,10 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
add(messageStickers);
|
||||
}};
|
||||
|
||||
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
StickerManagementAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +85,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull StickerSection section, int localPosition) {
|
||||
section.bindViewHolder(viewHolder, localPosition, glideRequests, eventListener);
|
||||
section.bindViewHolder(viewHolder, localPosition, glideRequests, eventListener, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,14 +201,15 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
int localPosition,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener)
|
||||
@NonNull EventListener eventListener,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
if (localPosition == 0) {
|
||||
((HeaderViewHolder) viewHolder).bind(titleResId);
|
||||
} else if (records.isEmpty()) {
|
||||
((EmptyViewHolder) viewHolder).bind(emptyResId);
|
||||
} else {
|
||||
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size());
|
||||
((StickerViewHolder) viewHolder).bind(glideRequests, eventListener, records.get(localPosition - 1), localPosition == records.size(), allowApngAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +258,8 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull StickerPackRecord stickerPack,
|
||||
boolean lastInList)
|
||||
boolean lastInList,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
title.setText(stickerPack.getTitle().or(itemView.getResources().getString(R.string.StickerManagementAdapter_untitled)));
|
||||
author.setText(stickerPack.getAuthor().or(itemView.getResources().getString(R.string.StickerManagementAdapter_unknown)));
|
||||
@@ -268,6 +273,7 @@ final class StickerManagementAdapter extends SectionedRecyclerViewAdapter<String
|
||||
|
||||
glideRequests.load(new DecryptableUri(stickerPack.getCover().getUri()))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(cover);
|
||||
|
||||
if (stickerPack.isInstalled()) {
|
||||
|
||||
@@ -21,10 +21,12 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManifest.Sticker;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -140,7 +142,7 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity
|
||||
this.shareButton = findViewById(R.id.sticker_install_share_button);
|
||||
this.shareButtonImage = findViewById(R.id.sticker_install_share_button_image);
|
||||
|
||||
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this), this);
|
||||
this.adapter = new StickerPackPreviewAdapter(GlideApp.with(this), this, DeviceProperties.shouldAllowApngStickerAnimation(this));
|
||||
this.layoutManager = new GridLayoutManager(this, 2);
|
||||
this.touchListener = new StickerRolloverTouchListener(this, GlideApp.with(this), this, this);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
@@ -192,6 +194,7 @@ public final class StickerPackPreviewActivity extends PassphraseRequiredActivity
|
||||
: new StickerRemoteUri(cover.getPackId(), cover.getPackKey(), cover.getId());
|
||||
GlideApp.with(this).load(model)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, DeviceProperties.shouldAllowApngStickerAnimation(this))
|
||||
.into(coverImage);
|
||||
} else {
|
||||
coverImage.setImageDrawable(null);
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.glide.cache.ApngOptions;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -23,11 +24,13 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerManifest.Sticker> list;
|
||||
private final boolean allowApngAnimation;
|
||||
|
||||
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.list = new ArrayList<>();
|
||||
public StickerPackPreviewAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
this.list = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,7 +40,7 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerViewHolder stickerViewHolder, int i) {
|
||||
stickerViewHolder.bind(glideRequests, list.get(i), eventListener);
|
||||
stickerViewHolder.bind(glideRequests, list.get(i), eventListener, allowApngAnimation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,12 +71,17 @@ public final class StickerPackPreviewAdapter extends RecyclerView.Adapter<Sticke
|
||||
this.image = itemView.findViewById(R.id.sticker_install_item_image);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests, @NonNull StickerManifest.Sticker sticker, @NonNull EventListener eventListener) {
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
@NonNull StickerManifest.Sticker sticker,
|
||||
@NonNull EventListener eventListener,
|
||||
boolean allowApngAnimation)
|
||||
{
|
||||
currentEmoji = sticker.getEmoji();
|
||||
currentGlideModel = sticker.getUri().isPresent() ? new DecryptableStreamUriLoader.DecryptableUri(sticker.getUri().get())
|
||||
: new StickerRemoteUri(sticker.getPackId(), sticker.getPackKey(), sticker.getId());
|
||||
glideRequests.load(currentGlideModel)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.set(ApngOptions.ANIMATE, allowApngAnimation)
|
||||
.into(image);
|
||||
|
||||
image.setOnLongClickListener(v -> {
|
||||
|
||||
@@ -40,9 +40,9 @@ import java.util.concurrent.TimeUnit;
|
||||
public class DateUtils extends android.text.format.DateUtils {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DateUtils.class.getSimpleName();
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
|
||||
private static final SimpleDateFormat BRIEF_EXACT_FORMAT = new SimpleDateFormat();
|
||||
private static final String TAG = DateUtils.class.getSimpleName();
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<SimpleDateFormat> BRIEF_EXACT_FORMAT = new ThreadLocal<>();
|
||||
|
||||
private static boolean isWithin(final long millis, final long span, final TimeUnit unit) {
|
||||
return System.currentTimeMillis() - millis <= unit.toMillis(span);
|
||||
@@ -166,7 +166,10 @@ public class DateUtils extends android.text.format.DateUtils {
|
||||
}
|
||||
|
||||
public static boolean isSameDay(long t1, long t2) {
|
||||
return DATE_FORMAT.format(new Date(t1)).equals(DATE_FORMAT.format(new Date(t2)));
|
||||
String d1 = getDateFormat().format(new Date(t1));
|
||||
String d2 = getDateFormat().format(new Date(t2));
|
||||
|
||||
return d1.equals(d2);
|
||||
}
|
||||
|
||||
public static boolean isSameExtendedRelativeTimestamp(@NonNull Context context, @NonNull Locale locale, long t1, long t2) {
|
||||
@@ -214,4 +217,28 @@ public class DateUtils extends android.text.format.DateUtils {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private static SimpleDateFormat getDateFormat() {
|
||||
SimpleDateFormat format = DATE_FORMAT.get();
|
||||
|
||||
if (format == null) {
|
||||
format = new SimpleDateFormat("yyyyMMdd");
|
||||
DATE_FORMAT.set(format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private static SimpleDateFormat getBriefExactFormat() {
|
||||
SimpleDateFormat format = BRIEF_EXACT_FORMAT.get();
|
||||
|
||||
if (format == null) {
|
||||
format = new SimpleDateFormat();
|
||||
BRIEF_EXACT_FORMAT.set(format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.MemoryInfo;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Easy access to various properties of the device, typically to make performance-related decisions.
|
||||
*/
|
||||
public final class DeviceProperties {
|
||||
|
||||
/**
|
||||
* Whether or not we believe the device has the performance capabilities to efficiently render
|
||||
* large numbers of APNGs simultaneously.
|
||||
*/
|
||||
public static boolean shouldAllowApngStickerAnimation(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MemoryInfo memoryInfo = getMemoryInfo(context);
|
||||
int memoryMb = (int) ByteUnit.BYTES.toMegabytes(memoryInfo.totalMem);
|
||||
|
||||
return !isLowMemoryDevice(context) &&
|
||||
!memoryInfo.lowMemory &&
|
||||
(memoryMb >= FeatureFlags.animatedStickerMinimumTotalMemoryMb() ||
|
||||
getMemoryClass(context) >= FeatureFlags.animatedStickerMinimumMemoryClass());
|
||||
}
|
||||
|
||||
public static boolean isLowMemoryDevice(@NonNull Context context) {
|
||||
ActivityManager activityManager = ServiceUtil.getActivityManager(context);
|
||||
return activityManager.isLowRamDevice();
|
||||
}
|
||||
|
||||
public static int getMemoryClass(@NonNull Context context) {
|
||||
ActivityManager activityManager = ServiceUtil.getActivityManager(context);
|
||||
return activityManager.getMemoryClass();
|
||||
}
|
||||
|
||||
public static @NonNull MemoryInfo getMemoryInfo(@NonNull Context context) {
|
||||
MemoryInfo info = new MemoryInfo();
|
||||
ActivityManager activityManager = ServiceUtil.getActivityManager(context);
|
||||
|
||||
activityManager.getMemoryInfo(info);
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -49,29 +49,32 @@ public final class FeatureFlags {
|
||||
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String GROUPS_V2_RECOMMENDED_LIMIT = "global.groupsv2.maxGroupSize";
|
||||
private static final String GROUPS_V2_HARD_LIMIT = "global.groupsv2.groupSizeHardLimit";
|
||||
private static final String GROUP_NAME_MAX_LENGTH = "global.groupsv2.maxNameLength";
|
||||
private static final String INTERNAL_USER = "android.internalUser";
|
||||
private static final String VERIFY_V2 = "android.verifyV2";
|
||||
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
|
||||
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
|
||||
public static final String RESEARCH_MEGAPHONE_1 = "research.megaphone.1";
|
||||
public static final String DONATE_MEGAPHONE = "android.donate";
|
||||
private static final String VIEWED_RECEIPTS = "android.viewed.receipts";
|
||||
private static final String GROUP_CALLING = "android.groupsv2.calling.2";
|
||||
private static final String GV1_MANUAL_MIGRATE = "android.groupsV1Migration.manual";
|
||||
private static final String GV1_FORCED_MIGRATE = "android.groupsV1Migration.forced";
|
||||
private static final String GV1_MIGRATION_JOB = "android.groupsV1Migration.job";
|
||||
private static final String SEND_VIEWED_RECEIPTS = "android.sendViewedReceipts";
|
||||
private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer";
|
||||
private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds";
|
||||
private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2";
|
||||
private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval";
|
||||
private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
|
||||
private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
|
||||
private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String GROUPS_V2_RECOMMENDED_LIMIT = "global.groupsv2.maxGroupSize";
|
||||
private static final String GROUPS_V2_HARD_LIMIT = "global.groupsv2.groupSizeHardLimit";
|
||||
private static final String GROUP_NAME_MAX_LENGTH = "global.groupsv2.maxNameLength";
|
||||
private static final String INTERNAL_USER = "android.internalUser";
|
||||
private static final String VERIFY_V2 = "android.verifyV2";
|
||||
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
|
||||
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
|
||||
public static final String RESEARCH_MEGAPHONE_1 = "research.megaphone.1";
|
||||
public static final String DONATE_MEGAPHONE = "android.donate";
|
||||
private static final String VIEWED_RECEIPTS = "android.viewed.receipts";
|
||||
private static final String GROUP_CALLING = "android.groupsv2.calling.2";
|
||||
private static final String GV1_MANUAL_MIGRATE = "android.groupsV1Migration.manual";
|
||||
private static final String GV1_FORCED_MIGRATE = "android.groupsV1Migration.forced";
|
||||
private static final String GV1_MIGRATION_JOB = "android.groupsV1Migration.job";
|
||||
private static final String SEND_VIEWED_RECEIPTS = "android.sendViewedReceipts";
|
||||
private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer";
|
||||
private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds";
|
||||
private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2";
|
||||
private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval";
|
||||
private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
|
||||
private static final String SERVER_ERROR_MAX_BACKOFF = "android.serverErrorMaxBackoff";
|
||||
private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
|
||||
private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
|
||||
private static final String ANIMATED_STICKER_MIN_MEMORY = "android.animatedStickerMinMemory";
|
||||
private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -99,8 +102,11 @@ public final class FeatureFlags {
|
||||
AUTOMATIC_SESSION_RESET,
|
||||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
SERVER_ERROR_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
SHARE_SELECTION_LIMIT
|
||||
SHARE_SELECTION_LIMIT,
|
||||
ANIMATED_STICKER_MIN_MEMORY,
|
||||
ANIMATED_STICKER_MIN_TOTAL_MEMORY
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -138,8 +144,11 @@ public final class FeatureFlags {
|
||||
AUTOMATIC_SESSION_RESET,
|
||||
AUTOMATIC_SESSION_INTERVAL,
|
||||
DEFAULT_MAX_BACKOFF,
|
||||
SERVER_ERROR_MAX_BACKOFF,
|
||||
OKHTTP_AUTOMATIC_RETRY,
|
||||
SHARE_SELECTION_LIMIT
|
||||
SHARE_SELECTION_LIMIT,
|
||||
ANIMATED_STICKER_MIN_MEMORY,
|
||||
ANIMATED_STICKER_MIN_TOTAL_MEMORY
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -315,8 +324,14 @@ public final class FeatureFlags {
|
||||
return getInteger(AUTOMATIC_SESSION_RESET, (int) TimeUnit.HOURS.toSeconds(1));
|
||||
}
|
||||
|
||||
public static int getDefaultMaxBackoffSeconds() {
|
||||
return getInteger(DEFAULT_MAX_BACKOFF, 60);
|
||||
/** The default maximum backoff for jobs. */
|
||||
public static long getDefaultMaxBackoff() {
|
||||
return TimeUnit.SECONDS.toMillis(getInteger(DEFAULT_MAX_BACKOFF, 60));
|
||||
}
|
||||
|
||||
/** The maximum backoff for network jobs that hit a 5xx error. */
|
||||
public static long getServerErrorMaxBackoff() {
|
||||
return TimeUnit.SECONDS.toMillis(getInteger(SERVER_ERROR_MAX_BACKOFF, (int) TimeUnit.HOURS.toSeconds(6)));
|
||||
}
|
||||
|
||||
/** Whether or not to allow automatic retries from OkHttp */
|
||||
@@ -324,6 +339,16 @@ public final class FeatureFlags {
|
||||
return getBoolean(OKHTTP_AUTOMATIC_RETRY, false);
|
||||
}
|
||||
|
||||
/** The minimum memory class required for rendering animated stickers in the keyboard and such */
|
||||
public static int animatedStickerMinimumMemoryClass() {
|
||||
return getInteger(ANIMATED_STICKER_MIN_MEMORY, 193);
|
||||
}
|
||||
|
||||
/** The minimum total memory for rendering animated stickers in the keyboard and such */
|
||||
public static int animatedStickerMinimumTotalMemoryMb() {
|
||||
return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3));
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.net.NetworkInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
public final class NetworkUtil {
|
||||
@@ -27,7 +28,11 @@ public final class NetworkUtil {
|
||||
return info != null && info.isConnected() && info.isRoaming() && info.getType() == ConnectivityManager.TYPE_MOBILE;
|
||||
}
|
||||
|
||||
public static boolean useLowBandwidthCalling(@NonNull Context context) {
|
||||
public static @NonNull CallManager.BandwidthMode getCallingBandwidthMode(@NonNull Context context) {
|
||||
return useLowBandwidthCalling(context) ? CallManager.BandwidthMode.LOW : CallManager.BandwidthMode.NORMAL;
|
||||
}
|
||||
|
||||
private static boolean useLowBandwidthCalling(@NonNull Context context) {
|
||||
switch (SignalStore.settings().getCallBandwidthMode()) {
|
||||
case HIGH_ON_WIFI:
|
||||
return !NetworkUtil.isConnectedWifi(context);
|
||||
|
||||
@@ -6,12 +6,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
@@ -45,6 +47,8 @@ import java.util.concurrent.TimeoutException;
|
||||
*/
|
||||
public final class ProfileUtil {
|
||||
|
||||
private static final String TAG = Log.tag(ProfileUtil.class);
|
||||
|
||||
private ProfileUtil() {
|
||||
}
|
||||
|
||||
@@ -164,6 +168,9 @@ public final class ProfileUtil {
|
||||
@Nullable StreamDetails avatar)
|
||||
throws IOException
|
||||
{
|
||||
Log.d(TAG, "Uploading " + (!Util.isEmpty(about) ? "non-" : "") + "empty about.");
|
||||
Log.d(TAG, "Uploading " + (!Util.isEmpty(aboutEmoji) ? "non-" : "") + "empty emoji.");
|
||||
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
String avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), about, aboutEmoji, avatar).orNull();
|
||||
|
||||
@@ -250,6 +250,11 @@ public final class ViewUtil {
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
public static void setBottomMargin(@NonNull View view, int margin) {
|
||||
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin = margin;
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
public static void setPaddingTop(@NonNull View view, int padding) {
|
||||
view.setPadding(view.getPaddingLeft(), padding, view.getPaddingRight(), view.getPaddingBottom());
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ final class GradientChatWallpaper implements ChatWallpaper, Parcelable {
|
||||
private final float[] positions;
|
||||
|
||||
private final Rect fillRect = new Rect();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
|
||||
private RotatableGradientDrawable(float degrees, int[] colors, @Nullable float[] positions) {
|
||||
this.degrees = degrees + 225f;
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_background_secondary" />
|
||||
<solid android:color="@color/conversation_item_wallpaper_bubble_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/wallpaper_bubble_color" />
|
||||
<corners android:bottomLeftRadius="12dp"
|
||||
android:bottomRightRadius="12dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/wallpaper_bubble_color" />
|
||||
</shape>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/wallpaper_bubble_color" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/wallpaper_bubble_color" />
|
||||
<corners android:topLeftRadius="12dp"
|
||||
android:topRightRadius="12dp"/>
|
||||
</shape>
|
||||
@@ -60,12 +60,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:background="@drawable/chat_wallpaper_preview_bubble_background_accent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="7dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -91,6 +93,7 @@
|
||||
android:id="@+id/preview_bubble_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/chat_wallpaper_preview_bubble_background"
|
||||
@@ -99,6 +102,7 @@
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="7dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -138,17 +142,17 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="17dp"
|
||||
android:text="@string/WallpaperCropActivity__blur_photo"
|
||||
android:textColor="@color/signal_button_primary"
|
||||
android:background="@drawable/wallpaper_crop_bubble_background"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
app:switchPadding="4dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/WallpaperCropActivity__blur_photo"
|
||||
android:textColor="@color/signal_button_primary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/preview_guideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:switchPadding="4dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/preview_set_wallpaper"
|
||||
@@ -165,7 +169,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/wallpaper_preview_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/navigation_bar_guideline"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/navigation_bar_guideline" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>
|
||||
@@ -189,7 +189,7 @@
|
||||
app:layout_constraintEnd_toEndOf="@id/chat_wallpaper_preview_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/chat_wallpaper_preview_top_bar"
|
||||
app:srcCompat="@drawable/chat_wallpaper_preview_bubble_10"
|
||||
app:tint="@color/signal_background_secondary" />
|
||||
app:tint="@color/conversation_item_wallpaper_bubble_color" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -51,12 +51,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:background="@drawable/chat_wallpaper_preview_bubble_background_accent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="7dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -65,7 +67,6 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="194dp"
|
||||
android:text="@string/ChatWallpaperPreviewActivity__swipe_to_preview_more_wallpapers"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_white" />
|
||||
@@ -83,6 +84,7 @@
|
||||
android:id="@+id/preview_bubble_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="48dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/chat_wallpaper_preview_bubble_background"
|
||||
@@ -91,6 +93,7 @@
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="7dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -100,7 +103,6 @@
|
||||
android:id="@+id/preview_bubble_2_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="194dp"
|
||||
android:text="@string/ChatWallpaperPreviewActivity__set_wallpaper_for_all_chats"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_primary" />
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/conversation_wallpaper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_wallpaper_dim"
|
||||
@@ -24,10 +25,11 @@
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/status_bar_guideline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintTop_toTopOf="@id/status_bar_guideline">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
@@ -75,10 +77,12 @@
|
||||
|
||||
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="@id/status_bar_guideline"
|
||||
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline">
|
||||
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/conversation_container"
|
||||
@@ -217,5 +221,15 @@
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
||||
|
||||
<include layout="@layout/conversation_reaction_scrubber" />
|
||||
<ViewStub
|
||||
android:id="@+id/conversation_reaction_scrubber_stub"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_bar_guideline"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/navigation_bar_guideline"
|
||||
app:layout_constraintStart_toStartOf="@+id/parent_start_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline"
|
||||
android:inflatedId="@+id/conversation_reaction_scrubber"
|
||||
android:layout="@layout/conversation_reaction_scrubber" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/last_seen_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:background="@color/core_grey_45" />
|
||||
|
||||
|
||||
@@ -8,21 +8,20 @@
|
||||
android:background="@drawable/conversation_item_background"
|
||||
android:focusable="true"
|
||||
android:paddingStart="26dp"
|
||||
android:paddingEnd="26dp"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false">
|
||||
android:paddingEnd="26dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/conversation_update_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="100dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginTop="@dimen/conversation_update_vertical_margin"
|
||||
android:layout_marginBottom="@dimen/conversation_update_vertical_margin"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingTop="@dimen/conversation_update_vertical_padding"
|
||||
android:paddingBottom="@dimen/conversation_update_vertical_padding"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp">
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user