Compare commits

...

41 Commits

Author SHA1 Message Date
Alex Hart
9e7c55847e Bump version to 4.78.3 2020-11-20 16:54:53 -04:00
Alex Hart
5209b74605 Updated language translations. 2020-11-20 16:51:57 -04:00
Cody Henthorne
b90a74d26a Add additional Group Calling features. 2020-11-20 15:42:46 -05:00
Alan Evans
8c1737e597 Increase uncompressed video attachment size to 300 Mb. 2020-11-20 15:15:42 -04:00
Greyson Parrelli
2ea5bd2d44 Convert GV1->GV2 migration flags to booleans. 2020-11-20 13:50:14 -05:00
Greyson Parrelli
4166e7931e Fix membership diffs that occur during a GV1->GV2 migration.
Co-authored-by: Alan Evans <alan@signal.org>
2020-11-20 13:26:17 -05:00
Alan Evans
89f2c25d73 Display video file output size and duration during clipping.
Prevent video upscale, i.e. use input bit rate if lower than our normal target rates.
Do not time limit videos that are under the send file size.
Increase time limit to 10 minutes to match our lowest acceptable bitrate.
2020-11-20 13:27:58 -04:00
Alan Evans
abb1ca2afe Increase in-app recording duration to 60 seconds. 2020-11-20 13:11:48 -04:00
Greyson Parrelli
f7befd1593 Block GV1 creation if forced migrations are enabled. 2020-11-20 11:49:18 -05:00
Greyson Parrelli
28511de23c Ensure we properly detect update messages for migrations. 2020-11-20 11:39:55 -05:00
Greyson Parrelli
2ff3d1b7c5 Update phrasing on donate megaphone dismiss button. 2020-11-19 13:46:35 -05:00
Greyson Parrelli
fe6ae7e142 Don't show donate or research megaphones on new app installs. 2020-11-19 08:42:35 -05:00
Greyson Parrelli
0da6c83ce4 Bump version to 4.78.2 2020-11-18 19:52:48 -05:00
Greyson Parrelli
184b7db43c Updated language translations. 2020-11-18 19:51:15 -05:00
Greyson Parrelli
e442e34c1b More reliably setup initial preferences. 2020-11-18 19:47:27 -05:00
Greyson Parrelli
011efb0ce7 Request READ_PHONE_NUMBERS permission when necessary. 2020-11-18 19:47:27 -05:00
Alex Hart
63d00f87d8 Bump version to 4.78.1 2020-11-18 19:26:12 -04:00
Alex Hart
0323858145 Updated language translations. 2020-11-18 19:22:19 -04:00
Greyson Parrelli
a70e8ec7a7 Update reproducible build instructions. 2020-11-18 19:11:48 -04:00
Alex Hart
b306a3ef41 Update Dockerfile to utilize new commandline tools distributable. 2020-11-18 19:11:48 -04:00
Greyson Parrelli
ccd3467a61 Fix crash with MediaSendActivity progress dialog.
Co-authored-by: Alan Evans <alan@signal.org>
2020-11-18 17:31:43 -05:00
Alex Hart
40338afe7a Bump version to 4.78.0 2020-11-18 16:30:43 -04:00
Alex Hart
ff97f6af56 Updated language translations. 2020-11-18 16:30:43 -04:00
Alan Evans
6e7858e00f Restrict video send duration. 2020-11-18 16:30:43 -04:00
Greyson Parrelli
95468c85a8 Break large read receipt messages into chunks. 2020-11-18 14:19:28 -05:00
Cody Henthorne
f59e10d82c Fix read/unread conversation list colors. 2020-11-18 14:00:14 -05:00
Alex Hart
930370783e Implement ShortcutInfo for API 30. 2020-11-18 14:25:01 -04:00
Alex Hart
75062ada8a Upgrade SDK to 30. 2020-11-18 13:38:27 -04:00
Cody Henthorne
23618923d8 Attempt to recover from reoccurring exceptions when showing notifications. 2020-11-18 12:28:05 -05:00
Greyson Parrelli
f1d3a2f322 Fix Android 11 issue where keyboard wasn't auto-showing for PIN reminders. 2020-11-18 11:57:33 -05:00
Cody Henthorne
3b7fbbaf6e Fix crash when call concluded on non-existent remote peer. 2020-11-18 11:34:06 -05:00
Greyson Parrelli
725d793b20 Fix issue with link preview UI sizing. 2020-11-18 11:33:44 -05:00
Greyson Parrelli
5c3baca055 Add support for a donation megaphone. 2020-11-18 10:33:46 -05:00
Alan Evans
6e5abc92a0 Fix situation where group thread does not yet exist. 2020-11-17 15:52:38 -04:00
Alex Hart
8df6e95781 Stop proximity sensor on pause. 2020-11-17 15:15:13 -04:00
Alex Hart
2290a6c0df Synchronize voice note queue reads and writes. 2020-11-17 14:42:01 -04:00
Jack Lloyd
907e8d93a3 Move to Signal Protocol written in Rust.
Co-authored-by: Alex Hart <alex@signal.org>
2020-11-16 12:28:11 -05:00
Cody Henthorne
918497fb94 Bump version to 4.77.3 2020-11-16 11:49:35 -05:00
Cody Henthorne
3ccd6304c7 Updated language translations. 2020-11-16 11:47:55 -05:00
Greyson Parrelli
51d47adf57 Fix issue where FeatureFlags were triggering listeners for non-changes. 2020-11-16 11:27:58 -05:00
Cody Henthorne
f1e5206f56 Fix Invite Friend theming bug. 2020-11-16 10:17:42 -05:00
181 changed files with 4187 additions and 888 deletions

View File

@@ -95,8 +95,8 @@ protobuf {
}
}
def canonicalVersionCode = 739
def canonicalVersionName = "4.77.2"
def canonicalVersionCode = 744
def canonicalVersionName = "4.78.3"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -109,8 +109,8 @@ def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties')
android {
flavorDimensions 'distribution', 'environment'
compileSdkVersion 29
buildToolsVersion '29.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.2'
useLibrary 'org.apache.http.legacy'
dexOptions {
@@ -133,7 +133,7 @@ android {
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 29
targetSdkVersion 30
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -147,6 +147,7 @@ android {
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
@@ -364,10 +365,11 @@ dependencies {
implementation project(':libsignal-service')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.3'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.8.0'
implementation 'org.signal:ringrtc-android:2.8.3'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -35,6 +35,7 @@
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

View File

@@ -34,6 +34,11 @@ public final class AppInitialization {
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setReadReceiptsEnabled(context, true);
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));

View File

@@ -60,6 +60,7 @@ public interface BindableConversationItem extends Unbindable {
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
void onJoinGroupCallClicked();
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -100,7 +100,7 @@ public class NewConversationActivity extends ContactSelectionActivity
private void launch(Recipient recipient) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
intent.setDataAndType(getIntent().getData(), getIntent().getType());

View File

@@ -66,12 +66,6 @@ public class PassphraseCreateActivity extends PassphraseActivity {
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
return null;
}

View File

@@ -53,7 +53,7 @@ public class SmsSendtoActivity extends Activity {
nextIntent = new Intent(this, ConversationActivity.class);
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
}
return nextIntent;
}

View File

@@ -207,7 +207,6 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void initializeResources() {
callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
callScreen.setEventListener(new EventListener());
}
private void initializeViewModel() {
@@ -218,6 +217,17 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null) {
if (state.needsNewRequestSizes()) {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
startService(intent);
}
}
});
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@@ -619,13 +629,4 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
viewModel.setIsViewingFocusedParticipant(page);
}
}
private class EventListener implements WebRtcCallView.EventListener {
@Override
public void onPotentialLayoutChange() {
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
startService(intent);
}
}
}

View File

