Compare commits

...

62 Commits

Author SHA1 Message Date
Greyson Parrelli
4f9f62992f Bump version to 5.3.8 2021-01-28 18:58:45 -05:00
Greyson Parrelli
1938d6cae0 Updated language translations. 2021-01-28 18:57:17 -05:00
Greyson Parrelli
13e8c55781 Delete duplicated internal preference. 2021-01-28 18:51:42 -05:00
Greyson Parrelli
6264f9b585 Have a much longer backoff maximum for 5xx errors. 2021-01-28 18:51:42 -05:00
Greyson Parrelli
4482bfcabb Ensure NonSuccessfulReponseCodeException knows the response code. 2021-01-28 18:51:42 -05:00
Alan Evans
015088a53f Fix registration issue where pin box is left disabled. 2021-01-28 18:51:42 -05:00
Alan Evans
ef7d707432 Fix wallpaper preview layout for longer text. 2021-01-28 18:44:04 -05:00
Alan Evans
d1f6a924fb Allow block of any recipient except MMS groups still. 2021-01-28 18:44:04 -05:00
Alan Evans
f312757daf Fix potential Base64 < 4 characters crash on group invite. 2021-01-28 18:44:04 -05:00
Greyson Parrelli
1d83729e6c Move backoff calculation into jobs. 2021-01-28 18:44:04 -05:00
Alan Evans
6a45858b4a Replace Firebase ML vision with built in face detection. 2021-01-28 18:44:04 -05:00
Alex Hart
1b448c2bdf Move reaction overlay UI into a stub. 2021-01-27 16:34:59 -04:00
Alan Evans
f6cd190245 Prevent warnings about multiple substitutions in non-positional format. 2021-01-27 12:32:10 -04:00
Alan Evans
23303e5407 Show name of message sender for groups in conversation list. 2021-01-27 11:53:31 -04:00
Alan Evans
b5237848e9 Restore pinned chats on archive undo. 2021-01-27 11:52:32 -04:00
Alan Evans
7cac0c9a7c UUID is now returned always. 2021-01-27 11:52:32 -04:00
Jim Gustafson
9dbbe4675f Update to RingRTC v2.9.0
Co-authored-by: Alex Hart <alex@signal.org>
2021-01-27 11:52:32 -04:00
Greyson Parrelli
95978f16e9 Possible fix to getting thrown to the bottom while reading unreads.
Shoutout to @fumiakiy for the excellent research here!

Sometimes we get thrown to the bottom of the list (or other list
locations) when reading content in the middle of the list. Most often,
this happens when you have a lot of unread messages and you open the
conversation.

FixedSizePagingController#onDataNeededAroundIndex() can be called very
fast in rapid succession, and we use the DataStatus class for
bookkeeping to know which requests are in-flight. We then make those
requests in LIFO order in order to make sure that the data visible on
screen now gets the highest priority.

...But in practice, that LIFO ordering can make things a little screwy.
Imagine we called onDataNeedAroundIndex() 50 times in rapid succession
(1, 2..., 50). Each time it's called, we generate a range and mark that
range as being fetched in DataStatus. That could mean that the latest
request for index 50 might only have, like, 1 item in it, because a
previously-enqueued fetch already got assigned most of it's data.

BUT we execute the nearly-empty request for index 50 first because of the
LIFO ordering. We give that data to RecyclerView first, and it doesn't like
that at all, and it jumps to weird places because we gave it mostly
null values, which are rendered as placeholder values (which are smaller
than real cells). So then, when we give it the real data right after,
its position is all off.

