mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-19 09:17:58 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7c55847e | ||
|
|
5209b74605 | ||
|
|
b90a74d26a | ||
|
|
8c1737e597 | ||
|
|
2ea5bd2d44 | ||
|
|
4166e7931e | ||
|
|
89f2c25d73 | ||
|
|
abb1ca2afe | ||
|
|
f7befd1593 | ||
|
|
28511de23c | ||
|
|
2ff3d1b7c5 | ||
|
|
fe6ae7e142 | ||
|
|
0da6c83ce4 | ||
|
|
184b7db43c | ||
|
|
e442e34c1b | ||
|
|
011efb0ce7 | ||
|
|
63d00f87d8 | ||
|
|
0323858145 | ||
|
|
a70e8ec7a7 | ||
|
|
b306a3ef41 | ||
|
|
ccd3467a61 | ||
|
|
40338afe7a | ||
|
|
ff97f6af56 | ||
|
|
6e7858e00f | ||
|
|
95468c85a8 | ||
|
|
f59e10d82c | ||
|
|
930370783e | ||
|
|
75062ada8a | ||
|
|
23618923d8 | ||
|
|
f1d3a2f322 | ||
|
|
3b7fbbaf6e | ||
|
|
725d793b20 | ||
|
|
5c3baca055 | ||
|
|
6e5abc92a0 | ||
|
|
8df6e95781 | ||
|
|
2290a6c0df | ||
|
|
907e8d93a3 | ||
|
|
918497fb94 | ||
|
|
3ccd6304c7 | ||
|
|
51d47adf57 | ||
|
|
f1e5206f56 |
@@ -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'
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}.
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user