@@ -12,7 +12,6 @@ import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Consumer;
import com.annimon.stream.function.Predicate;
import com.google.android.collect.Sets;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase;
@@ -38,6 +37,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.kdf.HKDFv3;
@@ -69,7 +69,7 @@ public class FullBackupExporter extends FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = FullBackupExporter.class.getSimpleName();
private static final Set<String> BLACKLISTED_TABLES = Sets.newHashSet(
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
SignedPreKeyDatabase.TABLE_NAME,
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -132,9 +133,23 @@ public final class AvatarImageView extends AppCompatImageView {
setAvatar(GlideApp.with(this), recipient, false);
}
/**
* Shows self as the profile avatar.
*/
public void setAvatarUsingProfile(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false, true);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
setAvatar(requestManager, recipient, quickContactEnabled, false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
if (recipient != null) {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self(),
Recipient.self().getProfileAvatar()))
: new RecipientContactPhoto(recipient);
if (!photo.equals(recipientContactPhoto)) {
requestManager.clear(this);
@@ -218,9 +233,13 @@ public final class AvatarImageView extends AppCompatImageView {
private final boolean ready;
RecipientContactPhoto(@NonNull Recipient recipient) {
this(recipient, recipient.getContactPhoto());
}
RecipientContactPhoto(@NonNull Recipient recipient, @Nullable ContactPhoto contactPhoto) {
this.recipient = recipient;
this.ready = !recipient.isResolving();
this.contactPhoto = recipient.getContactPhoto();
this.contactPhoto = contactPhoto;
}
public boolean equals(@Nullable RecipientContactPhoto other) {

View File

@@ -12,6 +12,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VersionTracker;
import java.util.concurrent.TimeUnit;
@@ -25,7 +26,7 @@ public class RatingManager {
public static void showRatingDialogIfNecessary(Context context) {
if (!TextSecurePreferences.isRatingEnabled(context)) return;
long daysSinceInstall = getDaysSinceInstalled(context);
long daysSinceInstall = VersionTracker.getDaysSinceFirstInstalled(context);
long laterTimestamp = TextSecurePreferences.getRatingLaterTimestamp(context);
if (daysSinceInstall >= DAYS_SINCE_INSTALL_THRESHOLD &&
@@ -72,17 +73,4 @@ public class RatingManager {
}
}
private static long getDaysSinceInstalled(Context context) {
try {
long installTimestamp = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0)
.firstInstallTime;
return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - installTimestamp);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, e);
return 0;
}
}
}

View File

@@ -140,56 +140,58 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
}
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
for (MediaDescriptionCompat description : descriptions) {
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
MediaDescriptionCompat next = createNextClone(description);
int currentIndex = player.getCurrentWindowIndex();
synchronized (queueDataAdapter) {
for (MediaDescriptionCompat description : descriptions) {
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
MediaDescriptionCompat next = createNextClone(description);
int currentIndex = player.getCurrentWindowIndex();
if (holderIndex != -1) {
queueDataAdapter.remove(holderIndex);
if (!queueDataAdapter.isEmpty()) {
if (holderIndex != -1) {
queueDataAdapter.remove(holderIndex);
}
queueDataAdapter.add(holderIndex, createNextClone(description));
queueDataAdapter.add(holderIndex, description);
if (currentIndex != holderIndex) {
dataSource.removeMediaSource(holderIndex);
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
}
if (currentIndex != holderIndex + 1) {
if (dataSource.getSize() > 1) {
dataSource.removeMediaSource(holderIndex + 1);
if (!queueDataAdapter.isEmpty()) {
queueDataAdapter.remove(holderIndex);
}
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
queueDataAdapter.add(holderIndex, createNextClone(description));
queueDataAdapter.add(holderIndex, description);
if (currentIndex != holderIndex) {
dataSource.removeMediaSource(holderIndex);
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
}
if (currentIndex != holderIndex + 1) {
if (dataSource.getSize() > 1) {
dataSource.removeMediaSource(holderIndex + 1);
}
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
}
} else {
int insertLocation = queueDataAdapter.indexAfter(description);
queueDataAdapter.add(insertLocation, next);
queueDataAdapter.add(insertLocation, description);
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
}
} else {
int insertLocation = queueDataAdapter.indexAfter(description);
queueDataAdapter.add(insertLocation, next);
queueDataAdapter.add(insertLocation, description);
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
}
}
int lastIndex = queueDataAdapter.size() - 1;
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
int lastIndex = queueDataAdapter.size() - 1;
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
queueDataAdapter.remove(lastIndex);
dataSource.removeMediaSource(lastIndex);
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
queueDataAdapter.remove(lastIndex);
dataSource.removeMediaSource(lastIndex);
if (queueDataAdapter.size() > 1) {
MediaDescriptionCompat end = createEndClone(last);
if (queueDataAdapter.size() > 1) {
MediaDescriptionCompat end = createEndClone(last);
queueDataAdapter.add(lastIndex, end);
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
queueDataAdapter.add(lastIndex, end);
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
}
}
}
}

View File

@@ -144,14 +144,15 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
switch (playbackState) {
case Player.STATE_BUFFERING:
case Player.STATE_READY:
voiceNoteProximityManager.onPlayerReady();
voiceNoteNotificationManager.showNotification(player);
if (!playWhenReady) {
stopForeground(false);
becomingNoisyReceiver.unregister();
voiceNoteProximityManager.onPlayerEnded();
} else {
becomingNoisyReceiver.register();
voiceNoteProximityManager.onPlayerReady();
}
break;
default:

View File

@@ -20,31 +20,31 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
@Override
public MediaDescriptionCompat getMediaDescription(int position) {
public synchronized MediaDescriptionCompat getMediaDescription(int position) {
return descriptions.get(position);
}
@Override
public void add(int position, MediaDescriptionCompat description) {
public synchronized void add(int position, MediaDescriptionCompat description) {
descriptions.add(position, description);
}
@Override
public void remove(int position) {
public synchronized void remove(int position) {
descriptions.remove(position);
}
@Override
public void move(int from, int to) {
public synchronized void move(int from, int to) {
MediaDescriptionCompat description = descriptions.remove(from);
descriptions.add(to, description);
}
int size() {
synchronized int size() {
return descriptions.size();
}
int indexOf(@NonNull Uri uri) {
synchronized int indexOf(@NonNull Uri uri) {
for (int i = 0; i < descriptions.size(); i++) {
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
return i;
@@ -54,7 +54,7 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
return -1;
}
int indexAfter(@NonNull MediaDescriptionCompat target) {
synchronized int indexAfter(@NonNull MediaDescriptionCompat target) {
if (isEmpty()) {
return 0;
}
@@ -71,11 +71,11 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
return descriptions.size();
}
boolean isEmpty() {
synchronized boolean isEmpty() {
return descriptions.isEmpty();
}
void clear() {
synchronized void clear() {
descriptions.clear();
}
}

View File

@@ -17,11 +17,13 @@ public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
private final WeakHashMap<Object, Point> requestingSizes;
private boolean dirtySizes;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.requestingSizes = new WeakHashMap<>();
this.dirtySizes = true;
}
public @Nullable EglBase getEglBase() {
@@ -46,12 +48,14 @@ public class BroadcastVideoSink implements VideoSink {
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
synchronized (requestingSizes) {
requestingSizes.put(object, size);
dirtySizes = true;
}
}
void removeRequestingSize(@NonNull Object object) {
synchronized (requestingSizes) {
requestingSizes.remove(object);
dirtySizes = true;
}
}
@@ -71,6 +75,14 @@ public class BroadcastVideoSink implements VideoSink {
return new RequestedSize(width, height);
}
public void newSizeRequested() {
dirtySizes = false;
}
public boolean needsNewRequestingSize() {
return dirtySizes;
}
public static class RequestedSize {
private final int width;
private final int height;

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -42,6 +43,7 @@ public class CallParticipantView extends ConstraintLayout {
private TextureViewRenderer renderer;
private ImageView pipAvatar;
private ContactPhoto contactPhoto;
private View audioMuted;
public CallParticipantView(@NonNull Context context) {
super(context);
@@ -59,9 +61,10 @@ public class CallParticipantView extends ConstraintLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
audioMuted = findViewById(R.id.call_participant_mic_muted);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
useLargeAvatar();
@@ -83,11 +86,13 @@ public class CallParticipantView extends ConstraintLayout {
}
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
avatar.setAvatar(participant.getRecipient());
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
avatar.setAvatarUsingProfile(participant.getRecipient());
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this, true);
setPipAvatar(participant.getRecipient());
contactPhoto = participant.getRecipient().getContactPhoto();
}
audioMuted.setVisibility(participant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE);
}
void setRenderInPip(boolean shouldRenderInPip) {
@@ -103,6 +108,10 @@ public class CallParticipantView extends ConstraintLayout {
changeAvatarParams(SMALL_AVATAR);
}
void releaseRenderer() {
renderer.release();
}
private void changeAvatarParams(int dimension) {
ViewGroup.LayoutParams params = avatar.getLayoutParams();
if (params.height != dimension) {

View File

@@ -14,6 +14,7 @@ import com.google.android.flexbox.FlexboxLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -88,6 +89,8 @@ public class CallParticipantsLayout extends FlexboxLayout {
}
} else if (childCount > count) {
for (int i = count; i < childCount; i++) {
CallParticipantView callParticipantView = getChildAt(count).findViewById(R.id.group_call_participant);
callParticipantView.releaseRenderer();
removeViewAt(count);
}
}

View File

@@ -5,9 +5,12 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import java.util.ArrayList;
@@ -146,6 +149,10 @@ public final class CallParticipantsState {
return isInPipMode;
}
public boolean needsNewRequestSizes() {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
@NonNull WebRtcViewModel webRtcViewModel,
boolean enableVideo)
@@ -161,7 +168,7 @@ public final class CallParticipantsState {
oldState.isInPipMode,
newShowVideoForOutgoing,
webRtcViewModel.getState(),
oldState.getAllRemoteParticipants().size(),
webRtcViewModel.getRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
@@ -233,8 +240,10 @@ public final class CallParticipantsState {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
} else {
} else if (numberOfRemoteParticipants == 1) {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
} else {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;

View File

@@ -89,6 +89,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
return;
}
eglRenderer.clearImage();
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
attachedVideoSink.removeRequestingSize(this);
@@ -265,7 +267,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
@Override
public void onFrame(VideoFrame videoFrame) {
eglRenderer.onFrame(videoFrame);
if (isAttachedToWindow()) {
eglRenderer.onFrame(videoFrame);
}
}
@Override

View File

@@ -92,9 +92,9 @@ public class WebRtcCallView extends FrameLayout {
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private MaterialButton startCall;
private TextView participantCount;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private EventListener eventListener;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
@@ -168,7 +168,6 @@ public class WebRtcCallView extends FrameLayout {
@Override
public void onPageSelected(int position) {
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
runIfNonNull(eventListener, EventListener::onPotentialLayoutChange);
}
});
@@ -239,10 +238,6 @@ public class WebRtcCallView extends FrameLayout {
this.controlsListener = controlsListener;
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public void setMicEnabled(boolean isMicEnabled) {
micToggle.setChecked(isMicEnabled, false);
}
@@ -262,6 +257,12 @@ public class WebRtcCallView extends FrameLayout {
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
}
if (state.getGroupCallState().isNotIdle() && participantCount != null) {
boolean includeSelf = state.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
participantCount.setText(String.valueOf(state.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
@@ -271,10 +272,6 @@ public class WebRtcCallView extends FrameLayout {
} else {
layoutParticipantsForSmallCount();
}
if (eventListener != null) {
eventListener.onPotentialLayoutChange();
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
@@ -362,7 +359,11 @@ public class WebRtcCallView extends FrameLayout {
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
toolbar.inflateMenu(R.menu.group_call);
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
View showParticipants = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list).getActionView();
showParticipants.setOnClickListener(unused -> showParticipantsList());
participantCount = showParticipants.findViewById(R.id.show_participants_menu_counter);
}
} else {
recipientName.setText(recipient.getDisplayName(getContext()));
@@ -398,15 +399,13 @@ public class WebRtcCallView extends FrameLayout {
case DISCONNECTED:
status.setText(R.string.WebRtcCallView__disconnected);
break;
case CONNECTING:
status.setText(R.string.WebRtcCallView__connecting);
break;
case RECONNECTING:
status.setText(R.string.WebRtcCallView__reconnecting);
break;
case CONNECTED_AND_JOINING:
status.setText(R.string.WebRtcCallView__joining);
break;
case CONNECTING:
case CONNECTED_AND_JOINED:
case CONNECTED:
status.setText("");

View File

@@ -117,7 +117,7 @@ public class WebRtcCallViewModel extends ViewModel {
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
callConnectedTime = webRtcViewModel.getCallConnectedTime();
startTimer();
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED) {
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED || webRtcViewModel.getGroupState().isNotIdleOrConnected()) {
cancelTimer();
callConnectedTime = -1;
}

View File

@@ -57,7 +57,7 @@ public final class WebRtcControls {
}
boolean displayGroupMembersButton() {
return groupCallState == GroupCallState.CONNECTED;
return groupCallState.isAtLeast(GroupCallState.CONNECTING);
}
boolean displayEndCall() {
@@ -156,8 +156,12 @@ public final class WebRtcControls {
public enum GroupCallState {
NONE,
DISCONNECTED,
RECONNECTING,
CONNECTING,
CONNECTED,
RECONNECTING
CONNECTED;
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull GroupCallState other) {
return compareTo(other) >= 0;
}
}
}

View File

@@ -1,18 +1,30 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
private final ImageView videoMuted;
private final ImageView audioMuted;
public CallParticipantViewHolder(@NonNull View itemView) {
super(itemView, null);
videoMuted = itemView.findViewById(R.id.call_participant_video_muted);
audioMuted = itemView.findViewById(R.id.call_participant_audio_muted);
}
@Override
public void bind(@NonNull CallParticipantViewState model) {
super.bind(model);
videoMuted.setVisibility(model.getVideoMutedVisibility());
audioMuted.setVisibility(model.getAudioMutedVisibility());
}
}

View File

@@ -1,7 +1,11 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
@@ -18,4 +22,18 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
public @NonNull Recipient getRecipient() {
return callParticipant.getRecipient();
}
@Override
public @NonNull String getName(@NonNull Context context) {
return callParticipant.getRecipient().isSelf() ? context.getString(R.string.GroupMembersDialog_you)
: super.getName(context);
}
public int getVideoMutedVisibility() {
return callParticipant.isVideoEnabled() ? View.GONE : View.VISIBLE;
}
public int getAudioMutedVisibility() {
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
}
}

View File

@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.IasTrustStore;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
@@ -69,7 +70,7 @@ class ContactDiscoveryV2 {
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException |InvalidKeyException e) {
Log.w(TAG, "Attestation error.", e);
throw new IOException(e);
}

View File

@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
@@ -242,6 +243,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
@@ -413,12 +415,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
long threadId)
{
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId.serialize());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.setAction(Intent.ACTION_DEFAULT);
return intent;
}
public static @NonNull RecipientId getRecipientId(@NonNull Intent intent) {
return RecipientId.from(Objects.requireNonNull(intent.getStringExtra(RECIPIENT_EXTRA)));
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -427,7 +434,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
protected void onCreate(Bundle state, boolean ready) {
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
RecipientId recipientId = getRecipientId(getIntent());
if (recipientId == null) {
Log.w(TAG, "[onCreate] Missing recipientId!");
@@ -437,7 +444,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
reportShortcutLaunch(recipientId);
setContentView(R.layout.conversation_activity);
getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
@@ -505,7 +512,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
silentlySetComposeText("");
}
RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
RecipientId recipientId = getRecipientId(intent);
if (recipientId == null) {
Log.w(TAG, "[onNewIntent] Missing recipientId!");
@@ -515,6 +522,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
reportShortcutLaunch(recipientId);
setIntent(intent);
initializeResources();
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
@@ -559,6 +567,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
ConversationUtil.pushShortcutForRecipient(getApplicationContext(), recipientSnapshot);
}
@Override
@@ -743,6 +753,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
}
private void reportShortcutLaunch(@NonNull RecipientId recipientId) {
if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
return;
}
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(this);
if (shortcutManager != null) {
shortcutManager.reportShortcutUsed(ConversationUtil.getShortcutId(recipientId));
}
}
private void handleImageFromDeviceCameraApp() {
if (attachmentManager.getCaptureUri() == null) {
Log.w(TAG, "No image available.");
@@ -1955,7 +1976,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
recipient.removeObservers(this);
}
recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
recipient = Recipient.live(getRecipientId(getIntent()));
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
glideRequests = GlideApp.with(this);
@@ -1963,7 +1984,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
recipient.observe(this, this::onRecipientChanged);
}
private void initializeLinkPreviewObserver() {
linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);

View File

@@ -61,7 +61,6 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.collect.Sets;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.LoggingFragment;
@@ -125,6 +124,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
@@ -480,7 +480,7 @@ public class ConversationFragment extends LoggingFragment {
int startingPosition = getStartPosition();
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
this.recipient = Recipient.live(ConversationActivity.getRecipientId(requireActivity().getIntent()));
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
@@ -1419,6 +1419,11 @@ public class ConversationFragment extends LoggingFragment {
public void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients) {
GroupsV1MigrationInfoBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
}
@Override
public void onJoinGroupCallClicked() {
CommunicationActions.startVideoCall(requireActivity(), recipient.get());
}
}
@Override
@@ -1498,8 +1503,8 @@ public class ConversationFragment extends LoggingFragment {
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(conversationMessage)); return true;
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(conversationMessage)); return true;
case R.id.action_delete: handleDeleteMessages(SetUtil.newHashSet(conversationMessage)); return true;
case R.id.action_copy: handleCopyMessage(SetUtil.newHashSet(conversationMessage)); return true;
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
case R.id.action_forward: handleForwardMessage(conversationMessage); return true;

View File

@@ -84,7 +84,7 @@ public class ConversationPopupActivity extends ConversationActivity {
public void onSuccess(Long result) {
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId());
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId().serialize());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
startActivity(intent, transition.toBundle());

View File

@@ -10,6 +10,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -26,13 +27,17 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout
@@ -109,7 +114,7 @@ public final class ConversationUpdateItem extends LinearLayout
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription);
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color));
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
present(conversationMessage, nextMessageRecord);
@@ -176,6 +181,28 @@ public final class ConversationUpdateItem extends LinearLayout
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationEventInvites());
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody());
Collection<UUID> uuids = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(uuids)) {
text = uuids.contains(TextSecurePreferences.getLocalUuid(getContext())) ? R.string.ConversationUpdateItem_return_to_call : R.string.ConversationUpdateItem_join_call;
}
if (text != 0) {
actionButton.setText(text);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onJoinGroupCallClicked();
}
});
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);

View File

@@ -29,6 +29,7 @@ import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
@@ -185,14 +186,14 @@ public final class ConversationListItem extends RelativeLayout
this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
this.subjectView.setTextColor(thread.isRead() ? ContextCompat.getColor(getContext(), R.color.signal_text_secondary)
: ContextCompat.getColor(getContext(), R.color.signal_inverse_primary));
: ContextCompat.getColor(getContext(), R.color.signal_text_primary));
if (thread.getDate() > 0) {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
dateView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
dateView.setTextColor(thread.isRead() ? ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary)
: ContextCompat.getColor(getContext(), R.color.signal_inverse_primary));
dateView.setTextColor(thread.isRead() ? ContextCompat.getColor(getContext(), R.color.signal_text_secondary)
: ContextCompat.getColor(getContext(), R.color.signal_text_primary));
}
if (thread.isArchived()) {
@@ -426,46 +427,51 @@ public final class ConversationListItem extends RelativeLayout
}
private static @NonNull LiveData<SpannableString> getThreadDisplayBody(@NonNull Context context, @NonNull ThreadRecord thread) {
int defaultTint = thread.isRead() ? ContextCompat.getColor(context, R.color.signal_text_secondary)
: ContextCompat.getColor(context, R.color.signal_text_primary);
if (!thread.isMessageRequestAccepted()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request), defaultTint);
} else if (SmsDatabase.Types.isGroupUpdate(thread.getType())) {
if (thread.getRecipient().isPushV2Group()) {
return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody()));
return emphasisAdded(context, MessageRecord.getGv2ChangeDescription(context, thread.getBody()), defaultTint);
} else {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_group_updated));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_group_updated), defaultTint);
}
} else if (SmsDatabase.Types.isGroupQuit(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group), defaultTint);
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message));
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint);
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message), defaultTint);
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session), defaultTint);
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_secure_session_reset));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_secure_session_reset), defaultTint);
} else if (MmsSmsColumns.Types.isLegacyType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
return emphasisAdded(context, context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported), defaultTint);
} else if (MmsSmsColumns.Types.isDraftMessageType(thread.getType())) {
String draftText = context.getString(R.string.ThreadRecord_draft);
return emphasisAdded(context, draftText + " " + thread.getBody());
return emphasisAdded(context, draftText + " " + thread.getBody(), defaultTint);
} else if (SmsDatabase.Types.isOutgoingAudioCall(thread.getType()) || SmsDatabase.Types.isOutgoingVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called), defaultTint);
} else if (SmsDatabase.Types.isIncomingAudioCall(thread.getType()) || SmsDatabase.Types.isIncomingVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called_you));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_called_you), defaultTint);
} else if (SmsDatabase.Types.isMissedAudioCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_audio_call));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_audio_call), defaultTint);
} else if (SmsDatabase.Types.isMissedVideoCall(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_missed_video_call), defaultTint);
} else if (MmsSmsColumns.Types.isGroupCall(thread.getType())) {
return emphasisAdded(context, MessageRecord.getGroupCallUpdateDescription(context, thread.getBody()), defaultTint);
} else if (SmsDatabase.Types.isJoinedType(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context)))));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(thread.getType())) {
int seconds = (int)(thread.getExpiresIn() / 1000);
if (seconds <= 0) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled), defaultTint);
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time), defaultTint);
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
if (r.isGroup()) {
@@ -475,19 +481,19 @@ public final class ConversationListItem extends RelativeLayout
}
}));
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_verified));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_verified), defaultTint);
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_unverified));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_unverified), defaultTint);
} else if (SmsDatabase.Types.isUnsupportedMessageType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed));
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint);
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
return emphasisAdded(context, "");
return emphasisAdded(context, "", defaultTint);
} else {
ThreadDatabase.Extra extra = thread.getExtra();
if (extra != null && extra.isViewOnce()) {
return emphasisAdded(context, getViewOnceDescription(context, thread.getContentType()));
return emphasisAdded(context, getViewOnceDescription(context, thread.getContentType()), defaultTint);
} 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));
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())));
}
@@ -506,12 +512,12 @@ public final class ConversationListItem extends RelativeLayout
}
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull String string) {
return emphasisAdded(context, UpdateDescription.staticDescription(string, 0));
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull String string, @ColorInt int defaultTint) {
return emphasisAdded(context, UpdateDescription.staticDescription(string, 0), defaultTint);
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull UpdateDescription description) {
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description));
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull UpdateDescription description, @ColorInt int defaultTint) {
return emphasisAdded(LiveUpdateMessage.fromMessageDescription(context, description, defaultTint));
}
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull LiveData<Spannable> description) {

View File

@@ -483,6 +483,11 @@ public final class GroupDatabase extends Database {
/**
* Migrates a V1 group to a V2 group.
*
* @param decryptedGroup The state that represents the group on the server. This will be used to
* determine if we need to save our old membership list and stuff. It will
* *not* be stored as the definitive group state as-is. In order to ensure
* proper diffing, we modify this model to have our V1 membership.
*/
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
@@ -513,7 +518,7 @@ public final class GroupDatabase extends Database {
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
update(groupMasterKey, decryptedGroup);
update(groupMasterKey, updateToHaveV1Membership(decryptedGroup, record.getMembers()));
db.setTransactionSuccessful();
} finally {
@@ -523,6 +528,23 @@ public final class GroupDatabase extends Database {
return groupIdV2;
}
private static DecryptedGroup updateToHaveV1Membership(@NonNull DecryptedGroup serverGroup, @NonNull List<RecipientId> v1Members) {
DecryptedGroup.Builder builder = serverGroup.toBuilder();
builder.clearMembers();
for (RecipientId v1MemberId : v1Members) {
Recipient v1Member = Recipient.resolved(v1MemberId);
if (v1Member.hasUuid()) {
builder.addMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(v1Member.getUuid().get()))
.setRole(Member.Role.ADMINISTRATOR)
.build());
}
}
return builder.build();
}
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
update(GroupId.v2(groupMasterKey), decryptedGroup);
}

View File

@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
import org.thoughtcrime.securesms.insights.InsightsConstants;
import org.thoughtcrime.securesms.logging.Log;
@@ -50,6 +49,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
public abstract class MessageDatabase extends Database implements MmsSmsColumns {
@@ -130,6 +130,12 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer);
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer);
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer);
public abstract @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
@NonNull RecipientId sender,
long timestamp,
@Nullable String messageGroupCallEraId,
@Nullable String peekGroupCallEraId,
@NonNull Collection<UUID> peekJoinedUuids);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);

View File

@@ -92,6 +92,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
@@ -397,6 +398,17 @@ public class MmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException();
}
@Override
public @NonNull void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
@NonNull RecipientId sender,
long timestamp,
@Nullable String messageGroupCallEraId,
@Nullable String peekGroupCallEraId,
@NonNull Collection<UUID> peekJoinedUuids)
{
throw new UnsupportedOperationException();
}
@Override
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
throw new UnsupportedOperationException();

View File