I switched to a serial executor. That prevents us from giving back weird
lists. The consequence is that if you scroll super fast, you run the
risk of the executor getting 'backed up' fetching data that's offscreen.
However, in practice, I couldn't trigger this. We'll see how it goes. I
think the true solution is a smarter way of fetching and ordering
requests, but that gets to be really tricky from a threading
perspective, and I'd rather keep things simple.
2021-01-27 11:52:32 -04:00
Alan Evans
d055bba452 Lint to prevent glide log usage. 2021-01-27 11:52:32 -04:00
Greyson Parrelli
8ef809a02b Only cluster updates of the same type together. 2021-01-27 11:52:32 -04:00
Alex Hart
458941f952 Enable dither on the gradient painter. 2021-01-27 11:52:32 -04:00
Greyson Parrelli
5852a508aa Bump version to 5.3.7 2021-01-27 10:17:53 -05:00
Greyson Parrelli
e2b4995fbb Updated language translations. 2021-01-27 10:17:20 -05:00
Greyson Parrelli
a3556d9f68 Ensure passphrases are disabled for all but the oldest users. 2021-01-27 10:10:26 -05:00
Greyson Parrelli
fe890a1a41 Added some additional logging around About. 2021-01-27 10:09:07 -05:00
Greyson Parrelli
c06bb18249 Fix navigation bar theming issue.
Fixes #10772
2021-01-27 09:09:56 -05:00
Greyson Parrelli
9099969b41 Bump version to 5.3.6 2021-01-25 18:14:14 -05:00
Greyson Parrelli
6358f59f67 Updated language translations. 2021-01-25 18:13:49 -05:00
Greyson Parrelli
073034dd3c Update logic on deciding whether to bulk animate stickers. 2021-01-25 13:57:24 -05:00
Alan Evans
17fb815805 Prevent duplicate member UUIDs in groups.
Fixes #10702
2021-01-25 13:06:15 -04:00
Alan Evans
409e7c41b4 Restore group update message "Loading" text. 2021-01-25 12:58:24 -04:00
Alan Evans
b9a1a5027c Fix rotation locked after voice record cancel and allow rotation when recording locked. 2021-01-25 12:46:49 -04:00
Alan Evans
49535f6378 Fix initial LiveData value for recipients. 2021-01-25 12:30:21 -04:00
Greyson Parrelli
c058452605 Bump version to 5.3.5 2021-01-24 17:50:40 -05:00
Greyson Parrelli
b3511dba77 Updated language translations. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
afbe27c55f Revert "Bump libsignal-client to 0.2.2"
This reverts commit ce156c3450.
2021-01-24 17:40:44 -05:00
Greyson Parrelli
41d227207d Fix author title on remote deleted group messages. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
92b586c061 Disable mass APNG animation on low-memory devices. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
acbc17c909 Bump version to 5.3.4 2021-01-24 03:35:56 -05:00
Greyson Parrelli
15f17747ee Updated language translations. 2021-01-24 03:35:56 -05:00
Greyson Parrelli
781054fc9d Switch dark theme bubbles with wallpaper to grey_95 instead of black. 2021-01-24 03:35:56 -05:00
Greyson Parrelli
b59769a30a Do not allow saving pending media. 2021-01-24 03:10:03 -05:00
Greyson Parrelli
26e0e09e24 Update padding and margins on conversation updates. 2021-01-24 03:07:49 -05:00
Greyson Parrelli
3a2990a911 Fix crash when sharing stickers you don't have installed. 2021-01-24 02:33:24 -05:00
Greyson Parrelli
d8060b3041 Fix inset issues in landscape. 2021-01-24 02:22:09 -05:00
Greyson Parrelli
f42ec5318f Bump version to 5.3.3 2021-01-23 18:58:48 -05:00
Greyson Parrelli
bd0d425cbf Updated language translations. 2021-01-23 18:58:48 -05:00
Greyson Parrelli
b3d5d7c33e Move cursor to end of text field after select About preset. 2021-01-23 18:58:48 -05:00
Greyson Parrelli
1746869dc3 Fix issue with rendering of group update timestamps.
TIL SimpleDateFormat is not thread safe.
Across instances.

God forgive them, for they know not what they did.
2021-01-23 18:48:14 -05:00
Greyson Parrelli
633f4cbbe5 Disable 'loading' update message. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
0944e2f758 Apply contact list SMS filter to 'recents' section. 2021-01-23 18:48:14 -05:00
Alex Hart
b49e4004ab Restrict SMS in multishare. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
68381f8b64 Fix text color of recent conversations in share activity. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
f180066058 Disallow link previews in multi-forward when sending to SMS. 2021-01-23 15:31:48 -05:00
Greyson Parrelli
6b7de2e85e Make voice note play button visible in wallpaper mode. 2021-01-23 15:20:58 -05:00
Greyson Parrelli
c650a978e9 Update styling of last seen divider. 2021-01-23 15:02:11 -05:00
Greyson Parrelli
e05cadafe6 Collapse adjacent conversation updates. 2021-01-23 14:55:19 -05:00
Greyson Parrelli
c6008a4f90 Disable forwarding of pending media. 2021-01-23 13:39:13 -05:00
Greyson Parrelli
5624855eba Make ManageProfileActivity work with screen lock. 2021-01-23 13:27:37 -05:00
Greyson Parrelli
799ff86fc0 Fixed tinting of wallpaper bubble previews. 2021-01-23 13:18:53 -05:00
Greyson Parrelli
798fc84e82 Fix issue where empty about could be rendered in contact list. 2021-01-23 12:56:00 -05:00
Greyson Parrelli
cc363a3c88 Fix wallpaper sizing issues in landscape. 2021-01-23 12:41:29 -05:00
199 changed files with 6696 additions and 2386 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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() {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,6 @@ public class JobMigrator {
jobSpec.getNextRunAttemptTime(),
jobSpec.getRunAttempt(),
jobSpec.getMaxAttempts(),
jobSpec.getMaxBackoff(),
jobSpec.getLifespan(),
dataSerializer.serialize(updatedJobData.getData()),
jobSpec.getSerializedInputData(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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