@@ -44,6 +44,7 @@ public interface MmsSmsColumns {
protected static final long GV1_MIGRATION_TYPE = 9;
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
protected static final long GROUP_CALL_TYPE = 12;
protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21;
@@ -214,7 +215,8 @@ public interface MmsSmsColumns {
isOutgoingAudioCall(type) ||
isOutgoingVideoCall(type) ||
isMissedAudioCall(type) ||
isMissedVideoCall(type);
isMissedVideoCall(type) ||
isGroupCall(type);
}
public static boolean isExpirationTimerUpdate(long type) {
@@ -237,7 +239,6 @@ public interface MmsSmsColumns {
return type == OUTGOING_VIDEO_CALL_TYPE;
}
public static boolean isMissedAudioCall(long type) {
return type == MISSED_AUDIO_CALL_TYPE;
}
@@ -246,6 +247,10 @@ public interface MmsSmsColumns {
return type == MISSED_VIDEO_CALL_TYPE;
}
public static boolean isGroupCall(long type) {
return type == GROUP_CALL_TYPE;
}
public static boolean isGroupUpdate(long type) {
return (type & GROUP_UPDATE_BIT) != 0;
}

View File

@@ -35,9 +35,11 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
@@ -57,6 +59,7 @@ import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -69,7 +72,9 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* Database for storage of SMS messages.
@@ -666,6 +671,89 @@ public class SmsDatabase extends MessageDatabase {
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
}
@Override
public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
@NonNull RecipientId sender,
long timestamp,
@Nullable String messageGroupCallEraId,
@Nullable String peekGroupCallEraId,
@NonNull Collection<UUID> peekJoinedUuids)
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
try {
db.beginTransaction();
Recipient recipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids);
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
byte[] updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(Util.emptyIfNull(peekGroupCallEraId))
.setStartedCallUuid(Recipient.resolved(sender).requireUuid().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList())
.build()
.toByteArray();
String body = Base64.encodeBytes(updateDetails);
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, sender.serialize());
values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, timestamp);
values.put(DATE_SENT, timestamp);
values.put(READ, 0);
values.put(BODY, body);
values.put(TYPE, Types.GROUP_CALL_TYPE);
values.put(THREAD_ID, threadId);
db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
}
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
private boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection<UUID> peekJoinedUuids) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = TYPE + " = ? AND " + THREAD_ID + " = ?";
String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId);
try (Reader reader = new Reader(db.query(TABLE_NAME, MESSAGE_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) {
MessageRecord record = reader.getNext();
if (record == null) {
return false;
}
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody());
boolean sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId);
List<String> inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList()
: Collections.emptyList();
String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids);
ContentValues contentValues = new ContentValues();
contentValues.put(BODY, body);
db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId()));
return sameEraId;
}
}
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
Recipient recipient = Recipient.resolved(recipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);

View File

@@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -245,6 +246,7 @@ public class ThreadDatabase extends Database {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, ID_WHERE, new String[] {threadId + ""});
notifyConversationListListeners();
ConversationUtil.clearShortcuts(context, Collections.singleton(threadId));
}
private void deleteThreads(Set<Long> threadIds) {
@@ -259,12 +261,14 @@ public class ThreadDatabase extends Database {
db.delete(TABLE_NAME, where, null);
notifyConversationListListeners();
ConversationUtil.clearShortcuts(context, threadIds);
}
private void deleteAllThreads() {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
notifyConversationListListeners();
ConversationUtil.clearAllShortcuts(context);
}
public void trimAllThreads(int length, long trimBeforeDate) {
@@ -1194,6 +1198,10 @@ public class ThreadDatabase extends Database {
}
}
public @NonNull ThreadRecord getThreadRecordFor(@NonNull Recipient recipient) {
return Objects.requireNonNull(getThreadRecord(getThreadIdFor(recipient)));
}
@NonNull MergeResult merge(@NonNull RecipientId primaryRecipientId, @NonNull RecipientId secondaryRecipientId) {
if (!databaseHelper.getWritableDatabase().inTransaction()) {
throw new IllegalStateException("Must be in a transaction!");

View File

@@ -164,6 +164,10 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isMissedVideoCall(type);
}
public final boolean isGroupCall() {
return SmsDatabase.Types.isGroupCall(type);
}
public boolean isVerificationStatusChange() {
return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type);
}

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.List;
public final class GroupCallUpdateDetailsUtil {
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
private GroupCallUpdateDetailsUtil() {
}
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetails.getDefaultInstance();
if (body == null) {
return groupCallUpdateDetails;
}
try {
groupCallUpdateDetails = GroupCallUpdateDetails.parseFrom(Base64.decode(body));
} catch (IOException e) {
Log.w(TAG, "Group call update details could not be read", e);
}
return groupCallUpdateDetails;
}
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids) {
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.toBuilder()
.clearInCallUuids();
if (Util.hasItems(inCallUuids)) {
builder.addAllInCallUuids(inCallUuids);
}
return Base64.encodeBytes(builder.build().toByteArray());
}
}

View File

@@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
/**
* Create a group call update message based on time and joined members.
*/
public class GroupCallUpdateMessageFactory implements UpdateDescription.StringFactory {
private final Context context;
private final List<UUID> joinedMembers;
private final GroupCallUpdateDetails groupCallUpdateDetails;
private final UUID selfUuid;
public GroupCallUpdateMessageFactory(@NonNull Context context, @NonNull List<UUID> joinedMembers, @NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
this.context = context;
this.joinedMembers = new ArrayList<>(joinedMembers);
this.groupCallUpdateDetails = groupCallUpdateDetails;
this.selfUuid = TextSecurePreferences.getLocalUuid(context);
boolean removed = this.joinedMembers.remove(selfUuid);
if (removed) {
this.joinedMembers.add(selfUuid);
}
}
@Override
public @NonNull String create() {
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.getStartedCallTimestamp());
switch (joinedMembers.size()) {
case 0:
return context.getString(R.string.MessageRecord_group_call_s, time);
case 1:
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.getStartedCallUuid())) {
return context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time);
} else if (Objects.equals(joinedMembers.get(0), selfUuid)) {
return context.getString(R.string.MessageRecord_you_are_in_the_group_call_s, describe(joinedMembers.get(0)), time);
} else {
return context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time);
}
case 2:
return context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
time);
default:
int others = joinedMembers.size() - 2;
return context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
others,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
others,
time);
}
}
private @NonNull String describe(@NonNull UUID uuid) {
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
return context.getString(R.string.MessageRecord_unknown);
}
Recipient recipient = Recipient.resolved(RecipientId.from(uuid, null));
if (recipient.isSelf()) {
return context.getString(R.string.MessageRecord_you);
} else {
return recipient.getShortDisplayName(context);
}
}
}

View File

@@ -8,6 +8,7 @@ import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
@@ -32,9 +33,9 @@ public final class LiveUpdateMessage {
* recreates the string asynchronously when they change.
*/
@AnyThread
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription) {
public static LiveData<Spannable> fromMessageDescription(@NonNull Context context, @NonNull UpdateDescription updateDescription, @ColorInt int defaultTint) {
if (updateDescription.isStringStatic()) {
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString()));
return LiveDataUtil.just(toSpannable(context, updateDescription, updateDescription.getStaticString(), defaultTint));
}
List<LiveData<Recipient>> allMentionedRecipients = Stream.of(updateDescription.getMentioned())
@@ -44,7 +45,7 @@ public final class LiveUpdateMessage {
LiveData<?> mentionedRecipientChangeStream = allMentionedRecipients.isEmpty() ? LiveDataUtil.just(new Object())
: LiveDataUtil.merge(allMentionedRecipients);
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString()));
return LiveDataUtil.mapAsync(mentionedRecipientChangeStream, event -> toSpannable(context, updateDescription, updateDescription.getString(), defaultTint));
}
/**
@@ -56,13 +57,13 @@ public final class LiveUpdateMessage {
return LiveDataUtil.mapAsync(Recipient.live(recipientId).getLiveData(), createStringInBackground);
}
private static @NonNull Spannable toSpannable(@NonNull Context context, @NonNull UpdateDescription updateDescription, @NonNull String string) {
private static @NonNull Spannable 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();
if (tint == 0) {
tint = ContextCompat.getColor(context, R.color.conversation_item_update_text_color);
tint = defaultTint;
}
if (drawableResource == 0) {

View File

@@ -28,6 +28,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
@@ -154,6 +157,8 @@ public abstract class MessageRecord extends DisplayRecord {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, getCallDateString(context)), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else if (isMissedVideoCall()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, getCallDateString(context)), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else if (isGroupCall()) {
return getGroupCallUpdateDescription(context, getBody());
} else if (isJoined()) {
return staticUpdateDescription(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().getDisplayName(context)), R.drawable.ic_update_group_add_16);
} else if (isExpirationTimerUpdate()) {
@@ -197,7 +202,7 @@ public abstract class MessageRecord extends DisplayRecord {
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, descriptionStrategy, Recipient.self().getUuid().get());
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
} else {
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
@@ -281,6 +286,19 @@ public abstract class MessageRecord extends DisplayRecord {
return context.getString(R.string.MessageRecord_changed_their_profile, getIndividualRecipient().getDisplayName(context));
}
public static @NonNull UpdateDescription getGroupCallUpdateDescription(@NonNull Context context, @NonNull String body) {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(body);
List<UUID> joinedMembers = Stream.of(groupCallUpdateDetails.getInCallUuidsList())
.map(UuidUtil::parseOrNull)
.withoutNulls()
.toList();
UpdateDescription.StringFactory stringFactory = new GroupCallUpdateMessageFactory(context, joinedMembers, groupCallUpdateDetails);
return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16);
}
/**
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
*/

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.JobMigrator;
import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.jobs.MarkerJob;
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
@@ -146,7 +147,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(context), SignalExecutors.newCachedSingleThreadExecutor("signal-fast-job-storage")))
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
.addReservedJobRunner(new FactoryJobPredicate(PushDecryptMessageJob.KEY, PushProcessMessageJob.KEY, MarkerJob.KEY))
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY))
.addReservedJobRunner(new FactoryJobPredicate(PushTextSendJob.KEY, PushMediaSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY, GroupCallUpdateSendJob.KEY))
.build());
}

View File

@@ -12,7 +12,7 @@ import java.util.Objects;
public class CallParticipant {
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false);
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false);
private final @NonNull CameraState cameraState;
private final @NonNull Recipient recipient;
@@ -36,9 +36,10 @@ public class CallParticipant {
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
@NonNull BroadcastVideoSink renderer,
boolean audioEnabled,
boolean videoEnabled)
{
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled);
}
private CallParticipant(@NonNull Recipient recipient,

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
@@ -120,10 +121,15 @@ public class AddGroupDetailsFragment extends LoggingFragment {
toolbar.setTitle(isMms ? R.string.AddGroupDetailsFragment__create_group : R.string.AddGroupDetailsFragment__name_this_group);
});
viewModel.getNonGv2CapableMembers().observe(getViewLifecycleOwner(), nonGv2CapableMembers -> {
boolean forcedMigration = FeatureFlags.groupsV1ForcedMigration();
int stringRes = forcedMigration ? R.plurals.AddGroupDetailsFragment__d_members_do_not_support_new_groups_so_this_group_cannot_be_created
: R.plurals.AddGroupDetailsFragment__d_members_do_not_support_new_groups;
gv2Warning.setVisibility(nonGv2CapableMembers.isEmpty() ? View.GONE : View.VISIBLE);
gv2Warning.setText(requireContext().getResources().getQuantityString(R.plurals.AddGroupDetailsFragment__d_members_do_not_support_new_groups, nonGv2CapableMembers.size(), nonGv2CapableMembers.size()));
gv2Warning.setText(requireContext().getResources().getQuantityString(stringRes, nonGv2CapableMembers.size(), nonGv2CapableMembers.size()));
gv2Warning.setLearnMoreVisible(true);
gv2Warning.setOnLinkClickListener(v -> NonGv2MemberDialog.showNonGv2Members(requireContext(), nonGv2CapableMembers));
gv2Warning.setOnLinkClickListener(v -> NonGv2MemberDialog.showNonGv2Members(requireContext(), nonGv2CapableMembers, forcedMigration));
});
viewModel.getAvatar().observe(getViewLifecycleOwner(), avatarBytes -> {
if (avatarBytes == null) {

View File

@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
@@ -62,7 +63,8 @@ public final class AddGroupDetailsViewModel extends ViewModel {
});
nonGv2CapableMembers = LiveDataUtil.mapAsync(membersToCheckGv2CapabilityOf, memberList -> repository.checkCapabilities(Stream.of(memberList).map(newGroupCandidate -> newGroupCandidate.getMember().getId()).toList()));
canSubmitForm = LiveDataUtil.combineLatest(isMms, isValidName, (mms, validName) -> mms || validName);
canSubmitForm = FeatureFlags.groupsV1ForcedMigration() ? LiveDataUtil.just(false)
: LiveDataUtil.combineLatest(isMms, isValidName, (mms, validName) -> mms || validName);
repository.resolveMembers(recipientIds, initialMembers::postValue);
}

View File

@@ -20,7 +20,7 @@ public final class NonGv2MemberDialog {
private NonGv2MemberDialog() {
}
public static @Nullable Dialog showNonGv2Members(@NonNull Context context, @NonNull List<Recipient> recipients) {
public static @Nullable Dialog showNonGv2Members(@NonNull Context context, @NonNull List<Recipient> recipients, boolean forcedMigration) {
int size = recipients.size();
if (size == 0) {
return null;
@@ -32,9 +32,13 @@ public final class NonGv2MemberDialog {
// })
.setPositiveButton(android.R.string.ok, null);
if (size == 1) {
builder.setMessage(context.getString(R.string.NonGv2MemberDialog_single_users_are_non_gv2_capable, recipients.get(0).getDisplayName(context)));
int stringRes = forcedMigration ? R.string.NonGv2MemberDialog_single_users_are_non_gv2_capable_forced_migration
: R.string.NonGv2MemberDialog_single_users_are_non_gv2_capable;
builder.setMessage(context.getString(stringRes, recipients.get(0).getDisplayName(context)));
} else {
builder.setMessage(context.getResources().getQuantityString(R.plurals.NonGv2MemberDialog_d_users_are_non_gv2_capable, size, size))
int pluralRes = forcedMigration ? R.plurals.NonGv2MemberDialog_d_users_are_non_gv2_capable_forced_migration
: R.plurals.NonGv2MemberDialog_d_users_are_non_gv2_capable;
builder.setMessage(context.getResources().getQuantityString(pluralRes, size, size))
.setView(R.layout.dialog_multiple_members_non_gv2_capable);
}

View File

@@ -0,0 +1,174 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
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.GroupUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Send a group call update message to every one in a V2 group. Used to indicate you
* have joined or left a call.
*/
public class GroupCallUpdateSendJob extends BaseJob {
public static final String KEY = "GroupCallUpdateSendJob";
private static final String TAG = Log.tag(GroupCallUpdateSendJob.class);
private static final String KEY_RECIPIENT_ID = "recipient_id";
private static final String KEY_ERA_ID = "era_id";
private static final String KEY_RECIPIENTS = "recipients";
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
private final RecipientId recipientId;
private final String eraId;
private final List<RecipientId> recipients;
private final int initialRecipientCount;
@WorkerThread
public static @NonNull GroupCallUpdateSendJob create(@NonNull RecipientId recipientId, @Nullable String eraId) {
Recipient conversationRecipient = Recipient.resolved(recipientId);
if (!conversationRecipient.isPushV2Group()) {
throw new AssertionError("We have a recipient, but it's not a V2 Group");
}
List<RecipientId> recipients = Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants()))
.filterNot(Recipient::isSelf)
.map(Recipient::getId)
.toList();
return new GroupCallUpdateSendJob(recipientId,
eraId,
recipients,
recipients.size(),
new Parameters.Builder()
.setQueue(conversationRecipient.getId().toQueueKey())
.setLifespan(TimeUnit.MINUTES.toMillis(5))
.setMaxAttempts(3)
.build());
}
private GroupCallUpdateSendJob(@NonNull RecipientId recipientId,
@NonNull String eraId,
@NonNull List<RecipientId> recipients,
int initialRecipientCount,
@NonNull Parameters parameters)
{
super(parameters);
this.recipientId = recipientId;
this.eraId = eraId;
this.recipients = recipients;
this.initialRecipientCount = initialRecipientCount;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
.putString(KEY_ERA_ID, eraId)
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws Exception {
Recipient conversationRecipient = Recipient.resolved(recipientId);
if (!conversationRecipient.isPushV2Group()) {
throw new AssertionError("We have a recipient, but it's not a V2 Group");
}
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> completions = deliver(conversationRecipient, destinations);
for (Recipient completion : completions) {
recipients.remove(completion.getId());
}
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
if (!recipients.isEmpty()) {
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
throw new RetryLaterException();
}
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof IOException ||
e instanceof RetryLaterException;
}
@Override
public void onFailure() {
if (recipients.size() < initialRecipientCount) {
Log.w(TAG, "Only sent a group update to " + recipients.size() + "/" + initialRecipientCount + " recipients. Still, it sent to someone, so it stays.");
return;
}
Log.w(TAG, "Failed to send the group update to all recipients!");
}
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations)
throws IOException, UntrustedIdentityException
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withGroupCallUpdate(new SignalServiceDataMessage.GroupCallUpdate(eraId));
if (conversationRecipient.isGroup()) {
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
}
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
return GroupSendJobHelper.getCompletedSends(context, results);
}
public static class Factory implements Job.Factory<GroupCallUpdateSendJob> {
@Override
public @NonNull
GroupCallUpdateSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT_ID));
String eraId = data.getString(KEY_ERA_ID);
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
return new GroupCallUpdateSendJob(recipientId, eraId, recipients, initialRecipientCount, parameters);
}
}
}

View File

@@ -70,6 +70,7 @@ public final class JobManagerFactories {
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());

View File

@@ -84,6 +84,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -373,14 +374,15 @@ public final class PushProcessMessageJob extends BaseJob {
}
}
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
else if (FeatureFlags.groupCalling() && message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId);
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
handleUnknownGroupMessage(content, message.getGroupContext().get());
@@ -649,6 +651,28 @@ public final class PushProcessMessageJob extends BaseJob {
context.startService(intent);
}
private void handleGroupCallUpdateMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<GroupId> groupId)
{
if (!groupId.isPresent() || !groupId.get().isV2()) {
Log.w(TAG, "Invalid group for group call update message");
return;
}
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(groupId.get());
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_GROUP_CALL_UPDATE_MESSAGE)
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_SENDER, RecipientId.from(content.getSender()).serialize())
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_GROUP, groupRecipientId.serialize())
.putExtra(WebRtcCallService.EXTRA_GROUP_CALL_ERA_ID, message.getGroupCallUpdate().get().getEraId())
.putExtra(WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP, content.getServerReceivedTimestamp());
context.startService(intent);
}
private void handleEndSessionMessage(@NonNull SignalServiceContent content,
@NonNull Optional<Long> smsMessageId)
{
@@ -953,6 +977,13 @@ public final class PushProcessMessageJob extends BaseJob {
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
if (message.getMessage().isGroupV2Message()) {
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey()));
if (possibleGv1.isPresent()) {
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1());
}
}
long threadId = -1;
if (message.isRecipientUpdate()) {
@@ -965,6 +996,8 @@ public final class PushProcessMessageJob extends BaseJob {
} else if (message.getMessage().isGroupV2Update()) {
handleSynchronizeSentGv2Update(content, message);
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
} else if (message.getMessage().isEmptyGroupV2Message()) {
// Do nothing
} else if (message.getMessage().isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(message);
} else if (message.getMessage().getReaction().isPresent()) {

View File

@@ -9,12 +9,15 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
@@ -32,16 +35,18 @@ public class SendReadReceiptJob extends BaseJob {
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
private static final int MAX_TIMESTAMPS = 500;
private static final String KEY_THREAD = "thread";
private static final String KEY_ADDRESS = "address";
private static final String KEY_RECIPIENT = "recipient";
private static final String KEY_MESSAGE_IDS = "message_ids";
private static final String KEY_TIMESTAMP = "timestamp";
private long threadId;
private RecipientId recipientId;
private List<Long> messageIds;
private long timestamp;
private final long threadId;
private final RecipientId recipientId;
private final List<Long> messageIds;
private final long timestamp;
public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List<Long> messageIds) {
this(new Job.Parameters.Builder()
@@ -51,7 +56,7 @@ public class SendReadReceiptJob extends BaseJob {
.build(),
threadId,
recipientId,
messageIds,
ensureSize(messageIds, MAX_TIMESTAMPS),
System.currentTimeMillis());
}
@@ -69,6 +74,23 @@ public class SendReadReceiptJob extends BaseJob {
this.timestamp = timestamp;
}
/**
* Enqueues all the necessary jobs for read receipts, ensuring that they're all within the
* maximum size.
*/
public static void enqueue(long threadId, @NonNull RecipientId recipientId, List<Long> messageIds) {
JobManager jobManager = ApplicationDependencies.getJobManager();
List<List<Long>> messageIdChunks = Util.chunk(messageIds, MAX_TIMESTAMPS);
if (messageIdChunks.size() > 1) {
Log.w(TAG, "Large receipt count! Had to break into multiple chunks. Total count: " + messageIds.size());
}
for (List<Long> chunk : messageIdChunks) {
jobManager.add(new SendReadReceiptJob(threadId, recipientId, chunk));
}
}
@Override
public @NonNull Data serialize() {
long[] ids = new long[messageIds.size()];
@@ -128,6 +150,13 @@ public class SendReadReceiptJob extends BaseJob {
Log.w(TAG, "Failed to send read receipts to: " + recipientId);
}
private static <E> List<E> ensureSize(@NonNull List<E> list, int maxSize) {
if (list.size() > maxSize) {
throw new IllegalArgumentException("Too large! Size: " + list.size() + ", maxSize: " + maxSize);
}
return list;
}
public static final class Factory implements Job.Factory<SendReadReceiptJob> {
private final Application application;

View File

@@ -1,34 +1,29 @@
package org.thoughtcrime.securesms.linkpreview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Html;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Stream;
import com.google.android.collect.Sets;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
@@ -50,7 +45,7 @@ public final class LinkPreviewUtil {
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>");
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"");
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = Sets.newHashSet("onion", "i2p");
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p");
/**
* @return All whitelisted URLs in the source text.

View File

@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
@@ -75,11 +76,7 @@ public final class SignalPinReminderDialog {
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
pinEditText.post(() -> {
if (pinEditText.requestFocus()) {
ServiceUtil.getInputMethodManager(pinEditText.getContext()).showSoftInput(pinEditText, 0);
}
});
ViewUtil.focusAndShowKeyboard(pinEditText);
ViewCompat.setAutofillHints(pinEditText, HintConstants.AUTOFILL_HINT_PASSWORD);
switch (SignalStore.pinValues().getKeyboardType()) {

View File

@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
@@ -29,7 +30,7 @@ final class ConfirmKbsPinRepository {
Log.i(TAG, "Pin set on KBS");
return PinSetResult.SUCCESS;
} catch (IOException | UnauthenticatedResponseException e) {
} catch (IOException | UnauthenticatedResponseException | InvalidKeyException e) {
Log.w(TAG, e);
PinState.onPinCreateFailure();
return PinSetResult.FAILURE;

View File

@@ -26,7 +26,6 @@ import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.camera.view.SignalCameraView;
@@ -47,8 +46,6 @@ import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.video.VideoUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -232,7 +229,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
camera.setCaptureMode(SignalCameraView.CaptureMode.MIXED);
int maxDuration = VideoUtil.getMaxVideoDurationInSeconds(requireContext(), viewModel.getMediaConstraints());
int maxDuration = VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), viewModel.getMediaConstraints());
Log.d(TAG, "Max duration: " + maxDuration + " sec");
captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper(
@@ -269,7 +266,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
"API: " + Build.VERSION.SDK_INT + ", " +
"MFD: " + MemoryFileDescriptor.supported() + ", " +
"Camera: " + CameraXUtil.getLowestSupportedHardwareLevel(requireContext()) + ", " +
"MaxDuration: " + VideoUtil.getMaxVideoDurationInSeconds(requireContext(), viewModel.getMediaConstraints()) + " sec");
"MaxDuration: " + VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), viewModel.getMediaConstraints()) + " sec");
}
viewModel.onCameraControlsInitialized();
@@ -280,7 +277,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
requireArguments().getBoolean(IS_VIDEO_ENABLED, true) &&
MediaConstraints.isVideoTranscodeAvailable() &&
CameraXUtil.isMixedModeSupported(context) &&
VideoUtil.getMaxVideoDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
VideoUtil.getMaxVideoRecordDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
}
private void displayVideoRecordingTooltipIfNecessary(CameraButtonView captureButton) {

View File

@@ -21,9 +21,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.util.Pair;
import androidx.core.util.Supplier;
import androidx.fragment.app.Fragment;
@@ -149,7 +147,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
private TextView charactersLeft;
private RecyclerView mediaRail;
private MediaRailAdapter mediaRailAdapter;
private AlertDialog progressDialog;
private int visibleHeight;
@@ -259,7 +256,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
} else if (!Util.isEmpty(media)) {
viewModel.onSelectedMediaChanged(this, media);
Fragment fragment = MediaSendFragment.newInstance(Locale.getDefault());
Fragment fragment = MediaSendFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
.commit();
@@ -309,7 +306,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
sendButton.setTransport(transport);
sendButton.disableTransport(transport.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
countButton.setOnClickListener(v -> navigateToMediaSend(Locale.getDefault()));
countButton.setOnClickListener(v -> navigateToMediaSend());
composeText.append(viewModel.getBody());
@@ -371,7 +368,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
@Override
public void onMediaSelected(@NonNull Media media) {
viewModel.onSingleMediaSelected(this, media);
navigateToMediaSend(Locale.getDefault());
navigateToMediaSend();
}
@Override
@@ -461,7 +458,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
viewModel.onMediaCaptured(media);
navigateToMediaSend(Locale.getDefault());
navigateToMediaSend();
});
}
@@ -472,7 +469,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
@Override
public void onCameraCountButtonClicked() {
navigateToMediaSend(Locale.getDefault());
navigateToMediaSend();
}
@Override
@@ -549,7 +546,11 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
MediaSendFragment fragment = getMediaSendFragment();
if (fragment != null) {
fragment.pausePlayback();
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 300, 0);
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
dialog.dismiss();
finish();
});
} else {
@@ -570,7 +571,14 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
sendButton.setEnabled(false);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions()).observe(this, this::setActivityResultAndFinish);
fragment.pausePlayback();
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 300, 0);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions())
.observe(this, result -> {
dialog.dismiss();
setActivityResultAndFinish(result);
});
}
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
@@ -771,15 +779,6 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
.setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
.show(TooltipPopup.POSITION_ABOVE);
break;
case SHOW_RENDER_PROGRESS:
progressDialog = SimpleProgressDialog.show(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog));
break;
case HIDE_RENDER_PROGRESS:
if (progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
break;
}
});
}
@@ -842,8 +841,8 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
}
private void navigateToMediaSend(@NonNull Locale locale) {
MediaSendFragment fragment = MediaSendFragment.newInstance(locale);
private void navigateToMediaSend() {
MediaSendFragment fragment = MediaSendFragment.newInstance();
String backstackTag = null;
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {

View File

@@ -1,22 +1,23 @@
package org.thoughtcrime.securesms.mediasend;
import androidx.lifecycle.ViewModelProviders;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ControllableViewPager;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.Util;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
@@ -24,20 +25,14 @@ import java.util.Map;
*/
public class MediaSendFragment extends Fragment {
private static final String TAG = MediaSendFragment.class.getSimpleName();
private static final String KEY_LOCALE = "locale";
private ViewGroup playbackControlsContainer;
private ControllableViewPager fragmentPager;
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
private MediaSendViewModel viewModel;
public static MediaSendFragment newInstance(@NonNull Locale locale) {
public static MediaSendFragment newInstance() {
Bundle args = new Bundle();
args.putSerializable(KEY_LOCALE, locale);
MediaSendFragment fragment = new MediaSendFragment();
fragment.setArguments(args);
@@ -60,8 +55,7 @@ public class MediaSendFragment extends Fragment {
fragmentPager = view.findViewById(R.id.mediasend_pager);
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager(), viewModel.isSms() ? MediaConstraints.getMmsMediaConstraints(-1) : MediaConstraints.getPushMediaConstraints());
fragmentPager.setAdapter(fragmentPagerAdapter);
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
@@ -147,6 +141,10 @@ public class MediaSendFragment extends Fragment {
});
}
void pausePlayback() {
fragmentPagerAdapter.pausePlayback();
}
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
@Override
public void onPageSelected(int position) {

View File

@@ -1,16 +1,22 @@
package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.net.http.LoggingEventHandler;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -24,12 +30,14 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
private final List<Media> media;
private final Map<Integer, MediaSendPageFragment> fragments;
private final Map<Uri, Object> savedState;
private final MediaConstraints mediaConstraints;
MediaSendFragmentPagerAdapter(@NonNull FragmentManager fm) {
MediaSendFragmentPagerAdapter(@NonNull FragmentManager fm, @NonNull MediaConstraints mediaConstraints) {
super(fm);
this.media = new ArrayList<>();
this.fragments = new HashMap<>();
this.savedState = new HashMap<>();
this.mediaConstraints = mediaConstraints;
this.media = new ArrayList<>();
this.fragments = new HashMap<>();
this.savedState = new HashMap<>();
}
@Override
@@ -41,7 +49,9 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
return ImageEditorFragment.newInstance(mediaItem.getUri());
} else if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
return MediaSendVideoFragment.newInstance(mediaItem.getUri());
return MediaSendVideoFragment.newInstance(mediaItem.getUri(),
mediaConstraints.getCompressedVideoMaxSize(ApplicationDependencies.getApplication()),
mediaConstraints.getVideoMaxSize(ApplicationDependencies.getApplication()));
} else {
throw new UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.getMimeType() + "'");
}
@@ -122,6 +132,14 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
return fragments.containsKey(position) ? fragments.get(position).getPlaybackControls() : null;
}
void pausePlayback() {
for (MediaSendPageFragment fragment : fragments.values()) {
if (fragment instanceof MediaSendVideoFragment) {
((MediaSendVideoFragment)fragment).pausePlayback();
}
}
}
void notifyHidden() {
Stream.of(fragments.values()).forEach(MediaSendPageFragment::notifyHidden);
}

View File

@@ -18,8 +18,8 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Throttler;
import org.thoughtcrime.securesms.video.VideoBitRateCalculator;
import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException;
@@ -29,7 +29,9 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
private static final String TAG = Log.tag(MediaSendVideoFragment.class);
private static final String KEY_URI = "uri";
private static final String KEY_URI = "uri";
private static final String KEY_MAX_OUTPUT = "max_output_size";
private static final String KEY_MAX_SEND = "max_send_size";
private final Throttler videoScanThrottle = new Throttler(150);
private final Handler handler = new Handler();
@@ -41,9 +43,11 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
@Nullable private VideoEditorHud hud;
private Runnable updatePosition;
public static MediaSendVideoFragment newInstance(@NonNull Uri uri) {
public static MediaSendVideoFragment newInstance(@NonNull Uri uri, long maxCompressedVideoSize, long maxAttachmentSize) {
Bundle args = new Bundle();
args.putParcelable(KEY_URI, uri);
args.putLong(KEY_MAX_OUTPUT, maxCompressedVideoSize);
args.putLong(KEY_MAX_SEND, maxAttachmentSize);
MediaSendVideoFragment fragment = new MediaSendVideoFragment();
fragment.setArguments(args);
@@ -72,7 +76,9 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
player = view.findViewById(R.id.video_player);
uri = requireArguments().getParcelable(KEY_URI);
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
long maxOutput = requireArguments().getLong(KEY_MAX_OUTPUT);
long maxSend = requireArguments().getLong(KEY_MAX_SEND);
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
player.setWindow(requireActivity().getWindow());
player.setVideoSource(slide, true);
@@ -85,7 +91,7 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
player.clip(data.startTimeUs, data.endTimeUs, true);
}
try {
hud.setVideoSource(slide);
hud.setVideoSource(slide, new VideoBitRateCalculator(maxOutput), maxSend);
hud.setVisibility(View.VISIBLE);
startPositionUpdates();
} catch (IOException e) {
@@ -203,6 +209,10 @@ public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.E
@Override
public void notifyHidden() {
pausePlayback();
}
public void pausePlayback() {
if (player != null) {
player.pause();
if (hud != null) {

View File

@@ -460,15 +460,12 @@ class MediaSendViewModel extends ViewModel {
}
MutableLiveData<MediaSendActivityResult> result = new MutableLiveData<>();
Runnable dialogRunnable = () -> event.postValue(Event.SHOW_RENDER_PROGRESS);
String trimmedBody = isViewOnce() ? "" : body.toString().trim();
List<Media> initialMedia = getSelectedMediaOrDefault();
List<Mention> trimmedMentions = isViewOnce() ? Collections.emptyList() : mentions;
Preconditions.checkState(initialMedia.size() > 0, "No media to send!");
Util.runOnMainDelayed(dialogRunnable, 250);
MediaRepository.transformMedia(application, initialMedia, modelsToTransform, (oldToNew) -> {
List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
@@ -499,7 +496,6 @@ class MediaSendViewModel extends ViewModel {
uploadRepository.deleteAbandonedAttachments();
}
Util.cancelRunnableOnMain(dialogRunnable);
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce(), trimmedMentions));
});
});
@@ -676,12 +672,16 @@ class MediaSendViewModel extends ViewModel {
}
}
boolean isSms() {
return transport.isSms();
}
enum Error {
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS, ONLY_ITEM_TOO_LARGE
}
enum Event {
VIEW_ONCE_TOOLTIP, SHOW_RENDER_PROGRESS, HIDE_RENDER_PROGRESS
VIEW_ONCE_TOOLTIP
}
enum Page {

View File

@@ -22,9 +22,11 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivity;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ResearchMegaphone;
import org.thoughtcrime.securesms.util.PopulationFeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VersionTracker;
import java.util.LinkedHashMap;
import java.util.List;
@@ -88,7 +90,8 @@ public final class Megaphones {
put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER);
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.RESEARCH, shouldShowResearchMegaphone() ? ShowForDurationSchedule.showForDays(7) : NEVER);
put(Event.RESEARCH, shouldShowResearchMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER);
put(Event.DONATE, shouldShowDonateMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER);
}};
}
@@ -108,6 +111,8 @@ public final class Megaphones {
return buildClientDeprecatedMegaphone(context);
case RESEARCH:
return buildResearchMegaphone(context);
case DONATE:
return buildDonateMegaphone(context);
default:
throw new IllegalArgumentException("Event not handled!");
}
@@ -219,12 +224,31 @@ public final class Megaphones {
.build();
}
private static @NonNull Megaphone buildDonateMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.DONATE, Megaphone.Style.BASIC)
.disableSnooze()
.setTitle(R.string.DonateMegaphone_donate_to_signal)
.setBody(R.string.DonateMegaphone_Signal_is_powered_by_people_like_you_show_your_support_today)
.setImage(R.drawable.ic_donate_megaphone)
.setActionButton(R.string.DonateMegaphone_donate, (megaphone, controller) -> {
controller.onMegaphoneCompleted(megaphone.getEvent());
CommunicationActions.openBrowserLink(controller.getMegaphoneActivity(), context.getString(R.string.donate_url));
})
.setSecondaryButton(R.string.DonateMegaphone_no_thanks, (megaphone, controller) -> controller.onMegaphoneCompleted(megaphone.getEvent()))
.setPriority(Megaphone.Priority.DEFAULT)
.build();
}
private static boolean shouldShowMessageRequestsMegaphone() {
return Recipient.self().getProfileName() == ProfileName.EMPTY;
}
private static boolean shouldShowResearchMegaphone() {
return ResearchMegaphone.isInResearchMegaphone();
private static boolean shouldShowResearchMegaphone(@NonNull Context context) {
return VersionTracker.getDaysSinceFirstInstalled(context) > 7 && PopulationFeatureFlags.isInResearchMegaphone();
}
private static boolean shouldShowDonateMegaphone(@NonNull Context context) {
return VersionTracker.getDaysSinceFirstInstalled(context) > 7 && PopulationFeatureFlags.isInDonateMegaphone();
}
private static boolean shouldShowLinkPreviewsMegaphone(@NonNull Context context) {
@@ -238,7 +262,8 @@ public final class Megaphones {
MESSAGE_REQUESTS("message_requests"),
LINK_PREVIEWS("link_previews"),
CLIENT_DEPRECATED("client_deprecated"),
RESEARCH("research");
RESEARCH("research"),
DONATE("donate");
private final String key;

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
@@ -60,7 +61,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
}
@Override
protected void onRun() throws IOException, UnauthenticatedResponseException {
protected void onRun() throws IOException, UnauthenticatedResponseException, InvalidKeyException {
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
Log.i(TAG, "Registration lock disabled");
return;

View File

@@ -38,7 +38,7 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getUncompressedVideoMaxSize(Context context) {
return isVideoTranscodeAvailable() ? 200 * MB
return isVideoTranscodeAvailable() ? 300 * MB
: getVideoMaxSize(context);
}

View File

@@ -29,6 +29,7 @@ import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.TransactionTooLargeException;
import android.service.notification.StatusBarNotification;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -69,6 +70,7 @@ import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.whispersystems.signalservice.internal.util.Util;
@@ -133,7 +135,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
sendInThreadNotification(context, recipient);
} else {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
@@ -436,8 +438,19 @@ public class DefaultMessageNotifier implements MessageNotifier {
}
Notification notification = builder.build();
NotificationManagerCompat.from(context).notify(notificationId, notification);
Log.i(TAG, "Posted notification.");
try {
NotificationManagerCompat.from(context).notify(notificationId, notification);
Log.i(TAG, "Posted notification.");
} catch (SecurityException e) {
Uri defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (!defaultValue.equals(notificationState.getRingtone(context))) {
Log.e(TAG, "Security exception when posting notification with custom ringtone", e);
clearNotificationRingtone(context, notifications.get(0).getRecipient());
} else {
throw e;
}
}
return shouldAlert;
}
@@ -489,8 +502,26 @@ public class DefaultMessageNotifier implements MessageNotifier {
}
Notification notification = builder.build();
NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build());
Log.i(TAG, "Posted notification. " + notification.toString());
try {
NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build());
Log.i(TAG, "Posted notification. " + notification.toString());
} catch (SecurityException securityException) {
Uri defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (!defaultValue.equals(notificationState.getRingtone(context))) {
Log.e(TAG, "Security exception when posting notification with custom ringtone", securityException);
clearNotificationRingtone(context, notifications.get(0).getRecipient());
} else {
throw securityException;
}
} catch (RuntimeException runtimeException) {
Throwable cause = runtimeException.getCause();
if (cause instanceof TransactionTooLargeException) {
Log.e(TAG, "Transaction too large", runtimeException);
} else {
throw runtimeException;
}
}
}
private static void sendInThreadNotification(Context context, Recipient recipient) {
@@ -710,6 +741,13 @@ public class DefaultMessageNotifier implements MessageNotifier {
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
}
private static void clearNotificationRingtone(@NonNull Context context, @NonNull Recipient recipient) {
SignalExecutors.BOUNDED.execute(() -> {
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), null);
NotificationChannels.updateMessageRingtone(context, recipient, null);
});
}
@Override
public void clearReminder(@NonNull Context context) {
Intent alarmIntent = new Intent(context, ReminderReceiver.class);

View File

@@ -99,7 +99,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
Stream.of(idMapForThread).forEach(entry -> {
List<Long> timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList();
ApplicationDependencies.getJobManager().add(new SendReadReceiptJob(threadToInfoEntry.getKey(), entry.getKey(), timestamps));
SendReadReceiptJob.enqueue(threadToInfoEntry.getKey(), entry.getKey(), timestamps);
});
});
}

View File

@@ -182,7 +182,7 @@ public class NotificationState {
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size());
Intent intent = new Intent(context, ConversationPopupActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));

View File

@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPrefere
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -88,6 +89,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal));
setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context)));
}
setShortcutId(ConversationUtil.getShortcutId(recipient));
}
private Drawable getContactDrawable(@NonNull Recipient recipient) {
@@ -230,13 +233,14 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
Person.Builder personBuilder = new Person.Builder()
.setKey(individualRecipient.getId().serialize())
.setKey(ConversationUtil.getShortcutId(individualRecipient))
.setBot(false);
this.threadRecipient = threadRecipient;
if (privacy.isDisplayContact()) {
personBuilder.setName(individualRecipient.getDisplayName(context));
personBuilder.setUri(individualRecipient.isSystemContact() ? individualRecipient.getContactUri().toString() : null);
Bitmap bitmap = getLargeBitmap(getContactDrawable(individualRecipient));
if (bitmap != null) {
@@ -283,13 +287,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
}
private void applyMessageStyle() {
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(
new Person.Builder()
.setBot(false)
.setName(Recipient.self().getDisplayName(context))
.setKey(Recipient.self().getId().serialize())
.setIcon(AvatarUtil.getIconForNotification(context, Recipient.self()))
.build());
ConversationUtil.pushShortcutForRecipientIfNeededSync(context, threadRecipient);
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self()));
if (threadRecipient.isGroup()) {
if (privacy.isDisplayContact()) {

View File

@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
@@ -90,7 +91,7 @@ public final class PinState {
}
return kbsData;
} catch (UnauthenticatedResponseException e) {
} catch (UnauthenticatedResponseException | InvalidKeyException e) {
Log.w(TAG, "Failed to restore key", e);
throw new IOException(e);
} catch (KeyBackupServicePinException e) {
@@ -170,7 +171,7 @@ public final class PinState {
*/
@WorkerThread
public static synchronized void onPinChangedOrCreated(@NonNull Context context, @NonNull String pin, @NonNull PinKeyboardType keyboard)
throws IOException, UnauthenticatedResponseException
throws IOException, UnauthenticatedResponseException, InvalidKeyException
{
Log.i(TAG, "onPinChangedOrCreated()");
@@ -272,7 +273,7 @@ public final class PinState {
*/
@WorkerThread
public static synchronized void onMigrateToRegistrationLockV2(@NonNull Context context, @NonNull String pin)
throws IOException, UnauthenticatedResponseException
throws IOException, UnauthenticatedResponseException, InvalidKeyException
{
Log.i(TAG, "onMigrateToRegistrationLockV2()");

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.jobs.StorageForcePushJob;
import org.thoughtcrime.securesms.keyvalue.InternalValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ConversationUtil;
public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragment {
private static final String TAG = Log.tag(InternalOptionsPreferenceFragment.class);
@@ -69,6 +70,12 @@ public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragme
Toast.makeText(getContext(), "Scheduled storage force push", Toast.LENGTH_SHORT).show();
return true;
});
findPreference("pref_delete_dynamic_shortcuts").setOnPreferenceClickListener(preference -> {
ConversationUtil.clearAllShortcuts(requireContext());
Toast.makeText(getContext(), "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show();
return true;
});
}
private void initializeSwitchPreference(@NonNull PreferenceDataStore preferenceDataStore,

View File

@@ -24,7 +24,9 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class ReviewUtil {
public final class ReviewUtil {
private ReviewUtil() { }
private static final long TIMEOUT = TimeUnit.HOURS.toMillis(24);
@@ -91,9 +93,13 @@ public class ReviewUtil {
@WorkerThread
public static @NonNull List<MessageRecord> getProfileChangeRecordsForGroup(@NonNull Context context, @NonNull GroupId.V2 groupId) {
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getByGroupId(groupId).get();
long threadId = Objects.requireNonNull(DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId));
Long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId);
return DatabaseFactory.getSmsDatabase(context).getProfileChangeDetailsRecords(threadId, System.currentTimeMillis() - TIMEOUT);
if (threadId == null) {
return Collections.emptyList();
} else {
return DatabaseFactory.getSmsDatabase(context).getProfileChangeDetailsRecords(threadId, System.currentTimeMillis() - TIMEOUT);
}
}
@WorkerThread

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.registration.fragments;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -12,6 +13,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
@@ -41,9 +43,18 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.READ_PHONE_STATE };
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
@@ -173,7 +184,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
private void initializeNumber() {
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE)) {
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
localNumber = Util.getDeviceNumber(requireContext());
}
@@ -198,8 +209,15 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
!TextSecurePreferences.isBackupEnabled(requireContext());
}
@SuppressLint("NewApi")
private static String[] getContinuePermissions(boolean isUserSelectionRequired) {
return isUserSelectionRequired ? PERMISSIONS_API_29 : PERMISSIONS;
if (isUserSelectionRequired) {
return PERMISSIONS_API_29;
} else if (Build.VERSION.SDK_INT >= 26) {
return PERMISSIONS_API_26;
} else {
return PERMISSIONS;
}
}
private static @StringRes int getContinueRationale(boolean isUserSelectionRequired) {

View File

@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.scribbles;
import android.animation.Animator;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.OvershootInterpolator;
@@ -14,11 +16,15 @@ import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.video.VideoBitRateCalculator;
import org.thoughtcrime.securesms.video.VideoUtil;
import org.thoughtcrime.securesms.video.videoconverter.VideoThumbnailsRangeSelectorView;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* The HUD (heads-up display) that contains all of the tools for editing video.
@@ -62,7 +68,9 @@ public final class VideoEditorHud extends LinearLayout {
}
@RequiresApi(api = 23)
public void setVideoSource(VideoSlide slide) throws IOException {
public void setVideoSource(@NonNull VideoSlide slide, @NonNull VideoBitRateCalculator videoBitRateCalculator, long maxSendSize)
throws IOException
{
Uri uri = slide.getUri();
if (uri == null || !slide.hasVideo()) {
@@ -71,6 +79,12 @@ public final class VideoEditorHud extends LinearLayout {
videoTimeLine.setInput(DecryptableUriMediaInput.createForUri(getContext(), uri));
long size = tryGetUriSize(getContext(), uri, Long.MAX_VALUE);
if (size > maxSendSize) {
videoTimeLine.setTimeLimit(VideoUtil.getMaxVideoUploadDurationInSeconds(), TimeUnit.SECONDS);
}
videoTimeLine.setOnRangeChangeListener(new VideoThumbnailsRangeSelectorView.OnRangeChangeListener() {
@Override
@@ -88,18 +102,26 @@ public final class VideoEditorHud extends LinearLayout {
}
@Override
public void onRangeDrag(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
public void onRangeDrag(long minValueUs, long maxValueUs, long durationUs, VideoThumbnailsRangeSelectorView.Thumb thumb) {
if (eventListener != null) {
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, false);
eventListener.onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, false);
}
}
@Override
public void onRangeDragEnd(long minValue, long maxValue, long duration, VideoThumbnailsRangeSelectorView.Thumb thumb) {
public void onRangeDragEnd(long minValueUs, long maxValueUs, long durationUs, VideoThumbnailsRangeSelectorView.Thumb thumb) {
if (eventListener != null) {
eventListener.onEditVideoDuration(duration, minValue, maxValue, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, true);
eventListener.onEditVideoDuration(durationUs, minValueUs, maxValueUs, thumb == VideoThumbnailsRangeSelectorView.Thumb.MIN, true);
}
}
@Override
public VideoThumbnailsRangeSelectorView.Quality getQuality(long clipDurationUs, long totalDurationUs) {
int inputBitRate = VideoBitRateCalculator.bitRate(size, TimeUnit.MICROSECONDS.toMillis(totalDurationUs));
VideoBitRateCalculator.Quality targetQuality = videoBitRateCalculator.getTargetQuality(TimeUnit.MICROSECONDS.toMillis(clipDurationUs), inputBitRate);
return new VideoThumbnailsRangeSelectorView.Quality(targetQuality.getFileSizeEstimate(), (int) (100 * targetQuality.getQuality()));
}
});
}
@@ -160,4 +182,29 @@ public final class VideoEditorHud extends LinearLayout {
void onSeek(long position, boolean dragComplete);
}
private long tryGetUriSize(@NonNull Context context, @NonNull Uri uri, long defaultValue) {
try {
return getSize(context, uri);
} catch (IOException e) {
Log.w(TAG, e);
return defaultValue;
}
}
private static long getSize(@NonNull Context context, @NonNull Uri uri) throws IOException {
long size = 0;
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
}
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, uri);
}
return size;
}
}

View File

@@ -4,51 +4,78 @@ package org.thoughtcrime.securesms.service;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentFilter;
import android.database.Cursor;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@RequiresApi(api = Build.VERSION_CODES.M)
public class DirectShareService extends ChooserTargetService {
private static final String TAG = DirectShareService.class.getSimpleName();
private static final String TAG = DirectShareService.class.getSimpleName();
private static final int MAX_TARGETS = 10;
@Override
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName,
IntentFilter matchedFilter)
{
List<ChooserTarget> results = new LinkedList<>();
ComponentName componentName = new ComponentName(this, ShareActivity.class);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
Cursor cursor = threadDatabase.getRecentConversationList(10, false, FeatureFlags.groupsV1ForcedMigration());
Map<RecipientId, ChooserTarget> results = new LinkedHashMap<>();
try {
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(this);
if (shortcutManager != null && !shortcutManager.getDynamicShortcuts().isEmpty()) {
addChooserTargetsFromDynamicShortcuts(results, shortcutManager.getDynamicShortcuts());
}
if (results.size() >= MAX_TARGETS) {
return new ArrayList<>(results.values());
}
}
ComponentName componentName = new ComponentName(this, ShareActivity.class);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getRecentConversationList(MAX_TARGETS, false, FeatureFlags.groupsV1ForcedMigration()))) {
ThreadRecord record;
while ((record = reader.getNext()) != null) {
if (results.containsKey(record.getRecipient().getId())) {
continue;
}
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
String name = recipient.getDisplayName(this);
@@ -71,25 +98,53 @@ public class DirectShareService extends ChooserTargetService {
avatar = getFallbackDrawable(recipient);
}
Bundle bundle = new Bundle();
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId());
bundle.putString(ShareActivity.EXTRA_RECIPIENT_ID, recipient.getId().serialize());
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType());
bundle.setClassLoader(getClassLoader());
Bundle bundle = buildExtras(record);
results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
results.put(recipient.getId(), new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
}
return results;
} finally {
if (cursor != null) cursor.close();
return new ArrayList<>(results.values());
}
}
private @NonNull Bundle buildExtras(@NonNull ThreadRecord threadRecord) {
Bundle bundle = new Bundle();
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, threadRecord.getThreadId());
bundle.putString(ShareActivity.EXTRA_RECIPIENT_ID, threadRecord.getRecipient().getId().serialize());
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, threadRecord.getDistributionType());
return bundle;
}
private Bitmap getFallbackDrawable(@NonNull Recipient recipient) {
Context themedContext = new ContextThemeWrapper(this, R.style.TextSecure_LightTheme);
return BitmapUtil.createFromDrawable(recipient.getFallbackContactPhotoDrawable(themedContext, false),
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height));
}
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
private void addChooserTargetsFromDynamicShortcuts(@NonNull Map<RecipientId, ChooserTarget> targetMap, @NonNull List<ShortcutInfo> shortcutInfos) {
Stream.of(shortcutInfos)
.sorted((lhs, rhs) -> Integer.compare(lhs.getRank(), rhs.getRank()))
.takeWhileIndexed((idx, info) -> idx < MAX_TARGETS)
.forEach(info -> {
Recipient recipient = Recipient.resolved(RecipientId.from(info.getId()));
ChooserTarget target = buildChooserTargetFromShortcutInfo(info, recipient);
targetMap.put(RecipientId.from(info.getId()), target);
});
}
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
private @NonNull ChooserTarget buildChooserTargetFromShortcutInfo(@NonNull ShortcutInfo info, @NonNull Recipient recipient) {
ThreadRecord threadRecord = DatabaseFactory.getThreadDatabase(this).getThreadRecordFor(recipient);
return new ChooserTarget(info.getShortLabel(),
AvatarUtil.getIconForShortcut(this, recipient),
info.getRank() / ((float) MAX_TARGETS),
new ComponentName(this, ShareActivity.class),
buildExtras(threadRecord));
}
}

View File

@@ -29,13 +29,16 @@ import org.signal.ringrtc.Remote;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.zkgroup.VerificationFailedException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -47,12 +50,15 @@ import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.ringrtc.TurnServerInfoParcel;
import org.thoughtcrime.securesms.service.webrtc.IdleActionProcessor;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData;
import org.thoughtcrime.securesms.service.webrtc.WebRtcInteractor;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.TelephonyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
@@ -125,6 +131,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String EXTRA_OPAQUE_MESSAGE = "opaque";
public static final String EXTRA_UUID = "uuid";
public static final String EXTRA_MESSAGE_AGE_SECONDS = "message_age_seconds";
public static final String EXTRA_GROUP_CALL_END_REASON = "group_call_end_reason";
public static final String EXTRA_GROUP_CALL_HASH = "group_call_hash";
public static final String EXTRA_GROUP_CALL_UPDATE_SENDER = "group_call_update_sender";
public static final String EXTRA_GROUP_CALL_UPDATE_GROUP = "group_call_update_group";
public static final String EXTRA_GROUP_CALL_ERA_ID = "era_id";
public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN";
public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL";
@@ -187,6 +198,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF = "GROUP_REQUEST_MEMBERSHIP_PROOF";
public static final String ACTION_GROUP_REQUEST_UPDATE_MEMBERS = "GROUP_REQUEST_UPDATE_MEMBERS";
public static final String ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS = "GROUP_UPDATE_RENDERED_RESOLUTIONS";
public static final String ACTION_GROUP_CALL_ENDED = "GROUP_CALL_ENDED";
public static final String ACTION_GROUP_CALL_UPDATE_MESSAGE = "GROUP_CALL_UPDATE_MESSAGE";
public static final int BUSY_TONE_LENGTH = 2000;
@@ -652,6 +665,38 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
sendMessage(new RemotePeer(RecipientId.from(uuid, null)), opaqueMessage);
}
public void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(GroupCallUpdateSendJob.create(recipient.getId(), groupCallEraId)));
peekGroupCall(new WebRtcData.GroupCallUpdateMetadata(Recipient.self().getId(), recipient.getId(), groupCallEraId, System.currentTimeMillis()));
}
public void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) {
networkExecutor.execute(() -> {
try {
Recipient group = Recipient.resolved(groupCallUpdateMetadata.getGroupRecipientId());
GroupId.V2 groupId = group.requireGroupId().requireV2();
GroupExternalCredential credential = GroupManager.getGroupExternalCredential(this, groupId);
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(this, groupId))
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
.toList();
callManager.peekGroupCall(BuildConfig.SIGNAL_SFU_URL, credential.getTokenBytes().toByteArray(), members, peekInfo -> {
DatabaseFactory.getSmsDatabase(this).insertOrUpdateGroupCall(group.getId(),
groupCallUpdateMetadata.getSender(),
groupCallUpdateMetadata.getServerReceivedTimestamp(),
groupCallUpdateMetadata.getGroupCallEraId(),
peekInfo.getEraId(),
peekInfo.getJoinedMembers());
});
} catch (IOException | VerificationFailedException | CallException e) {
Log.e(TAG, "error peeking", e);
}
});
}
@Override
public void onStartCall(@Nullable Remote remote, @NonNull CallId callId, @NonNull Boolean isOutgoing, @Nullable CallManager.CallMediaType callMediaType) {
Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType);
@@ -967,11 +1012,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF)
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray());
.putExtra(EXTRA_GROUP_EXTERNAL_TOKEN, credential.getTokenBytes().toByteArray())
.putExtra(EXTRA_GROUP_CALL_HASH, groupCall.hashCode());
startService(intent);
} catch (IOException | VerificationFailedException e) {
Log.w(TAG, "Unable to fetch group membership proof", e);
onEnded(groupCall, GroupCall.GroupCallEndReason.DEVICE_EXPLICITLY_DISCONNECTED);
}
});
}
@@ -992,12 +1039,18 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
@Override
public void onJoinedMembersChanged(@NonNull GroupCall groupCall) {
public void onPeekChanged(@NonNull GroupCall groupCall) {
startService(new Intent(this, WebRtcCallService.class).setAction(ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED));
}
@Override
public void onEnded(@NonNull GroupCall groupCall, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
Log.i(TAG, "onEnded: " + groupCallEndReason);
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(ACTION_GROUP_CALL_ENDED)
.putExtra(EXTRA_GROUP_CALL_HASH, groupCall.hashCode())
.putExtra(EXTRA_GROUP_CALL_END_REASON, groupCallEndReason.ordinal());
startService(intent);
}
}

View File

@@ -149,8 +149,13 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
Log.i(tag, "handleCallConcluded():");
if (remotePeer == null) {
return currentState;
}
Log.i(tag, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
return currentState.builder()
.changeCallInfoState()

View File

@@ -44,6 +44,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
remotePeer.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
true,
false
))
.build();
@@ -82,6 +83,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
remotePeer.getRecipient(),
null,
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
true,
false
))
.build();

View File

@@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@@ -14,7 +13,6 @@ import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import java.util.ArrayList;
import java.util.Objects;
/**
* Handles action for a connected/ongoing call. At this point it's mostly responding
@@ -121,7 +119,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}
}

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.service.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
/**
@@ -39,13 +41,20 @@ public class DisconnectingCallActionProcessor extends WebRtcActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
Log.i(TAG, "handleCallConcluded():");
Log.i(TAG, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
return currentState.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor))
.changeCallInfoState()
.removeRemotePeer(remotePeer)
.build();
WebRtcServiceStateBuilder builder = currentState.builder()
.actionProcessor(new IdleActionProcessor(webRtcInteractor));
if (remotePeer != null) {
Log.i(TAG, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode());
builder.changeCallInfoState()
.removeRemotePeer(remotePeer)
.commit();
}
return builder.build();
}
}

View File

@@ -48,14 +48,14 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
LongSparseArray<GroupCall.RemoteDeviceState> remoteDevices = groupCall.getRemoteDeviceStates();
for(int i = 0; i < remoteDevices.size(); i++) {
for (int i = 0; i < remoteDevices.size(); i++) {
GroupCall.RemoteDeviceState device = remoteDevices.get(remoteDevices.keyAt(i));
Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false);
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
CallParticipant callParticipant = participants.get(callParticipantId);
BroadcastVideoSink videoSink;
VideoTrack videoTrack = device.getVideoTrack();
VideoTrack videoTrack = device.getVideoTrack();
if (videoTrack != null) {
videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink()
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase());
@@ -68,17 +68,23 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
CallParticipant.createRemote(recipient,
null,
videoSink,
true));
Boolean.FALSE.equals(device.getAudioMuted()),
Boolean.FALSE.equals(device.getVideoMuted())));
}
return builder.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
if (groupCall == null || groupCall.hashCode() != groupCallHash) {
return currentState;
}
try {
groupCall.setMembershipProof(groupMembershipToken);
} catch (CallException e) {
@@ -96,7 +102,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
List<GroupCall.GroupMemberInfo> members = Stream.of(GroupManager.getUuidCipherTexts(context, group.requireGroupId().requireV2()))
.map(e -> new GroupCall.GroupMemberInfo(e.getKey(), e.getValue().serialize()))
.map(entry -> new GroupCall.GroupMemberInfo(entry.getKey(), entry.getValue().serialize()))
.toList();
try {
@@ -109,17 +115,20 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
protected @NonNull WebRtcServiceState handleUpdateRenderedResolutions(@NonNull WebRtcServiceState currentState) {
Map<CallParticipantId, CallParticipant> participants = currentState.getCallInfoState().getRemoteCallParticipantsMap();
ArrayList<GroupCall.RenderedResolution> renderedResolutions = new ArrayList<>(participants.size());
ArrayList<GroupCall.VideoRequest> resolutionRequests = new ArrayList<>(participants.size());
for (Map.Entry<CallParticipantId, CallParticipant> entry : participants.entrySet()) {
BroadcastVideoSink.RequestedSize maxSize = entry.getValue().getVideoSink().getMaxRequestingSize();
renderedResolutions.add(new GroupCall.RenderedResolution(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
BroadcastVideoSink videoSink = entry.getValue().getVideoSink();
BroadcastVideoSink.RequestedSize maxSize = videoSink.getMaxRequestingSize();
resolutionRequests.add(new GroupCall.VideoRequest(entry.getKey().getDemuxId(), maxSize.getWidth(), maxSize.getHeight(), null));
videoSink.newSizeRequested();
}
try {
currentState.getCallInfoState().requireGroupCall().setRenderedResolutions(renderedResolutions);
currentState.getCallInfoState().requireGroupCall().requestVideo(resolutionRequests);
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to set rendered resolutions", e);
}
@@ -127,7 +136,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().receivedHttpResponse(httpData.getRequestId(), httpData.getStatus(), httpData.getBody() != null ? httpData.getBody() : new byte[0]);
} catch (CallException e) {
@@ -136,7 +145,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
protected @NonNull WebRtcServiceState handleHttpFailure(@NonNull WebRtcServiceState currentState, @NonNull WebRtcData.HttpData httpData) {
try {
webRtcInteractor.getCallManager().httpRequestFailed(httpData.getRequestId());
} catch (CallException e) {
@@ -174,9 +183,43 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
return currentState;
}
@Override
protected @NonNull WebRtcServiceState handleGroupCallEnded(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
Log.i(tag, "handleGroupCallEnded(): reason: " + groupCallEndReason);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
if (groupCall == null || groupCall.hashCode() != groupCallHash) {
return currentState;
}
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
.groupCallState(WebRtcViewModel.GroupCallState.DISCONNECTED)
.build();
webRtcInteractor.sendMessage(currentState);
return terminateGroupCall(currentState);
}
public @NonNull WebRtcServiceState groupCallFailure(@NonNull WebRtcServiceState currentState, @NonNull String message, @NonNull Throwable error) {
Log.w(tag, "groupCallFailure(): " + message, error);
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
Recipient recipient = currentState.getCallInfoState().getCallRecipient();
if (recipient != null && currentState.getCallInfoState().getGroupCallState().isConnected()) {
webRtcInteractor.sendGroupCallMessage(recipient, WebRtcUtil.getGroupCallEraId(groupCall));
}
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)
@@ -186,7 +229,6 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
webRtcInteractor.sendMessage(currentState);
try {
GroupCall groupCall = currentState.getCallInfoState().getGroupCall();
if (groupCall != null) {
groupCall.disconnect();
}

View File

@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ringrtc.Camera;
@@ -20,6 +21,32 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
super(webRtcInteractor, TAG);
}
@Override
protected @NonNull WebRtcServiceState handleGroupLocalDeviceStateChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupLocalDeviceStateChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
GroupCall.LocalDeviceState device = groupCall.getLocalDeviceState();
GroupCall.ConnectionState connectionState = device.getConnectionState();
GroupCall.JoinState joinState = device.getJoinState();
Log.i(tag, "local device changed: " + connectionState + " " + joinState);
WebRtcViewModel.GroupCallState groupCallState = WebRtcUtil.groupCallStateForConnection(connectionState);
if (connectionState == GroupCall.ConnectionState.CONNECTED || connectionState == GroupCall.ConnectionState.CONNECTING) {
if (joinState == GroupCall.JoinState.JOINED) {
groupCallState = WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
} else if (joinState == GroupCall.JoinState.JOINING) {
groupCallState = WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING;
}
}
return currentState.builder().changeCallInfoState()
.groupCallState(groupCallState)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) {
Log.i(TAG, "handleSetEnableVideo():");
@@ -58,16 +85,43 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
.build();
}
@Override
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
PeekInfo peekInfo = groupCall.getPeekInfo();
if (peekInfo == null) {
return currentState;
}
if (currentState.getCallSetupState().hasSentJoinedMessage()) {
return currentState;
}
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
return currentState.builder()
.changeCallSetupState()
.sentJoinedMessage(true)
.build();
}
@Override
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
Log.i(TAG, "handleLocalHangup():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
try {
groupCall.disconnect();
} catch (CallException e) {
return groupCallFailure(currentState, "Unable to disconnect from group call", e);
}
webRtcInteractor.sendGroupCallMessage(currentState.getCallInfoState().getCallRecipient(), WebRtcUtil.getGroupCallEraId(groupCall));
currentState = currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_DISCONNECTED)

View File

@@ -8,6 +8,8 @@ import com.annimon.stream.Stream;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -40,6 +42,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId();
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
BuildConfig.SIGNAL_SFU_URL,
currentState.getVideoState().requireEglBase(),
webRtcInteractor.getGroupCallObserver());
@@ -96,8 +99,14 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
Log.i(tag, "handleGroupJoinedMembershipChanged():");
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
PeekInfo peekInfo = groupCall.getPeekInfo();
List<Recipient> callParticipants = Stream.of(groupCall.getJoinedGroupMembers())
if (peekInfo == null) {
Log.i(tag, "No peek info available");
return currentState;
}
List<Recipient> callParticipants = Stream.of(peekInfo.getJoinedMembers())
.map(uuid -> Recipient.externalPush(context, uuid, null, false))
.toList();
@@ -105,7 +114,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
.changeCallInfoState();
for (Recipient recipient : callParticipants) {
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), false));
builder.putParticipant(recipient, CallParticipant.createRemote(recipient, null, new BroadcastVideoSink(null), true, true));
}
return builder.build();

View File

@@ -211,7 +211,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}

View File

@@ -211,7 +211,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
}
@Override
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
return activeCallDelegate.handleCallConcluded(currentState, remotePeer);
}

View File

@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.GroupCallUpdateMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.HttpData;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
@@ -58,6 +60,8 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_SIGNALING_FAILURE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_ENDED_TIMEOUT;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_FLIP_CAMERA;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_ENDED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_CALL_UPDATE_MESSAGE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED;
import static org.thoughtcrime.securesms.service.WebRtcCallService.ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED;
@@ -116,9 +120,12 @@ import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getCa
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getEnable;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorCallState;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getErrorIdentityKey;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupCallEndReason;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupCallHash;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getGroupMembershipToken;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceCandidates;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getIceServers;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getNullableRemotePeerFromMap;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getOfferMessageType;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemotePeer;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemotePeerFromMap;
@@ -180,7 +187,7 @@ public abstract class WebRtcActionProcessor {
case ACTION_CALL_CONNECTED: return handleCallConnected(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_RECEIVED_OFFER_WHILE_ACTIVE: return handleReceivedOfferWhileActive(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_SEND_BUSY: return handleSendBusy(currentState, CallMetadata.fromIntent(intent), getBroadcastFlag(intent));
case ACTION_CALL_CONCLUDED: return handleCallConcluded(currentState, getRemotePeerFromMap(intent, currentState));
case ACTION_CALL_CONCLUDED: return handleCallConcluded(currentState, getNullableRemotePeerFromMap(intent, currentState));
case ACTION_REMOTE_VIDEO_ENABLE: return handleRemoteVideoEnable(currentState, getEnable(intent));
case ACTION_RECEIVE_HANGUP: return handleReceivedHangup(currentState, CallMetadata.fromIntent(intent), HangupMetadata.fromIntent(intent));
case ACTION_LOCAL_HANGUP: return handleLocalHangup(currentState);
@@ -226,9 +233,11 @@ public abstract class WebRtcActionProcessor {
case ACTION_GROUP_LOCAL_DEVICE_STATE_CHANGED: return handleGroupLocalDeviceStateChanged(currentState);
case ACTION_GROUP_REMOTE_DEVICE_STATE_CHANGED: return handleGroupRemoteDeviceStateChanged(currentState);
case ACTION_GROUP_JOINED_MEMBERSHIP_CHANGED: return handleGroupJoinedMembershipChanged(currentState);
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupMembershipToken(intent));
case ACTION_GROUP_REQUEST_MEMBERSHIP_PROOF: return handleGroupRequestMembershipProof(currentState, getGroupCallHash(intent), getGroupMembershipToken(intent));
case ACTION_GROUP_REQUEST_UPDATE_MEMBERS: return handleGroupRequestUpdateMembers(currentState);
case ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS: return handleUpdateRenderedResolutions(currentState);
case ACTION_GROUP_CALL_ENDED: return handleGroupCallEnded(currentState, getGroupCallHash(intent), getGroupCallEndReason(intent));
case ACTION_GROUP_CALL_UPDATE_MESSAGE: return handleGroupCallUpdateMessage(currentState, GroupCallUpdateMetadata.fromIntent(intent));
case ACTION_HTTP_SUCCESS: return handleHttpSuccess(currentState, HttpData.fromIntent(intent));
case ACTION_HTTP_FAILURE: return handleHttpFailure(currentState, HttpData.fromIntent(intent));
@@ -425,7 +434,7 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
protected @NonNull WebRtcServiceState handleCallConcluded(@NonNull WebRtcServiceState currentState, @Nullable RemotePeer remotePeer) {
Log.i(tag, "handleCallConcluded not processed");
return currentState;
}
@@ -693,7 +702,7 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, @NonNull byte[] groupMembershipToken) {
protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull byte[] groupMembershipToken) {
Log.i(tag, "handleGroupRequestMembershipProof not processed");
return currentState;
}
@@ -708,6 +717,16 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupCallEnded(@NonNull WebRtcServiceState currentState, int groupCallHash, @NonNull GroupCall.GroupCallEndReason groupCallEndReason) {
Log.i(tag, "handleGroupCallEnded not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleGroupCallUpdateMessage(@NonNull WebRtcServiceState currentState, @NonNull GroupCallUpdateMetadata groupCallUpdateMetadata) {
webRtcInteractor.peekGroupCall(groupCallUpdateMetadata);
return currentState;
}
//endregion
protected @NonNull WebRtcServiceState handleHttpSuccess(@NonNull WebRtcServiceState currentState, @NonNull HttpData httpData) {

View File

@@ -7,12 +7,16 @@ import androidx.annotation.Nullable;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.UUID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_ERA_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_GROUP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_UPDATE_SENDER;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_DEVICE_ID;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_IS_LEGACY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HANGUP_TYPE;
@@ -22,6 +26,7 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_HTTP_RE
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MESSAGE_AGE_SECONDS;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRecipientId;
import static org.thoughtcrime.securesms.service.webrtc.WebRtcIntentParser.getRemoteDevice;
/**
@@ -304,4 +309,44 @@ public class WebRtcData {
return messageAgeSeconds;
}
}
/**
* Metadata associated with a group call update message.
*/
public static class GroupCallUpdateMetadata {
private final RecipientId sender;
private final RecipientId groupRecipientId;
private final String groupCallEraId;
private final long serverReceivedTimestamp;
static @NonNull GroupCallUpdateMetadata fromIntent(@NonNull Intent intent) {
return new GroupCallUpdateMetadata(getRecipientId(intent, EXTRA_GROUP_CALL_UPDATE_SENDER),
getRecipientId(intent, EXTRA_GROUP_CALL_UPDATE_GROUP),
intent.getStringExtra(EXTRA_GROUP_CALL_ERA_ID),
intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, 0));
}
public GroupCallUpdateMetadata(@NonNull RecipientId sender, @NonNull RecipientId groupRecipientId, @Nullable String groupCallEraId, long serverReceivedTimestamp) {
this.sender = sender;
this.groupRecipientId = groupRecipientId;
this.groupCallEraId = groupCallEraId;
this.serverReceivedTimestamp = serverReceivedTimestamp;
}
public @NonNull RecipientId getSender() {
return sender;
}
public @NonNull RecipientId getGroupRecipientId() {
return groupRecipientId;
}
public @Nullable String getGroupCallEraId() {
return groupCallEraId;
}
public long getServerReceivedTimestamp() {
return serverReceivedTimestamp;
}
}
}

View File

@@ -6,9 +6,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@@ -36,6 +38,8 @@ import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_CAMERA_
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ENABLE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_CALL_STATE;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ERROR_IDENTITY_KEY;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_END_REASON;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_CALL_HASH;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_GROUP_EXTERNAL_TOKEN;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_ICE_CANDIDATES;
import static org.thoughtcrime.securesms.service.WebRtcCallService.EXTRA_MULTI_RING;
@@ -75,16 +79,19 @@ public final class WebRtcIntentParser {
}
public static @NonNull RemotePeer getRemotePeerFromMap(@NonNull Intent intent, @NonNull WebRtcServiceState currentState) {
int remotePeerKey = getRemotePeerKey(intent);
RemotePeer remotePeer = currentState.getCallInfoState().getPeer(remotePeerKey);
RemotePeer remotePeer = getNullableRemotePeerFromMap(intent, currentState);
if (remotePeer == null) {
throw new AssertionError("No RemotePeer in map for key: " + remotePeerKey + "!");
throw new AssertionError("No RemotePeer in map for key: " + getRemotePeerKey(intent) + "!");
}
return remotePeer;
}
public static @Nullable RemotePeer getNullableRemotePeerFromMap(@NonNull Intent intent, @NonNull WebRtcServiceState currentState) {
return currentState.getCallInfoState().getPeer(getRemotePeerKey(intent));
}
public static int getRemotePeerKey(@NonNull Intent intent) {
if (!intent.hasExtra(EXTRA_REMOTE_PEER_KEY)) {
throw new AssertionError("No RemotePeer key in intent!");
@@ -171,15 +178,34 @@ public final class WebRtcIntentParser {
public static @NonNull CameraState getCameraState(@NonNull Intent intent) {
return Objects.requireNonNull(intent.getParcelableExtra(EXTRA_CAMERA_STATE));
}
public static @NonNull WebRtcViewModel.State getErrorCallState(@NonNull Intent intent) {
return (WebRtcViewModel.State) Objects.requireNonNull(intent.getSerializableExtra(EXTRA_ERROR_CALL_STATE));
}
public static @NonNull Optional<IdentityKey> getErrorIdentityKey(@NonNull Intent intent) {
IdentityKeyParcelable identityKeyParcelable = (IdentityKeyParcelable) intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
IdentityKeyParcelable identityKeyParcelable = intent.getParcelableExtra(EXTRA_ERROR_IDENTITY_KEY);
if (identityKeyParcelable != null) {
return Optional.fromNullable(identityKeyParcelable.get());
}
return Optional.absent();
}
public static int getGroupCallHash(@NonNull Intent intent) {
return intent.getIntExtra(EXTRA_GROUP_CALL_HASH, 0);
}
public static @NonNull GroupCall.GroupCallEndReason getGroupCallEndReason(@NonNull Intent intent) {
int ordinal = intent.getIntExtra(EXTRA_GROUP_CALL_END_REASON, -1);
if (ordinal >= 0 && ordinal < GroupCall.GroupCallEndReason.values().length) {
return GroupCall.GroupCallEndReason.values()[ordinal];
}
return GroupCall.GroupCallEndReason.DEVICE_EXPLICITLY_DISCONNECTED;
}
public static @NonNull RecipientId getRecipientId(@NonNull Intent intent, @NonNull String name) {
return RecipientId.from(Objects.requireNonNull(intent.getStringExtra(name)));
}
}

View File

@@ -8,7 +8,6 @@ import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
@@ -89,6 +88,10 @@ public class WebRtcInteractor {
webRtcCallService.sendOpaqueCallMessage(uuid, callMessage);
}
void sendGroupCallMessage(@NonNull Recipient recipient, @Nullable String groupCallEraId) {
webRtcCallService.sendGroupCallMessage(recipient, groupCallEraId);
}
void setCallInProgressNotification(int type, @NonNull RemotePeer remotePeer) {
webRtcCallService.setCallInProgressNotification(type, remotePeer.getRecipient());
}
@@ -144,4 +147,8 @@ public class WebRtcInteractor {
void startAudioCommunication(boolean preserveSpeakerphone) {
audioManager.startCommunication(preserveSpeakerphone);
}
void peekGroupCall(@NonNull WebRtcData.GroupCallUpdateMetadata groupCallUpdateMetadata) {
webRtcCallService.peekGroupCall(groupCallUpdateMetadata);
}
}

View File

@@ -4,15 +4,16 @@ import android.content.Context;
import android.media.AudioManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.DjbECPublicKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
@@ -25,11 +26,7 @@ public final class WebRtcUtil {
public static @NonNull byte[] getPublicKeyBytes(@NonNull byte[] identityKey) throws InvalidKeyException {
ECPublicKey key = Curve.decodePoint(identityKey, 0);
if (key instanceof DjbECPublicKey) {
return ((DjbECPublicKey) key).getPublicKey();
}
throw new InvalidKeyException();
return key.getPublicKeyBytes();
}
public static @NonNull LockManager.PhoneState getInCallPhoneState(@NonNull Context context) {
@@ -71,4 +68,13 @@ public final class WebRtcUtil {
return WebRtcViewModel.GroupCallState.DISCONNECTED;
}
}
public static @Nullable String getGroupCallEraId(@Nullable GroupCall groupCall) {
if (groupCall == null) {
return null;
}
PeekInfo peekInfo = groupCall.getPeekInfo();
return peekInfo != null ? peekInfo.getEraId() : null;
}
}

View File

@@ -9,19 +9,21 @@ public final class CallSetupState {
boolean enableVideoOnCreate;
boolean isRemoteVideoOffer;
boolean acceptWithVideo;
boolean sentJoinedMessage;
public CallSetupState() {
this(false, false, false);
this(false, false, false, false);
}
public CallSetupState(@NonNull CallSetupState toCopy) {
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo);
this(toCopy.enableVideoOnCreate, toCopy.isRemoteVideoOffer, toCopy.acceptWithVideo, toCopy.sentJoinedMessage);
}
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo) {
public CallSetupState(boolean enableVideoOnCreate, boolean isRemoteVideoOffer, boolean acceptWithVideo, boolean sentJoinedMessage) {
this.enableVideoOnCreate = enableVideoOnCreate;
this.isRemoteVideoOffer = isRemoteVideoOffer;
this.acceptWithVideo = acceptWithVideo;
this.sentJoinedMessage = sentJoinedMessage;
}
public boolean isEnableVideoOnCreate() {
@@ -35,4 +37,8 @@ public final class CallSetupState {
public boolean isAcceptWithVideo() {
return acceptWithVideo;
}
public boolean hasSentJoinedMessage() {
return sentJoinedMessage;
}
}

View File

@@ -127,6 +127,11 @@ public class WebRtcServiceStateBuilder {
toBuild.acceptWithVideo = acceptWithVideo;
return this;
}
public @NonNull CallSetupStateBuilder sentJoinedMessage(boolean sentJoinedMessage) {
toBuild.sentJoinedMessage = sentJoinedMessage;
return this;
}
}
public class VideoStateBuilder {

View File

@@ -325,7 +325,7 @@ public class ShareActivity extends PassphraseRequiredActivity
Log.i(TAG, "Shared data was not external.");
}
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId.serialize());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
@@ -10,6 +11,7 @@ import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
@@ -22,8 +24,11 @@ import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -33,19 +38,31 @@ import java.util.concurrent.ExecutionException;
public final class AvatarUtil {
private static final String TAG = Log.tag(AvatarUtil.class);
private AvatarUtil() {
}
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target) {
loadBlurredIconIntoViewBackground(recipient, target, false);
}
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target, boolean useSelfProfileAvatar) {
Context context = target.getContext();
if (recipient.getContactPhoto() == null) {
ContactPhoto photo;
if (recipient.isSelf() && useSelfProfileAvatar) {
photo = new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar());
} else if (recipient.getContactPhoto() == null) {
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
return;
} else {
photo = recipient.getContactPhoto();
}
GlideApp.with(target)
.load(recipient.getContactPhoto())
.load(photo)
.transform(new CenterCrop(), new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS))
.into(new CustomViewTarget<View, Drawable>(target) {
@Override
@@ -93,6 +110,16 @@ public final class AvatarUtil {
}
}
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
@WorkerThread
public static Icon getIconForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
try {
return Icon.createWithAdaptiveBitmap(getShortcutInfoBitmap(context, recipient));
} catch (ExecutionException | InterruptedException e) {
return Icon.createWithAdaptiveBitmap(getFallbackForShortcut(context, recipient));
}
}
@WorkerThread
public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient) {
try {
@@ -102,13 +129,8 @@ public final class AvatarUtil {
}
}
public static GlideRequest<Drawable> getSelfAvatarOrFallbackIcon(@NonNull Context context, @DrawableRes int fallbackIcon) {
return GlideApp.with(context)
.asDrawable()
.load(new ProfileContactPhoto(Recipient.self(), Recipient.self().getProfileAvatar()))
.error(fallbackIcon)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL);
private static @NonNull Bitmap getShortcutInfoBitmap(@NonNull Context context, @NonNull Recipient recipient) throws ExecutionException, InterruptedException {
return DrawableUtil.wrapBitmapForShortcutInfo(request(GlideApp.with(context).asBitmap(), context, recipient, false).circleCrop().submit().get());
}
private static <T> GlideRequest<T> requestCircle(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
@@ -120,11 +142,40 @@ public final class AvatarUtil {
}
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient) {
return glideRequest.load(new ProfileContactPhoto(recipient, recipient.getProfileAvatar()))
return request(glideRequest, context, recipient, true);
}
private static <T> GlideRequest<T> request(@NonNull GlideRequest<T> glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf) {
final ContactPhoto photo;
if (Recipient.self().equals(recipient) && loadSelf) {
photo = new ProfileContactPhoto(recipient, recipient.getProfileAvatar());
} else {
photo = recipient.getContactPhoto();
}
return glideRequest.load(photo)
.error(getFallback(context, recipient))
.diskCacheStrategy(DiskCacheStrategy.ALL);
}
private static @NonNull Bitmap getFallbackForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
@DrawableRes final int photoSource;
if (recipient.isSelf()) {
photoSource = R.drawable.ic_note_80;
} else if (recipient.isGroup()) {
photoSource = R.drawable.ic_group_80;
} else {
photoSource = R.drawable.ic_profile_80;
}
Bitmap toWrap = DrawableUtil.toBitmap(new FallbackPhoto80dp(photoSource, recipient.getColor()).asDrawable(context, -1), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap);
toWrap.recycle();
return wrapped;
}
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
String name = Optional.fromNullable(recipient.getDisplayName(context)).or("");
MaterialColor fallbackColor = recipient.getColor();

View File

@@ -0,0 +1,239 @@
package org.thoughtcrime.securesms.util;
import android.app.Person;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* ConversationUtil encapsulates support for Android 11+'s new Conversations system
*/
public final class ConversationUtil {
public static final int CONVERSATION_SUPPORT_VERSION = 30;
private ConversationUtil() {}
/**
* Pushes a new dynamic shortcut for the given recipient and updates the ranks of all current
* shortcuts.
*/
public static void pushShortcutForRecipient(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
SignalExecutors.BOUNDED.execute(() -> {
pushShortcutAndUpdateRanks(context, recipient);
});
}
}
/**
* Synchronously pushes a new dynamic shortcut for the given recipient if one does not already exist.
*
* If added, this recipient is given a high ranking with the intention of not appearing immediately in results.
*/
@WorkerThread
public static void pushShortcutForRecipientIfNeededSync(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
String shortcutId = getShortcutId(recipient);
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
boolean hasPushedRecipientShortcut = Stream.of(shortcuts)
.filter(info -> Objects.equals(shortcutId, info.getId()))
.findFirst()
.isPresent();
if (!hasPushedRecipientShortcut) {
pushShortcutForRecipientInternal(context, recipient, shortcuts.size());
}
}
}
/**
* Clears all currently set dynamic shortcuts
*/
public static void clearAllShortcuts(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
List<ShortcutInfo> shortcutInfos = shortcutManager.getDynamicShortcuts();
shortcutManager.removeLongLivedShortcuts(Stream.of(shortcutInfos).map(ShortcutInfo::getId).toList());
}
}
/**
* Clears the shortcuts tied to a given thread.
*/
public static void clearShortcuts(@NonNull Context context, @NonNull Set<Long> threadIds) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
SignalExecutors.BOUNDED.execute(() -> {
List<RecipientId> recipientIds = DatabaseFactory.getThreadDatabase(context).getRecipientIdsForThreadIds(threadIds);
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
shortcutManager.removeLongLivedShortcuts(Stream.of(recipientIds).map(ConversationUtil::getShortcutId).toList());
});
}
}
/**
* Returns an ID that is unique between all recipients.
*
* @param recipientId The recipient ID to get a shortcut ID for
*
* @return A unique identifier that is stable for a given recipient id
*/
public static @NonNull String getShortcutId(@NonNull RecipientId recipientId) {
return recipientId.serialize();
}
/**
* Returns an ID that is unique between all recipients.
*
* @param recipient The recipient to get a shortcut for.
*
* @return A unique identifier that is stable for a given recipient id
*/
public static @NonNull String getShortcutId(@NonNull Recipient recipient) {
return getShortcutId(recipient.getId());
}
/**
* Updates the rank of each existing shortcut by 1 and then publishes a new shortcut of rank 0
* for the given recipient.
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static void pushShortcutAndUpdateRanks(@NonNull Context context, @NonNull Recipient recipient) {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
List<ShortcutInfo> currentShortcuts = shortcutManager.getDynamicShortcuts();
if (Util.isEmpty(currentShortcuts)) {
for (ShortcutInfo shortcutInfo : currentShortcuts) {
RecipientId recipientId = RecipientId.from(shortcutInfo.getId());
Recipient resolved = Recipient.resolved(recipientId);
ShortcutInfo updated = buildShortcutInfo(context, resolved, shortcutInfo.getRank() + 1);
shortcutManager.pushDynamicShortcut(updated);
}
}
pushShortcutForRecipientInternal(context, recipient, 0);
}
/**
* Pushes a dynamic shortcut for a given recipient to the shortcut manager
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static void pushShortcutForRecipientInternal(@NonNull Context context, @NonNull Recipient recipient, int rank) {
ShortcutInfo shortcutInfo = buildShortcutInfo(context, recipient, rank);
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
shortcutManager.pushDynamicShortcut(shortcutInfo);
}
/**
* Builds the shortcut info object for a given Recipient.
*
* @param context The Context under which we are operating
* @param recipient The Recipient to generate a ShortcutInfo for
* @param rank The rank that should be assigned to this recipient
* @return The new ShortcutInfo
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull ShortcutInfo buildShortcutInfo(@NonNull Context context,
@NonNull Recipient recipient,
int rank)
{
Recipient resolved = recipient.resolve();
Person[] persons = buildPersons(context, resolved);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(resolved);
String shortName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getShortDisplayName(context);
String longName = resolved.isSelf() ? context.getString(R.string.note_to_self) : resolved.getDisplayName(context);
return new ShortcutInfo.Builder(context, getShortcutId(resolved))
.setLongLived(true)
.setIntent(ConversationActivity.buildIntent(context, resolved.getId(), threadId))
.setShortLabel(shortName)
.setLongLabel(longName)
.setIcon(AvatarUtil.getIconForShortcut(context, resolved))
.setPersons(persons)
.setCategories(Collections.singleton(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION))
.setActivity(new ComponentName(context, "org.thoughtcrime.securesms.RoutingActivity"))
.setRank(rank)
.build();
}
/**
* @return an array of Person objects correlating to members of a conversation (other than self)
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person[] buildPersons(@NonNull Context context, @NonNull Recipient recipient) {
if (recipient.isGroup()) {
return buildPersonsForGroup(context, recipient.getGroupId().get());
} else {
return new Person[]{buildPerson(context, recipient)};
}
}
/**
* @return an array of Person objects correlating to members of a group (other than self)
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person[] buildPersonsForGroup(@NonNull Context context, @NonNull GroupId groupId) {
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return Stream.of(members).map(member -> buildPerson(context, member.resolve())).toArray(Person[]::new);
}
/**
* @return A Person object representing the given Recipient
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
private static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) {
return new Person.Builder()
.setKey(getShortcutId(recipient.getId()))
.setName(recipient.getDisplayName(context))
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
.build();
}
/**
* @return A Compat Library Person object representing the given Recipient
*/
@WorkerThread
public static @NonNull androidx.core.app.Person buildPersonCompat(@NonNull Context context, @NonNull Recipient recipient) {
return new androidx.core.app.Person.Builder()
.setKey(getShortcutId(recipient.getId()))
.setName(recipient.getDisplayName(context))
.setIcon(AvatarUtil.getIconForNotification(context, recipient))
.setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null)
.build();
}
}

View File

@@ -6,10 +6,15 @@ import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.camera.core.impl.SingleImageProxyBundle;
import androidx.core.graphics.drawable.DrawableCompat;
public final class DrawableUtil {
private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108);
private static final int SHORTCUT_INFO_WRAPPED_SIZE = ViewUtil.dpToPx(72);
private static final int SHORTCUT_INFO_PADDING = (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2;
private DrawableUtil() {}
public static @NonNull Bitmap toBitmap(@NonNull Drawable drawable, int width, int height) {
@@ -22,6 +27,16 @@ public final class DrawableUtil {
return bitmap;
}
public static @NonNull Bitmap wrapBitmapForShortcutInfo(@NonNull Bitmap toWrap) {
Bitmap bitmap = Bitmap.createBitmap(SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
Bitmap scaled = Bitmap.createScaledBitmap(toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(scaled, SHORTCUT_INFO_PADDING, SHORTCUT_INFO_PADDING, null);
return bitmap;
}
/**
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
*/

View File

@@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Stream;
import com.google.android.collect.Sets;
import org.json.JSONException;
import org.json.JSONObject;
@@ -22,6 +21,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
@@ -59,19 +59,19 @@ public final class FeatureFlags {
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 MAX_ENVELOPE_SIZE = "android.maxEnvelopeSize";
private static final String GV1_AUTO_MIGRATE_VERSION = "android.groupsv2.autoMigrateVersion";
private static final String GV1_MANUAL_MIGRATE_VERSION = "android.groupsv2.manualMigrateVersion";
private static final String GV1_FORCED_MIGRATE_VERSION = "android.groupsv2.forcedMigrateVersion";
private static final String GROUP_CALLING_VERSION = "android.groupsv2.callingVersion";
private static final String GV1_AUTO_MIGRATE = "android.groupsV1Migration.auto";
private static final String GV1_MANUAL_MIGRATE = "android.groupsV1Migration.manual";
private static final String GV1_FORCED_MIGRATE = "android.groupsV1Migration.forced";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
* remotely, place it in here.
*/
private static final Set<String> REMOTE_CAPABLE = Sets.newHashSet(
private static final Set<String> REMOTE_CAPABLE = SetUtil.newHashSet(
GROUPS_V2_RECOMMENDED_LIMIT,
GROUPS_V2_HARD_LIMIT,
GROUPS_V2_JOIN_VERSION,
@@ -81,10 +81,12 @@ public final class FeatureFlags {
VERIFY_V2,
CLIENT_EXPIRATION,
RESEARCH_MEGAPHONE_1,
DONATE_MEGAPHONE,
VIEWED_RECEIPTS,
MAX_ENVELOPE_SIZE,
GV1_AUTO_MIGRATE_VERSION,
GV1_MANUAL_MIGRATE_VERSION,
GV1_AUTO_MIGRATE,
GV1_MANUAL_MIGRATE,
GV1_FORCED_MIGRATE,
GROUP_CALLING_VERSION
);
@@ -105,7 +107,7 @@ public final class FeatureFlags {
* will be updated arbitrarily at runtime. This will make values more responsive, but also places
* more burden on the reader to ensure that the app experience remains consistent.
*/
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
private static final Set<String> HOT_SWAPPABLE = SetUtil.newHashSet(
GROUPS_V2_JOIN_VERSION,
VERIFY_V2,
CLIENT_EXPIRATION,
@@ -115,7 +117,7 @@ public final class FeatureFlags {
/**
* Flags in this set will stay true forever once they receive a true value from a remote config.
*/
private static final Set<String> STICKY = Sets.newHashSet(
private static final Set<String> STICKY = SetUtil.newHashSet(
VERIFY_V2
);
@@ -131,7 +133,7 @@ public final class FeatureFlags {
* desired test state.
*/
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
put(GV1_AUTO_MIGRATE_VERSION, change -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
put(GV1_AUTO_MIGRATE, change -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
}};
private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
@@ -242,6 +244,11 @@ public final class FeatureFlags {
return getString(RESEARCH_MEGAPHONE_1, "");
}
/** The raw donate megaphone CSV string */
public static String donateMegaphone() {
return getString(DONATE_MEGAPHONE, "");
}
/**
* Whether the user can choose phone number privacy settings, and;
* Whether to fetch and store the secondary certificate
@@ -260,24 +267,24 @@ public final class FeatureFlags {
return getInteger(MAX_ENVELOPE_SIZE, 0);
}
/** Whether or not auto-migration from GV1->GV2 is enabled. */
public static boolean groupsV1AutoMigration() {
return getVersionFlag(GV1_AUTO_MIGRATE_VERSION) == VersionFlag.ON;
}
/** Whether or not manual migration from GV1->GV2 is enabled. */
public static boolean groupsV1ManualMigration() {
return groupsV1AutoMigration() && getVersionFlag(GV1_MANUAL_MIGRATE_VERSION) == VersionFlag.ON;
}
/** Whether or not group calling is enabled. */
public static boolean groupCalling() {
return getVersionFlag(GROUP_CALLING_VERSION) == VersionFlag.ON;
}
/** Whether or not auto-migration from GV1->GV2 is enabled. */
public static boolean groupsV1AutoMigration() {
return getBoolean(GV1_AUTO_MIGRATE, false);
}
/** Whether or not manual migration from GV1->GV2 is enabled. */
public static boolean groupsV1ManualMigration() {
return getBoolean(GV1_MANUAL_MIGRATE, false) && groupsV1AutoMigration();
}
/** Whether or not forced migration from GV1->GV2 is enabled. */
public static boolean groupsV1ForcedMigration() {
return groupsV1AutoMigration() && getVersionFlag(GV1_FORCED_MIGRATE_VERSION) == VersionFlag.ON;
return getBoolean(GV1_FORCED_MIGRATE, false) && groupsV1ManualMigration() && groupsV1AutoMigration();
}
/** Only for rendering debug info. */
@@ -388,7 +395,7 @@ public final class FeatureFlags {
changes.put(key, Change.REMOVED);
} else if (newValue != oldValue && newValue instanceof Boolean) {
changes.put(key, (boolean) newValue ? Change.ENABLED : Change.DISABLED);
} else if (newValue != oldValue) {
} else if (!Objects.equals(oldValue, newValue)) {
changes.put(key, Change.CHANGED);
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import java.text.DecimalFormat;
/**
* Used for the pretty formatting of bytes for user display.
*/
public enum MemoryUnitFormat {
BYTES(" B"),
KILO_BYTES(" kB"),
MEGA_BYTES(" MB"),
GIGA_BYTES(" GB"),
TERA_BYTES(" TB");
private static final DecimalFormat ONE_DP = new DecimalFormat("#,##0.0");
private static final DecimalFormat OPTIONAL_ONE_DP = new DecimalFormat("#,##0.#");
private final String unitString;
MemoryUnitFormat(String unitString) {
this.unitString = unitString;
}
public double fromBytes(long bytes) {
return bytes / Math.pow(1000, ordinal());
}
/**
* Creates a string suitable to present to the user from the specified {@param bytes}.
* It will pick a suitable unit of measure to display depending on the size of the bytes.
* It will not select a unit of measure lower than the specified {@param minimumUnit}.
*
* @param forceOneDp If true, will include 1 decimal place, even if 0. If false, will only show 1 dp when it's non-zero.
*/
public static String formatBytes(long bytes, @NonNull MemoryUnitFormat minimumUnit, boolean forceOneDp) {
if (bytes <= 0) bytes = 0;
int ordinal = bytes != 0 ? (int) (Math.log10(bytes) / 3) : 0;
if (ordinal >= MemoryUnitFormat.values().length) {
ordinal = MemoryUnitFormat.values().length - 1;
}
MemoryUnitFormat unit = MemoryUnitFormat.values()[ordinal];
if (unit.ordinal() < minimumUnit.ordinal()) {
unit = minimumUnit;
}
return (forceOneDp ? ONE_DP : OPTIONAL_ONE_DP).format(unit.fromBytes(bytes)) + unit.unitString;
}
public static String formatBytes(long bytes) {
return formatBytes(bytes, BYTES, false);
}
}

View File

@@ -19,9 +19,9 @@ import java.util.Map;
* in the list. For example, "1:20000,*:40000" would mean 2% of the NANPA phone numbers and 4% of the rest of
* the world should see the megaphone.
*/
public final class ResearchMegaphone {
public final class PopulationFeatureFlags {
private static final String TAG = Log.tag(ResearchMegaphone.class);
private static final String TAG = Log.tag(PopulationFeatureFlags.class);
private static final String COUNTRY_WILDCARD = "*";
@@ -29,7 +29,18 @@ public final class ResearchMegaphone {
* In research megaphone group for given country code
*/
public static boolean isInResearchMegaphone() {
Map<String, Integer> countryCountEnabled = parseCountryCounts(FeatureFlags.researchMegaphone());
return isEnabled(FeatureFlags.RESEARCH_MEGAPHONE_1, FeatureFlags.researchMegaphone());
}
/**
* In donate megaphone group for given country code
*/
public static boolean isInDonateMegaphone() {
return isEnabled(FeatureFlags.DONATE_MEGAPHONE, FeatureFlags.donateMegaphone());
}
private static boolean isEnabled(@NonNull String flag, @NonNull String serialized) {
Map<String, Integer> countryCountEnabled = parseCountryCounts(serialized);
Recipient self = Recipient.self();
if (countryCountEnabled.isEmpty() || !self.getE164().isPresent() || !self.getUuid().isPresent()) {
@@ -37,7 +48,7 @@ public final class ResearchMegaphone {
}
long countEnabled = determineCountEnabled(countryCountEnabled, self.getE164().or(""));
long currentUserBucket = BucketingUtil.bucket(FeatureFlags.RESEARCH_MEGAPHONE_1, self.requireUuid(), 1_000_000);
long currentUserBucket = BucketingUtil.bucket(flag, self.requireUuid(), 1_000_000);
return countEnabled > currentUserBucket;
}

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager;
import android.app.job.JobScheduler;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.ShortcutManager;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.location.LocationManager;
@@ -31,6 +32,11 @@ public class ServiceUtil {
return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
@RequiresApi(25)
public static @Nullable ShortcutManager getShortcutManager(@NonNull Context context) {
return ContextCompat.getSystemService(context, ShortcutManager.class);
}
public static WindowManager getWindowManager(Context context) {
return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE);
}

View File

@@ -1,6 +1,11 @@
package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint;
import com.google.android.collect.Sets;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -24,4 +29,9 @@ public final class SetUtil {
result.addAll(b);
return result;
}
@SuppressLint("NewApi")
public static <E> HashSet<E> newHashSet(E... elements) {
return Sets.newHashSet(elements);
}
}

View File

@@ -4,29 +4,26 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.BidiFormatter;
import com.google.android.collect.Sets;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
public final class StringUtil {
private static final Set<Character> WHITESPACE = Sets.newHashSet('\u200E', // left-to-right mark
'\u200F', // right-to-left mark
'\u2007'); // figure space
private static final Set<Character> WHITESPACE = SetUtil.newHashSet('\u200E', // left-to-right mark
'\u200F', // right-to-left mark
'\u2007'); // figure space
private static final class Bidi {
/** Override text direction */
private static final Set<Integer> OVERRIDES = Sets.newHashSet("\u202a".codePointAt(0), /* LRE */
"\u202b".codePointAt(0), /* RLE */
"\u202d".codePointAt(0), /* LRO */
"\u202e".codePointAt(0) /* RLO */);
private static final Set<Integer> OVERRIDES = SetUtil.newHashSet("\u202a".codePointAt(0), /* LRE */
"\u202b".codePointAt(0), /* RLE */
"\u202d".codePointAt(0), /* LRO */
"\u202e".codePointAt(0) /* RLO */);
/** Set direction and isolate surrounding text */
private static final Set<Integer> ISOLATES = Sets.newHashSet("\u2066".codePointAt(0), /* LRI */
"\u2067".codePointAt(0), /* RLI */
"\u2068".codePointAt(0) /* FSI */);
private static final Set<Integer> ISOLATES = SetUtil.newHashSet("\u2066".codePointAt(0), /* LRI */
"\u2067".codePointAt(0), /* RLI */
"\u2068".codePointAt(0) /* FSI */);
/** Closes things in {@link #OVERRIDES} */
private static final int PDF = "\u202c".codePointAt(0);

View File

@@ -62,7 +62,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -625,12 +624,7 @@ public class Util {
}
public static String getPrettyFileSize(long sizeBytes) {
if (sizeBytes <= 0) return "0";
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(sizeBytes) / 3);
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1000, digitGroups)) + " " + units[digitGroups];
return MemoryUnitFormat.formatBytes(sizeBytes);
}
public static void sleep(long millis) {

Some files were not shown because too many files have changed in this diff Show More