Copione merged onto master
continuous-integration/drone/push Build is passing Details

master
blallo 2020-07-09 00:01:07 +02:00
commit 1145d6000c
178 changed files with 4353 additions and 1459 deletions

View File

@ -80,8 +80,8 @@ protobuf {
}
}
def canonicalVersionCode = 667
def canonicalVersionName = "4.65.2"
def canonicalVersionCode = 668
def canonicalVersionName = "4.66.0"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@ -197,7 +197,7 @@ android {
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "CDS_MRENCLAVE", "\"b657cad56d518827b0938949bb1e5727a9a4db358dd6a88e55e710a89ffa50bd\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
@ -304,7 +304,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.2.0'
implementation 'org.signal:ringrtc-android:2.3.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@ -120,8 +120,15 @@
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity=".calling"
android:launchMode="singleTask"/>
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity"
android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden"

View File

@ -271,7 +271,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
if (listCallback != null) {
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
if (FeatureFlags.groupsV2create() && FeatureFlags.internalUser()) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
} else {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
@ -462,6 +462,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionLimitReached()) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();

View File

@ -307,7 +307,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
byte[] localId;
byte[] remoteId;
if (FeatureFlags.uuids() && recipient.resolve().getUuid().isPresent()) {
if (FeatureFlags.uuidOnlyContacts() && recipient.resolve().getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));

View File

@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
@ -155,14 +156,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
@Override
protected void onUserLeaveHint() {
if (deviceSupportsPipMode()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(16, 9))
.build();
setPictureInPictureParams(params);
enterPipModeIfPossible();
}
//noinspection deprecation
enterPictureInPictureMode();
@Override
public void onBackPressed() {
if (!enterPipModeIfPossible()) {
super.onBackPressed();
}
}
@ -171,8 +171,19 @@ public class WebRtcCallActivity extends AppCompatActivity {
viewModel.setIsInPipMode(isInPictureInPictureMode);
}
private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16))
.build();
enterPictureInPictureMode(params);
return true;
}
return false;
}
private boolean isInPipMode() {
return deviceSupportsPipMode() && isInPictureInPictureMode();
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
}
private void processIntent(@NonNull Intent intent) {
@ -391,6 +402,9 @@ public class WebRtcCallActivity extends AppCompatActivity {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
if (hangupType == HangupMessage.Type.NEED_PERMISSION) {
startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.getId()));
}
delayedFinish();
}
@ -489,7 +503,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
.show();
}
private boolean deviceSupportsPipMode() {
private boolean isSystemPipEnabledAndAvailable() {
return Build.VERSION.SDK_INT >= 26 &&
FeatureFlags.callingPip() &&
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
@ -510,19 +524,20 @@ public class WebRtcCallActivity extends AppCompatActivity {
viewModel.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
callScreen.setLocalRenderer(event.getLocalRenderer());

View File

@ -39,6 +39,7 @@ public abstract class Attachment {
private final String fastPreflightId;
private final boolean voiceNote;
private final boolean borderless;
private final int width;
private final int height;
private final boolean quote;
@ -59,11 +60,26 @@ public abstract class Attachment {
@NonNull
private final TransformProperties transformProperties;
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
public Attachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@Nullable String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
boolean quote,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this.contentType = contentType;
@ -77,6 +93,7 @@ public abstract class Attachment {
this.digest = digest;
this.fastPreflightId = fastPreflightId;
this.voiceNote = voiceNote;
this.borderless = borderless;
this.width = width;
this.height = height;
this.quote = quote;
@ -150,6 +167,10 @@ public abstract class Attachment {
return voiceNote;
}
public boolean isBorderless() {
return borderless;
}
public int getWidth() {
return width;
}

View File

@ -20,17 +20,34 @@ public class DatabaseAttachment extends Attachment {
private final boolean hasThumbnail;
private final int displayOrder;
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
boolean hasData, boolean hasThumbnail,
String contentType, int transferProgress, long size,
String fileName, int cdnNumber, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties, int displayOrder,
public DatabaseAttachment(AttachmentId attachmentId,
long mmsId,
boolean hasData,
boolean hasThumbnail,
String contentType,
int transferProgress,
long size,
String fileName,
int cdnNumber,
String location,
String key,
String relay,
byte[] digest,
String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties,
int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;

View File

@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
}
@Nullable

View File

@ -18,14 +18,26 @@ import java.util.List;
public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, int cdnNumber, @NonNull String location,
@Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
private PointerAttachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@NonNull String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
int width,
int height,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
}
@Nullable
@ -91,21 +103,22 @@ public class PointerAttachment extends Attachment {
}
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
}
@ -123,6 +136,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
null,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,

View File

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override

View File

@ -15,20 +15,42 @@ public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
public UriAttachment(@NonNull Uri uri,
@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
boolean voiceNote,
boolean borderless,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId,
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
public UriAttachment(@NonNull Uri dataUri,
@Nullable Uri thumbnailUri,
@NonNull String contentType,
int transferState,
long size,
int width,
int height,
@Nullable String fileName,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}

View File

@ -7,23 +7,26 @@ import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.CenterInside;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
public class StickerView extends FrameLayout {
public class BorderlessImageView extends FrameLayout {
private ThumbnailView image;
private View missingShade;
public StickerView(@NonNull Context context) {
public BorderlessImageView(@NonNull Context context) {
super(context);
init();
}
public StickerView(@NonNull Context context, @Nullable AttributeSet attrs) {
public BorderlessImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@ -50,10 +53,17 @@ public class StickerView extends FrameLayout {
image.setOnLongClickListener(l);
}
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getDataUri() == null;
if (slide.hasSticker()) {
image.setFit(new CenterInside());
image.setImageResource(glideRequests, slide, showControls, false);
} else {
image.setFit(new CenterCrop());
image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().getWidth(), slide.asAttachment().getHeight());
}
image.setImageResource(glideRequests, stickerSlide, showControls, false);
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
}

View File

@ -398,6 +398,10 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().showProgressSpinner();
}
public void setFit(@NonNull BitmapTransformation fit) {
this.fit = fit;
}
protected void setRadius(int radius) {
this.radius = radius;
}

View File

@ -120,6 +120,10 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
canvas.scale(-1, 1, usableWidth / 2f, usableHeight / 2f);
}
for (int bar = 0; bar < data.length; bar++) {
float x = bar * (barWidth + barGap) + barWidth / 2f;
float y = data[bar] * maxHeight;

View File

@ -286,6 +286,7 @@ public class WebRtcCallView extends FrameLayout {
public void setStatusFromHangupType(@NonNull HangupMessage.Type hangupType) {
switch (hangupType) {
case NORMAL:
case NEED_PERMISSION:
status.setText(R.string.RedPhone_ending_call);
break;
case ACCEPTED:
@ -306,7 +307,10 @@ public class WebRtcCallView extends FrameLayout {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear();
visibleViewSet.addAll(topViews);
if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews);
}
if (webRtcControls.displayIncomingCallButtons()) {
visibleViewSet.addAll(incomingCallViews);

View File

@ -180,6 +180,7 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
isInPipMode.getValue() == Boolean.TRUE,
callState,
audioOutput));
}
@ -189,9 +190,9 @@ public class WebRtcCallViewModel extends ViewModel {
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean neverDisplayControls, @NonNull WebRtcControls controls) {
if (neverDisplayControls) return WebRtcControls.NONE;
else return controls;
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
if (isInPipMode) return WebRtcControls.PIP;
else return controls;
}
private void startTimer() {

View File

@ -5,22 +5,25 @@ import androidx.annotation.NonNull;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, CallState.NONE, WebRtcAudioOutput.HANDSET);
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isBluetoothAvailable;
private final boolean isInPipMode;
private final CallState callState;
private final WebRtcAudioOutput audioOutput;
private WebRtcControls() {
this(false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
this(false, false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
}
WebRtcControls(boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean isInPipMode,
@NonNull CallState callState,
@NonNull WebRtcAudioOutput audioOutput)
{
@ -28,6 +31,7 @@ public final class WebRtcControls {
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
this.isInPipMode = isInPipMode;
this.callState = callState;
this.audioOutput = audioOutput;
}
@ -80,6 +84,10 @@ public final class WebRtcControls {
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
}
boolean displayTopViews() {
return !isInPipMode;
}
WebRtcAudioOutput getAudioOutput() {
return audioOutput;
}

View File

@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV1 {
private static final String TAG = ContactDiscoveryV1.class.getSimpleName();
static @NonNull DirectoryResult getDirectoryResult(@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
List<ContactTokenDetails> activeTokens = getTokens(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
HashMap<String, UUID> uuids = new HashMap<>();
for (String number : outputResult.getNumbers()) {
uuids.put(number, null);
}
return new DirectoryResult(uuids, outputResult.getRewrites());
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull String number) throws IOException {
return getDirectoryResult(Collections.singleton(number), Collections.singleton(number));
}
private static @NonNull List<ContactTokenDetails> getTokens(@NonNull Set<String> numbers) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
if (numbers.size() == 1) {
Optional<ContactTokenDetails> details = accountManager.getContact(numbers.iterator().next());
return details.isPresent() ? Collections.singletonList(details.get()) : Collections.emptyList();
} else {
return accountManager.getContacts(numbers);
}
}
}

View File

@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
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.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
@WorkerThread
static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
Set<String> sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers());
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
KeyStore iasKeyStore = getIasKeyStore(context);
try {
Map<String, UUID> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites());
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
Log.w(TAG, "Attestation error.", e);
throw new IOException(e);
}
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull Context context, @NonNull String number) throws IOException {
return getDirectoryResult(context, Collections.singleton(number), Collections.singleton(number));
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
private static KeyStore getIasKeyStore(@NonNull Context context) {
try {
TrustStore contactTrustStore = new IasTrustStore(context);
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray());
return keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -1,32 +1,153 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Manages all the stuff around determining if a user is registered or not.
*/
public class DirectoryHelper {
private static final String TAG = Log.tag(DirectoryHelper.class);
@WorkerThread
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
Log.i(TAG, "Have not yet set our own local number. Skipping.");
return;
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
Log.i(TAG, "No contact permissions. Skipping.");
return;
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
HashMap<RecipientId, String> uuidMap = new HashMap<>();
// TODO [greyson] [cds] Probably want to do this in a DB transaction to prevent concurrent operations
for (Map.Entry<String, UUID> entry : result.getRegisteredNumbers().entrySet()) {
// TODO [greyson] [cds] This is where we'll have to do record merging
String e164 = entry.getKey();
UUID uuid = entry.getValue();
Optional<RecipientId> uuidEntry = uuid != null ? recipientDatabase.getByUuid(uuid) : Optional.absent();
// TODO [greyson] [cds] Handle phone numbers changing, possibly conflicting
if (uuidEntry.isPresent()) {
recipientDatabase.setPhoneNumber(uuidEntry.get(), e164);
}
RecipientId id = uuidEntry.isPresent() ? uuidEntry.get() : recipientDatabase.getOrInsertFromE164(e164);
uuidMap.put(id, uuid != null ? uuid.toString() : null);
}
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
notifyNewUsers(context, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
StorageSyncHelper.scheduleSyncForDataChange();
@ -34,20 +155,249 @@ public class DirectoryHelper {
@WorkerThread
public static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
} else {
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
if (recipient.getUuid().isPresent()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
if (!recipient.getE164().isPresent()) {
Log.w(TAG, "No UUID or E164?");
return RegisteredState.NOT_REGISTERED;
}
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
} else {
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
}
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
if (result.getRegisteredNumbers().size() > 0) {
recipientDatabase.markRegistered(recipient.getId(), result.getRegisteredNumbers().values().iterator().next());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Collections.singletonList(recipient.getId()), false, result.getNumberRewrites());
}
newRegisteredState = result.getRegisteredNumbers().size() > 0 ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
if (newRegisteredState != originalRegisteredState) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
StorageSyncHelper.scheduleSyncForDataChange();
}
return newRegisteredState;
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static void updateContactsDatabase(@NonNull Context context,
@NonNull Collection<RecipientId> activeIds,
boolean removeMissing,
@NonNull Map<String, String> rewrites)
{
AccountHolder account = getOrCreateSystemAccount(context);
if (account == null) {
Log.w(TAG, "Failed to create an account!");
return;
}
try {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(context);
List<String> activeAddresses = Stream.of(activeIds)
.map(Recipient::resolved)
.filter(Recipient::hasE164)
.map(Recipient::requireE164)
.toList();
contactsDatabase.removeDeletedRawContacts(account.getAccount());
contactsDatabase.setRegisteredUsers(account.getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static @Nullable AccountHolder getOrCreateSystemAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID);
AccountHolder account;
if (accounts.length == 0) {
account = createAccount(context);
} else {
account = new AccountHolder(accounts[0], false);
}
if (account != null && !ContentResolver.getSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static @Nullable AccountHolder createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), BuildConfig.APPLICATION_ID);
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return new AccountHolder(account, true);
} else {
Log.w(TAG, "Failed to create account!");
return null;
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull Collection<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: " + hour + ")");
}
}
}
}
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
static class DirectoryResult {
private final Map<String, UUID> registeredNumbers;
private final Map<String, String> numberRewrites;
DirectoryResult(@NonNull Map<String, UUID> registeredNumbers,
@NonNull Map<String, String> numberRewrites)
{
this.registeredNumbers = registeredNumbers;
this.numberRewrites = numberRewrites;
}
@NonNull Map<String, UUID> getRegisteredNumbers() {
return registeredNumbers;
}
@NonNull Map<String, String> getNumberRewrites() {
return numberRewrites;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@ -1,401 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class DirectoryHelperV1 {
private static final String TAG = DirectoryHelperV1.class.getSimpleName();
@WorkerThread
static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
List<RecipientId> newlyActiveUsers = refreshDirectory(context, ApplicationDependencies.getSignalServiceAccountManager());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (notifyOfNewUsers) notifyNewUsers(context, newlyActiveUsers);
}
@SuppressLint("CheckResult")
private static @NonNull List<RecipientId> refreshDirectory(@NonNull Context context, @NonNull SignalServiceAccountManager accountManager) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
return Collections.emptyList();
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
return Collections.emptyList();
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> allRecipientNumbers = recipientDatabase.getAllPhoneNumbers();
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(allRecipientNumbers);
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet());
Set<String> storedNumbers = Stream.of(allRecipientNumbers).collect(Collectors.toSet());
DirectoryResult directoryResult = getDirectoryResult(context, accountManager, recipientDatabase, storedNumbers, eligibleContactNumbers);
return directoryResult.getNewlyActiveRecipients();
}
@WorkerThread
static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
if (recipient.getUuid().isPresent() && !recipient.getE164().isPresent()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
return getRegisteredState(context, ApplicationDependencies.getSignalServiceAccountManager(), recipientDatabase, recipient);
}
private static void updateContactsDatabase(@NonNull Context context, @NonNull List<RecipientId> activeIds, boolean removeMissing, Map<String, String> rewrites) {
Optional<AccountHolder> account = getOrCreateAccount(context);
if (account.isPresent()) {
try {
List<String> activeAddresses = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasE164).map(Recipient::requireE164).toList();
DatabaseFactory.getContactsDatabase(context).removeDeletedRawContacts(account.get().getAccount());
DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull List<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), false);
}
}
}
}
}
private static Optional<AccountHolder> getOrCreateAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms");
Optional<AccountHolder> account;
if (accounts.length == 0) account = createAccount(context);
else account = Optional.of(new AccountHolder(accounts[0], false));
if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static Optional<AccountHolder> createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms");
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return Optional.of(new AccountHolder(account, true));
} else {
Log.w(TAG, "Failed to create account!");
return Optional.absent();
}
}
private static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Set<String> locallyStoredNumbers,
@NonNull Set<String> eligibleContactNumbers)
throws IOException
{
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(eligibleContactNumbers, locallyStoredNumbers);
List<ContactTokenDetails> activeTokens = accountManager.getContacts(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
if (inputResult.getFuzzies().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Got a fuzzy number result.");
}
if (outputResult.getRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
}
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
List<RecipientId> activeIds = new LinkedList<>();
List<RecipientId> inactiveIds = new LinkedList<>();
Set<String> inactiveContactNumbers = new HashSet<>(inputResult.getNumbers());
inactiveContactNumbers.removeAll(outputResult.getRewrites().keySet());
for (String number : outputResult.getNumbers()) {
activeIds.add(recipientDatabase.getOrInsertFromE164(number));
inactiveContactNumbers.remove(number);
}
for (String inactiveContactNumber : inactiveContactNumbers) {
inactiveIds.add(recipientDatabase.getOrInsertFromE164(inactiveContactNumber));
}
Set<RecipientId> currentActiveIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> contactIds = new HashSet<>(recipientDatabase.getSystemContacts());
List<RecipientId> newlyActiveIds = Stream.of(activeIds)
.filter(id -> !currentActiveIds.contains(id))
.filter(contactIds::contains)
.toList();
recipientDatabase.setRegistered(activeIds, inactiveIds);
updateContactsDatabase(context, activeIds, true, outputResult.getRewrites());
Set<String> activeContactNumbers = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasSmsAddress).map(Recipient::requireSmsAddress).collect(Collectors.toSet());
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) {
return new DirectoryResult(activeContactNumbers, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
return new DirectoryResult(activeContactNumbers);
}
}
private static RegisteredState getRegisteredState(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Recipient recipient)
throws IOException
{
boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED;
boolean systemContact = recipient.isSystemContact();
Optional<ContactTokenDetails> details = Optional.absent();
Map<String, String> rewrites = new HashMap<>();
if (recipient.hasE164()) {
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(Collections.singletonList(recipient.requireE164()), recipientDatabase.getAllPhoneNumbers());
if (inputResult.getNumbers().size() > 1) {
Log.i(TAG, "[getRegisteredState] Got a fuzzy number result.");
List<ContactTokenDetails> detailList = accountManager.getContacts(inputResult.getNumbers());
Collection<String> registered = Stream.of(detailList).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(registered, inputResult);
String finalNumber = recipient.requireE164();
ContactTokenDetails detail = new ContactTokenDetails();
if (outputResult.getRewrites().size() > 0 && outputResult.getRewrites().containsKey(finalNumber)) {
Log.i(TAG, "[getRegisteredState] Need to rewrite a number.");
finalNumber = outputResult.getRewrites().get(finalNumber);
rewrites = outputResult.getRewrites();
}
detail.setNumber(finalNumber);
details = Optional.of(detail);
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
} else {
details = accountManager.getContact(recipient.requireE164());
}
}
if (details.isPresent()) {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.REGISTERED);
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Util.asList(recipient.getId()), false, rewrites);
}
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
return RegisteredState.REGISTERED;
} else {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.NOT_REGISTERED);
return RegisteredState.NOT_REGISTERED;
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static class DirectoryResult {
private final Set<String> numbers;
private final List<RecipientId> newlyActiveRecipients;
DirectoryResult(@NonNull Set<String> numbers) {
this(numbers, Collections.emptyList());
}
DirectoryResult(@NonNull Set<String> numbers, @NonNull List<RecipientId> newlyActiveRecipients) {
this.numbers = numbers;
this.newlyActiveRecipients = newlyActiveRecipients;
}
Set<String> getNumbers() {
return numbers;
}
List<RecipientId> getNewlyActiveRecipients() {
return newlyActiveRecipients;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@ -8,6 +8,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* A helper class to match a single number with multiple possible registered numbers. An example is
@ -67,6 +68,32 @@ class FuzzyPhoneNumberHelper {
return new OutputResult(allNumbers, rewrites);
}
/**
* This should be run on the list of numbers we find out are registered with the server. Based on
* these results and our initial input set, we can decide if we need to rewrite which number we
* have stored locally.
*/
static @NonNull OutputResultV2 generateOutputV2(@NonNull Map<String, UUID> registeredNumbers, @NonNull InputResult inputResult) {
Map<String, UUID> allNumbers = new HashMap<>(registeredNumbers);
Map<String, String> rewrites = new HashMap<>();
for (Map.Entry<String, String> entry : inputResult.getFuzzies().entrySet()) {
if (registeredNumbers.containsKey(entry.getKey()) && registeredNumbers.containsKey(entry.getValue())) {
if (mxHas1(entry.getKey())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
} else {
allNumbers.remove(entry.getValue());
}
} else if (registeredNumbers.containsKey(entry.getValue())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
}
}
return new OutputResultV2(allNumbers, rewrites);
}
private static boolean mx(@NonNull String number) {
return number.startsWith("+52") && (number.length() == 13 || number.length() == 14);
@ -127,4 +154,22 @@ class FuzzyPhoneNumberHelper {
return rewrites;
}
}
public static class OutputResultV2 {
private final Map<String, UUID> numbers;
private final Map<String, String> rewrites;
private OutputResultV2(@NonNull Map<String, UUID> numbers, @NonNull Map<String, String> rewrites) {
this.numbers = numbers;
this.rewrites = rewrites;
}
public @NonNull Map<String, UUID> getNumbers() {
return numbers;
}
public @NonNull Map<String, String> getRewrites() {
return rewrites;
}
}
}

View File

@ -643,7 +643,7 @@ public class Contact implements Parcelable {
private static Attachment attachmentFromUri(@Nullable Uri uri) {
if (uri == null) return null;
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null, null);
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, false, null, null, null, null, null);
}
@Override

View File

@ -41,7 +41,6 @@ import android.provider.Browser;
import android.provider.ContactsContract;
import android.provider.Telephony;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.KeyEvent;
@ -63,6 +62,7 @@ import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
@ -73,6 +73,7 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.ViewModelProviders;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
@ -246,7 +247,10 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@ -611,7 +615,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
setMedia(data.getData(),
MediaType.GIF,
data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0));
data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0),
data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false));
break;
case SMS_DEFAULT:
initializeSecurity(isSecureText, isDefaultSms);
@ -635,9 +640,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), null));
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
} else {
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
}
@ -1984,10 +1989,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
//////// Helper Methods
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
return setMedia(uri, mediaType, 0, 0);
return setMedia(uri, mediaType, 0, 0, false);
}
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) {
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height, boolean borderless) {
if (uri == null) {
return new SettableFuture<>(false);
}
@ -1996,7 +2001,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, 0, Optional.absent(), Optional.absent(), Optional.absent());
String mimeType = MediaUtil.getMimeType(this, uri);
if (mimeType == null) {
mimeType = mediaType.toFallbackMimeType();
}
Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, Optional.absent(), Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
@ -2362,7 +2372,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
String body,
@NonNull String body,
SlideDeck slideDeck,
QuoteModel quote,
List<Contact> contacts,
@ -2650,10 +2660,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onMediaSelected(@NonNull Uri uri, String contentType) {
if (!TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif")) {
setMedia(uri, MediaType.GIF);
} else if (MediaUtil.isImageType(contentType)) {
setMedia(uri, MediaType.IMAGE);
if (MediaUtil.isGif(contentType) || MediaUtil.isImageType(contentType)) {
SimpleTask.run(getLifecycle(),
() -> getKeyboardImageDetails(uri),
details -> sendKeyboardImage(uri, contentType, details));
} else if (MediaUtil.isVideoType(contentType)) {
setMedia(uri, MediaType.VIDEO);
} else if (MediaUtil.isAudioType(contentType)) {
@ -2688,7 +2698,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
if (sendButton.getSelectedTransport().isSms()) {
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, Optional.absent(), Optional.absent(), Optional.absent());
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent());
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
startActivityForResult(intent, MEDIA_SENDER);
return;
@ -3065,6 +3075,55 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
@WorkerThread
private @Nullable KeyboardImageDetails getKeyboardImageDetails(@NonNull Uri uri) {
try {
Bitmap bitmap = glideRequests.asBitmap()
.load(uri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.submit()
.get(1000, TimeUnit.MILLISECONDS);
int topLeft = bitmap.getPixel(0, 0);
return new KeyboardImageDetails(bitmap.getWidth(), bitmap.getHeight(), Color.alpha(topLeft) < 255);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
return null;
}
}
private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) {
if (details == null || !details.hasTransparency) {
setMedia(uri, Objects.requireNonNull(MediaType.from(contentType)));
return;
}
long expiresIn = recipient.get().getExpireMessages() * 1000L;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
boolean initiating = threadId == -1;
QuoteModel quote = inputPanel.getQuote().orNull();
SlideDeck slideDeck = new SlideDeck();
if (MediaUtil.isGif(contentType)) {
slideDeck.addSlide(new GifSlide(this, uri, 0, details.width, details.height, details.hasTransparency, null));
} else if (MediaUtil.isImageType(contentType)) {
slideDeck.addSlide(new ImageSlide(this, uri, contentType, 0, details.width, details.height, details.hasTransparency, null, null));
} else {
throw new AssertionError("Only images are supported!");
}
sendMediaMessage(isSmsForced(),
"",
slideDeck,
quote,
Collections.emptyList(),
Collections.emptyList(),
expiresIn,
false,
subscriptionId,
initiating,
true);
}
private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
@Override
public void onDismissed(final List<IdentityRecord> unverifiedIdentities) {
@ -3154,4 +3213,16 @@ public class ConversationActivity extends PassphraseRequiredActivity
messageRequestBottomView.setRecipient(recipient);
}
private static class KeyboardImageDetails {
private final int width;
private final int height;
private final boolean hasTransparency;
private KeyboardImageDetails(int width, int height, boolean hasTransparency) {
this.width = width;
this.height = height;
this.hasTransparency = hasTransparency;
}
}
}

View File

@ -81,6 +81,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
@ -168,6 +169,8 @@ public class ConversationFragment extends LoggingFragment {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_text_only, parent, 15);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_text_only, parent, 15);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
@ -692,19 +695,35 @@ public class ConversationFragment extends LoggingFragment {
});
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
SignalExecutors.BOUNDED.execute(() -> {
for (MessageRecord message : messageRecords) {
MessageSender.sendRemoteDelete(context, message.getId(), message.isMms());
}
});
});
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> handleDeleteForEveryone(messageRecords));
}
builder.setNegativeButton(android.R.string.cancel, null);
return builder;
}
private void handleDeleteForEveryone(Set<MessageRecord> messageRecords) {
Runnable deleteForEveryone = () -> {
SignalExecutors.BOUNDED.execute(() -> {
for (MessageRecord message : messageRecords) {
MessageSender.sendRemoteDelete(ApplicationDependencies.getApplication(), message.getId(), message.isMms());
}
});
};
if (SignalStore.uiHints().hasConfirmedDeleteForEveryoneOnce()) {
deleteForEveryone.run();
} else {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.ConversationFragment_this_message_will_be_permanently_deleted_for_everyone)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce();
deleteForEveryone.run();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}
private void handleDisplayDetails(MessageRecord message) {
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId));
@ -747,6 +766,7 @@ public class ConversationFragment extends LoggingFragment {
attachment.getHeight(),
attachment.getSize(),
0,
attachment.isBorderless(),
Optional.absent(),
Optional.fromNullable(attachment.getCaption()),
Optional.absent()));

View File

@ -36,7 +36,6 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
@ -70,7 +69,7 @@ import org.thoughtcrime.securesms.components.LinkPreviewView;
import org.thoughtcrime.securesms.components.Outliner;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.SharedContactView;
import org.thoughtcrime.securesms.components.StickerView;
import org.thoughtcrime.securesms.components.BorderlessImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -169,7 +168,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private Stub<DocumentView> documentViewStub;
private Stub<SharedContactView> sharedContactStub;
private Stub<LinkPreviewView> linkPreviewStub;
private Stub<StickerView> stickerStub;
private Stub<BorderlessImageView> stickerStub;
private Stub<ViewOnceMessageView> revealableStub;
private @Nullable EventListener eventListener;
@ -389,11 +388,11 @@ public class ConversationItem extends LinearLayout implements BindableConversati
/// MessageRecord Attribute Parsers
private void setBubbleState(MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
} else if (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord)) {
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
@ -456,7 +455,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) {
return !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
}
private boolean isCaptionlessMms(MessageRecord messageRecord) {
@ -475,12 +475,20 @@ public class ConversationItem extends LinearLayout implements BindableConversati
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() != null;
}
private boolean isBorderless(MessageRecord messageRecord) {
//noinspection ConstantConditions
return isCaptionlessMms(messageRecord) &&
hasThumbnail(messageRecord) &&
((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide().isBorderless();
}
private boolean hasOnlyThumbnail(MessageRecord messageRecord) {
return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) &&
!hasDocument(messageRecord) &&
!hasSharedContact(messageRecord) &&
!hasSticker(messageRecord) &&
!isBorderless(messageRecord) &&
!isViewOnceMessage(messageRecord);
}
@ -529,12 +537,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted);
String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted);
SpannableString italics = new SpannableString(deletedMessage);
italics.setSpan(new RelativeSizeSpan(0.9f), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new ForegroundColorSpan(ThemeUtil.getThemedColor(context, R.attr.conversation_item_delete_for_everyone_text_color)),
0,
deletedMessage.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
bodyText.setText(italics);
bodyText.setVisibility(View.VISIBLE);
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
@ -670,7 +682,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(VISIBLE);
} else if (hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) {
} else if ((hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) || isBorderless(messageRecord)) {
bodyBubble.setBackgroundColor(Color.TRANSPARENT);
stickerStub.get().setVisibility(View.VISIBLE);
@ -681,9 +693,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
stickerStub.get().setSticker(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide());
stickerStub.get().setThumbnailClickListener(new StickerClickListener());
if (hasSticker(messageRecord)) {
//noinspection ConstantConditions
stickerStub.get().setSlide(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide());
stickerStub.get().setThumbnailClickListener(new StickerClickListener());
} else {
//noinspection ConstantConditions
stickerStub.get().setSlide(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide());
stickerStub.get().setThumbnailClickListener((v, slide) -> performClick());
}
stickerStub.get().setDownloadClickListener(downloadClickListener);
stickerStub.get().setOnLongClickListener(passthroughClickListener);
stickerStub.get().setOnClickListener(passthroughClickListener);
@ -701,7 +720,6 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
mediaThumbnailStub.get().setImageResource(glideRequests,
thumbnailSlides,
@ -974,7 +992,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord)) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
return stickerFooter;
} else if (hasSharedContact(messageRecord)) {
return sharedContactStub.get().getFooter();
@ -1010,7 +1028,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (shouldDrawBodyBubbleOutline(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else if (hasSticker(messageRecord)) {
} else if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else {
@ -1307,7 +1325,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
public void onClick(View v, Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick();
} else if (eventListener != null && hasSticker(messageRecord)){
} else if (eventListener != null && hasSticker(messageRecord)) {
//noinspection ConstantConditions
eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().getSticker());
}

View File

@ -13,13 +13,16 @@ import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
@ -66,22 +69,26 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
int totalCount = getTotalCount();
int effectiveCount = params.requestedStartPosition;
List<Recipient> recipients = new LinkedList<>();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
effectiveCount++;
}
}
ApplicationDependencies.getRecipientCache().addToCache(recipients);
if (!isInvalid()) {
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
}
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
}
@Override
@ -89,17 +96,21 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.loadSize);
List<Recipient> recipients = new LinkedList<>();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(getCursor(params.startPosition, params.loadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
}
}
ApplicationDependencies.getRecipientCache().addToCache(recipients);
callback.onResult(conversations);
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | start: " + params.startPosition + ", size: " + params.loadSize + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
}
protected abstract int getTotalCount();

View File

@ -88,6 +88,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
@ -266,8 +267,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onStart() {
super.onStart();
// TODO [greyson] Re-enable when we figure out how to invalidate the cache after a system theme change
// ConversationFragment.prepare(requireContext());
ConversationFragment.prepare(requireContext());
}
@Override

View File

@ -453,7 +453,7 @@ public class ConversationListItem extends RelativeLayout
if (extra != null && extra.isViewOnce()) {
return new SpannableString(emphasisAdded(getViewOnceDescription(context, thread.getContentType())));
} else if (extra != null && extra.isRemoteDelete()) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_this_message_was_deleted)));
return new SpannableString(emphasisAdded(context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted)));
} else {
return new SpannableString(Util.emptyIfNull(thread.getBody()));
}

View File

@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
@ -112,6 +113,7 @@ public class AttachmentDatabase extends Database {
public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note";
static final String BORDERLESS = "borderless";
static final String QUOTE = "quote";
public static final String STICKER_PACK_ID = "sticker_pack_id";
public static final String STICKER_PACK_KEY = "sticker_pack_key";
@ -146,7 +148,7 @@ public class AttachmentDatabase extends Database {
CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL,
TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL,
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM,
FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM,
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, VISUAL_HASH,
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
@ -175,6 +177,7 @@ public class AttachmentDatabase extends Database {
DIGEST + " BLOB, " +
FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " +
BORDERLESS + " INTEGER DEFAULT 0, " +
DATA_RANDOM + " BLOB, " +
THUMBNAIL_RANDOM + " BLOB, " +
QUOTE + " INTEGER DEFAULT 0, " +
@ -1168,6 +1171,7 @@ public class AttachmentDatabase extends Database {
null,
object.getString(FAST_PREFLIGHT_ID),
object.getInt(VOICE_NOTE) == 1,
object.getInt(BORDERLESS) == 1,
object.getInt(WIDTH),
object.getInt(HEIGHT),
object.getInt(QUOTE) == 1,
@ -1204,6 +1208,7 @@ public class AttachmentDatabase extends Database {
cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)),
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1,
@ -1269,6 +1274,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(SIZE, template.getSize());
contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0);
contentValues.put(BORDERLESS, attachment.isBorderless() ? 1 : 0);
contentValues.put(WIDTH, template.getWidth());
contentValues.put(HEIGHT, template.getHeight());
contentValues.put(QUOTE, quote);

View File

@ -96,7 +96,7 @@ public final class GroupDatabase extends Database {
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
};
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();

View File

@ -37,6 +37,7 @@ public class MediaDatabase extends Database {
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", "

View File

@ -209,6 +209,7 @@ public class MmsDatabase extends MessagingDatabase {
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
"'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," +
"'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," +
"'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + "," +
"'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," +
"'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," +
"'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +

View File

@ -388,6 +388,7 @@ public class MmsSmsDatabase extends Database {
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
"'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " +
"'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " +
"'" + AttachmentDatabase.BORDERLESS + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BORDERLESS + ", " +
"'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " +
"'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " +
"'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +

View File

@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
@ -56,7 +57,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -522,6 +522,7 @@ public class RecipientDatabase extends Database {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Set<RecipientId> needsRefresh = new HashSet<>();
db.beginTransaction();
@ -556,7 +557,7 @@ public class RecipientDatabase extends Database {
}
threadDatabase.setArchived(recipientId, insert.isArchived());
Recipient.live(recipientId).refresh();
needsRefresh.add(recipientId);
}
}
@ -598,7 +599,7 @@ public class RecipientDatabase extends Database {
}
threadDatabase.setArchived(recipientId, update.getNew().isArchived());
Recipient.live(recipientId).refresh();
needsRefresh.add(recipientId);
}
for (SignalGroupV1Record insert : groupV1Inserts) {
@ -607,7 +608,7 @@ public class RecipientDatabase extends Database {
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
recipient.live().refresh();
needsRefresh.add(recipient.getId());
}
for (RecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
@ -621,7 +622,7 @@ public class RecipientDatabase extends Database {
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
recipient.live().refresh();
needsRefresh.add(recipient.getId());
}
for (SignalGroupV2Record insert : groupV2Inserts) {
@ -633,7 +634,7 @@ public class RecipientDatabase extends Database {
ApplicationDependencies.getJobManager().add(new WakeGroupV2Job(insert.getMasterKey()));
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
recipient.live().refresh();
needsRefresh.add(recipient.getId());
}
for (RecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
@ -647,13 +648,17 @@ public class RecipientDatabase extends Database {
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(update.getOld().getMasterKey()));
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
recipient.live().refresh();
needsRefresh.add(recipient.getId());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
for (RecipientId id : needsRefresh) {
Recipient.live(id).refresh();
}
}
public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) {
@ -822,44 +827,45 @@ public class RecipientDatabase extends Database {
return out;
}
private static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
GroupId groupId = GroupId.parseNullableOrThrow(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)));
int groupType = cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE));
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
static @NonNull RecipientSettings getRecipientSettings(@NonNull Context context, @NonNull Cursor cursor) {
long id = CursorUtil.requireLong(cursor, ID);
UUID uuid = UuidUtil.parseOrNull(CursorUtil.requireString(cursor, UUID));
String username = CursorUtil.requireString(cursor, USERNAME);
String e164 = CursorUtil.requireString(cursor, PHONE);
String email = CursorUtil.requireString(cursor, EMAIL);
GroupId groupId = GroupId.parseNullableOrThrow(CursorUtil.requireString(cursor, GROUP_ID));
int groupType = CursorUtil.requireInt(cursor, GROUP_TYPE);
boolean blocked = CursorUtil.requireBoolean(cursor, BLOCKED);
String messageRingtone = CursorUtil.requireString(cursor, MESSAGE_RINGTONE);
String callRingtone = CursorUtil.requireString(cursor, CALL_RINGTONE);
int messageVibrateState = CursorUtil.requireInt(cursor, MESSAGE_VIBRATE);
int callVibrateState = CursorUtil.requireInt(cursor, CALL_VIBRATE);
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String profileKeyCredentialString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY_CREDENTIAL));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String serializedColor = CursorUtil.requireString(cursor, COLOR);
int insightsBannerTier = CursorUtil.requireInt(cursor, SEEN_INVITE_REMINDER);
int defaultSubscriptionId = CursorUtil.requireInt(cursor, DEFAULT_SUBSCRIPTION_ID);
int expireMessages = CursorUtil.requireInt(cursor, MESSAGE_EXPIRATION_TIME);
int registeredState = CursorUtil.requireInt(cursor, REGISTERED);
String profileKeyString = CursorUtil.requireString(cursor, PROFILE_KEY);
String profileKeyCredentialString = CursorUtil.requireString(cursor, PROFILE_KEY_CREDENTIAL);
String systemDisplayName = CursorUtil.requireString(cursor, SYSTEM_DISPLAY_NAME);
String systemContactPhoto = CursorUtil.requireString(cursor, SYSTEM_PHOTO_URI);
String systemPhoneLabel = CursorUtil.requireString(cursor, SYSTEM_PHONE_LABEL);
String systemContactUri = CursorUtil.requireString(cursor, SYSTEM_CONTACT_URI);
String profileGivenName = CursorUtil.requireString(cursor, PROFILE_GIVEN_NAME);
String profileFamilyName = CursorUtil.requireString(cursor, PROFILE_FAMILY_NAME);
String signalProfileAvatar = CursorUtil.requireString(cursor, SIGNAL_PROFILE_AVATAR);
boolean profileSharing = CursorUtil.requireBoolean(cursor, PROFILE_SHARING);
long lastProfileFetch = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_PROFILE_FETCH));
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
int uuidCapabilityValue = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_CAPABILITY));
int groupsV2CapabilityValue = cursor.getInt(cursor.getColumnIndexOrThrow(GROUPS_V2_CAPABILITY));
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_ID));
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
String notificationChannel = CursorUtil.requireString(cursor, NOTIFICATION_CHANNEL);
int unidentifiedAccessMode = CursorUtil.requireInt(cursor, UNIDENTIFIED_ACCESS_MODE);
boolean forceSmsSelection = CursorUtil.requireBoolean(cursor, FORCE_SMS_SELECTION);
int uuidCapabilityValue = CursorUtil.requireInt(cursor, UUID_CAPABILITY);
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
GroupMasterKey groupMasterKey = null;
@ -904,9 +910,9 @@ public class RecipientDatabase extends Database {
}
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
byte[] identityKey = identityKeyRaw != null ? Base64.decodeOrThrow(identityKeyRaw) : null;
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();;
IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw);
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
VibrateState.fromId(messageVibrateState),
@ -1314,10 +1320,14 @@ public class RecipientDatabase extends Database {
return results;
}
public void markRegistered(@NonNull RecipientId id, @NonNull UUID uuid) {
ContentValues contentValues = new ContentValues(3);
public void markRegistered(@NonNull RecipientId id, @Nullable UUID uuid) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
contentValues.put(UUID, uuid.toString().toLowerCase());
if (uuid != null) {
contentValues.put(UUID, uuid.toString().toLowerCase());
}
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
Recipient.live(id).refresh();
@ -1330,7 +1340,7 @@ public class RecipientDatabase extends Database {
* preferred.
*/
public void markRegistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
@ -1341,7 +1351,7 @@ public class RecipientDatabase extends Database {
public void markUnregistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
contentValues.put(UUID, (String) null);
contentValues.putNull(UUID);
if (update(id, contentValues)) {
markDirty(id, DirtyState.DELETE);
Recipient.live(id).refresh();
@ -1356,14 +1366,18 @@ public class RecipientDatabase extends Database {
for (Map.Entry<RecipientId, String> entry : registered.entrySet()) {
ContentValues values = new ContentValues(2);
values.put(REGISTERED, RegisteredState.REGISTERED.getId());
values.put(UUID, entry.getValue().toLowerCase());
if (entry.getValue() != null) {
values.put(UUID, entry.getValue().toLowerCase());
}
if (update(entry.getKey(), values)) {
markDirty(entry.getKey(), DirtyState.INSERT);
}
}
for (RecipientId id : unregistered) {
ContentValues values = new ContentValues(1);
ContentValues values = new ContentValues(2);
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
values.put(UUID, (String) null);
if (update(id, values)) {
@ -1817,7 +1831,7 @@ public class RecipientDatabase extends Database {
}
private static ContentValues validateContactValuesForInsert(ContentValues values) {
if (!FeatureFlags.uuids() &&
if (!FeatureFlags.uuidOnlyContacts() &&
values.getAsString(UUID) != null &&
values.getAsString(PHONE) == null)
{

View File

@ -621,13 +621,13 @@ public class SmsDatabase extends MessagingDatabase {
long messageId = db.insert(TABLE_NAME, null, values);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
if (unread) {
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
}
notifyConversationListeners(threadId);
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
return new Pair<>(messageId, threadId);
}

View File

@ -33,6 +33,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -42,9 +43,11 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientDetails;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -308,7 +311,11 @@ public class ThreadDatabase extends Database {
}
public boolean hasCalledSince(@NonNull Recipient recipient, long timestamp) {
return DatabaseFactory.getMmsSmsDatabase(context).hasReceivedAnyCallsSince(getThreadIdFor(recipient), timestamp);
return hasReceivedAnyCallsSince(getThreadIdFor(recipient), timestamp);
}
public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) {
return DatabaseFactory.getMmsSmsDatabase(context).hasReceivedAnyCallsSince(threadId, timestamp);
}
public List<MarkedMessageInfo> setEntireThreadRead(long threadId) {
@ -928,9 +935,29 @@ public class ThreadDatabase extends Database {
}
public ThreadRecord getCurrent() {
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID)));
Recipient recipient = Recipient.live(recipientId).get();
RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID));
RecipientSettings recipientSettings = RecipientDatabase.getRecipientSettings(context, cursor);
Recipient recipient;
if (recipientSettings.getGroupId() != null) {
GroupDatabase.GroupRecord group = new GroupDatabase.Reader(cursor).getCurrent();
if (group != null) {
RecipientDetails details = new RecipientDetails(group.getTitle(),
group.hasAvatar() ? Optional.of(group.getAvatarId()) : Optional.absent(),
false,
false,
recipientSettings,
null);
recipient = new Recipient(recipientId, details, false);
} else {
recipient = Recipient.live(recipientId).get();
}
} else {
RecipientDetails details = RecipientDetails.forIndividual(context, recipientSettings);
recipient = new Recipient(recipientId, details, false);
}
int readReceiptCount = TextSecurePreferences.isReadReceiptsEnabled(context) ? cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ_RECEIPT_COUNT))
: 0;

View File

@ -21,8 +21,6 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
@ -138,8 +136,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int LAST_PROFILE_FETCH = 63;
private static final int SERVER_DELIVERED_TIMESTAMP = 64;
private static final int QUOTE_CLEANUP = 65;
private static final int BORDERLESS = 66;
private static final int DATABASE_VERSION = 65;
private static final int DATABASE_VERSION = 66;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -955,6 +954,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
Log.i(TAG, "[QuoteCleanup] Cleaned up " + count + " quotes.");
}
if (oldVersion < BORDERLESS) {
db.execSQL("ALTER TABLE part ADD COLUMN borderless INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -78,20 +78,39 @@ final class GroupsV2UpdateMessageProducer {
List<String> describeChange(@NonNull DecryptedGroupChange change) {
List<String> updates = new LinkedList<>();
describeMemberAdditions(change, updates);
describeMemberRemovals(change, updates);
describeModifyMemberRoles(change, updates);
describeInvitations(change, updates);
describeRevokedInvitations(change, updates);
describePromotePending(change, updates);
describeNewTitle(change, updates);
describeNewAvatar(change, updates);
describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates);
if (change.getEditor().isEmpty() || UuidUtil.UNKNOWN_UUID.equals(UuidUtil.fromByteString(change.getEditor()))) {
describeUnknownEditorMemberAdditions(change, updates);
describeUnknownEditorMemberRemovals(change, updates);
describeUnknownEditorModifyMemberRoles(change, updates);
describeUnknownEditorInvitations(change, updates);
describeUnknownEditorRevokedInvitations(change, updates);
describeUnknownEditorPromotePending(change, updates);
describeUnknownEditorNewTitle(change, updates);
describeUnknownEditorNewAvatar(change, updates);
describeUnknownEditorNewTimer(change, updates);
describeUnknownEditorNewAttributeAccess(change, updates);
describeUnknownEditorNewMembershipAccess(change, updates);
if (updates.isEmpty()) {
describeUnknownChange(change, updates);
if (updates.isEmpty()) {
describeUnknownEditorUnknownChange(updates);
}
} else {
describeMemberAdditions(change, updates);
describeMemberRemovals(change, updates);
describeModifyMemberRoles(change, updates);
describeInvitations(change, updates);
describeRevokedInvitations(change, updates);
describePromotePending(change, updates);
describeNewTitle(change, updates);
describeNewAvatar(change, updates);
describeNewTimer(change, updates);
describeNewAttributeAccess(change, updates);
describeNewMembershipAccess(change, updates);
if (updates.isEmpty()) {
describeUnknownChange(change, updates);
}
}
return updates;
@ -110,6 +129,10 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorUnknownChange(@NonNull List<String> updates) {
updates.add(context.getString(R.string.MessageRecord_the_group_was_updated));
}
private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -136,6 +159,18 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
for (DecryptedMember member : change.getNewMembersList()) {
boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(member.getUuid())));
}
}
}
private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -162,16 +197,28 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
for (ByteString member : change.getDeleteMembersList()) {
boolean removedMemberIsYou = member.equals(selfUuidBytes);
if (removedMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_in_the_group, describe(member)));
}
}
}
private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
boolean newMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_made_s_an_admin, describe(roleChange.getUuid())));
} else {
if (newMemberIsYou) {
if (changedMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_made_you_an_admin, describe(change.getEditor())));
} else {
updates.add(context.getString(R.string.MessageRecord_s_made_s_an_admin, describe(change.getEditor()), describe(roleChange.getUuid())));
@ -179,11 +226,10 @@ final class GroupsV2UpdateMessageProducer {
}
}
} else {
boolean newMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (editorIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_revoked_admin_privileges_from_s, describe(roleChange.getUuid())));
} else {
if (newMemberIsYou) {
if (changedMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_s_revoked_your_admin_privileges, describe(change.getEditor())));
} else {
updates.add(context.getString(R.string.MessageRecord_s_revoked_admin_privileges_from_s, describe(change.getEditor()), describe(roleChange.getUuid())));
@ -193,6 +239,26 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) {
boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes);
if (roleChange.getRole() == Member.Role.ADMINISTRATOR) {
if (changedMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_are_now_an_admin));
} else {
updates.add(context.getString(R.string.MessageRecord_s_is_now_an_admin, describe(roleChange.getUuid())));
}
} else {
if (changedMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin));
} else {
updates.add(context.getString(R.string.MessageRecord_s_is_no_longer_an_admin, describe(roleChange.getUuid())));
}
}
}
}
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
int notYouInviteCount = 0;
@ -216,6 +282,24 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
int notYouInviteCount = 0;
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_were_invited_to_the_group));
} else {
notYouInviteCount++;
}
}
if (notYouInviteCount > 0) {
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, notYouInviteCount, notYouInviteCount));
}
}
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
int notDeclineCount = 0;
@ -242,6 +326,24 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
int notDeclineCount = 0;
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes);
if (inviteeWasYou) {
updates.add(context.getString(R.string.MessageRecord_your_invitation_to_the_group_was_revoked));
} else {
notDeclineCount++;
}
}
if (notDeclineCount > 0) {
updates.add(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notDeclineCount, notDeclineCount));
}
}
private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -269,6 +371,19 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
ByteString uuid = newMember.getUuid();
boolean newMemberIsYou = uuid.equals(selfUuidBytes);
if (newMemberIsYou) {
updates.add(context.getString(R.string.MessageRecord_you_joined_the_group));
} else {
updates.add(context.getString(R.string.MessageRecord_s_joined_the_group, describe(uuid)));
}
}
}
private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -281,6 +396,12 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
if (change.hasNewTitle()) {
updates.add(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, change.getNewTitle().getValue()));
}
}
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -293,6 +414,12 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
if (change.hasNewAvatar()) {
updates.add(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed));
}
}
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -306,6 +433,13 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
if (change.hasNewTimer()) {
String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration());
updates.add(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time));
}
}
private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -319,6 +453,13 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess());
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel));
}
}
private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
@ -332,6 +473,13 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeUnknownEditorNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List<String> updates) {
if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) {
String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess());
updates.add(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel));
}
}
private @NonNull String describe(@NonNull ByteString uuid) {
return descriptionStrategy.describe(UuidUtil.fromByteString(uuid));
}

View File

@ -19,6 +19,7 @@ public class WebRtcViewModel {
CALL_RINGING,
CALL_BUSY,
CALL_DISCONNECTED,
CALL_NEEDS_PERMISSION,
// Error states
NETWORK_FAILURE,

View File

@ -14,6 +14,13 @@ public class GiphyImage {
@JsonProperty
private ImageTypes images;
@JsonProperty("is_sticker")
private boolean isSticker;
public boolean isSticker() {
return isSticker;
}
public String getGifUrl() {
ImageData data = getGifData();
return data != null ? data.url : null;

View File

@ -42,10 +42,11 @@ public class GiphyActivity extends PassphraseRequiredActivity
private static final String TAG = GiphyActivity.class.getSimpleName();
public static final String EXTRA_IS_MMS = "extra_is_mms";
public static final String EXTRA_WIDTH = "extra_width";
public static final String EXTRA_HEIGHT = "extra_height";
public static final String EXTRA_COLOR = "extra_color";
public static final String EXTRA_IS_MMS = "extra_is_mms";
public static final String EXTRA_WIDTH = "extra_width";
public static final String EXTRA_HEIGHT = "extra_height";
public static final String EXTRA_COLOR = "extra_color";
public static final String EXTRA_BORDERLESS = "extra_borderless";
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@ -151,6 +152,7 @@ public class GiphyActivity extends PassphraseRequiredActivity
intent.setData(uri);
intent.putExtra(EXTRA_WIDTH, viewHolder.image.getGifWidth());
intent.putExtra(EXTRA_HEIGHT, viewHolder.image.getGifHeight());
intent.putExtra(EXTRA_BORDERLESS, viewHolder.image.isSticker());
setResult(RESULT_OK, intent);
finish();
} else {

View File

@ -138,7 +138,7 @@ final class GroupManagerV1 {
if (avatar != null) {
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null, null);
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, false, null, null, null, null, null);
}
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());

View File

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@ -117,6 +118,10 @@ final class GroupManagerV2 {
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
if (SignalStore.internalValues().forceGv2Invites()) {
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
}
if (!self.hasProfileKeyCredential()) {
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
@ -186,6 +191,11 @@ final class GroupManagerV2 {
}
Set<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidates(new HashSet<>(newMembers));
if (SignalStore.internalValues().forceGv2Invites()) {
groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates);
}
return commitChangeWithConflictResolution(groupOperations.createModifyGroupMembershipChange(groupCandidates, selfUuid));
}

View File

@ -3,7 +3,12 @@ package org.thoughtcrime.securesms.groups;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -12,13 +17,38 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
final class GroupsV2CapabilityChecker {
public final class GroupsV2CapabilityChecker {
private static final String TAG = Log.tag(GroupsV2CapabilityChecker.class);
GroupsV2CapabilityChecker() {
public GroupsV2CapabilityChecker() {}
/**
* @param resolved A collection of resolved recipients.
*/
@WorkerThread
public void refreshCapabilitiesIfNecessary(@NonNull Collection<Recipient> resolved) throws IOException {
List<RecipientId> needsRefresh = Stream.of(resolved)
.filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED)
.map(Recipient::getId)
.toList();
if (needsRefresh.size() > 0) {
Log.d(TAG, "[refreshCapabilitiesIfNecessary] Need to refresh " + needsRefresh.size() + " recipients.");
List<Job> jobs = RetrieveProfileJob.forRecipients(needsRefresh);
JobManager jobManager = ApplicationDependencies.getJobManager();
for (Job job : jobs) {
if (!jobManager.runSynchronously(job, TimeUnit.SECONDS.toMillis(5000)).isPresent()) {
throw new IOException("Recipient capability was not retrieved in time");
}
}
}
}
@WorkerThread
@ -36,25 +66,16 @@ final class GroupsV2CapabilityChecker {
boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
throws IOException
{
final HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
Set<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
Set<Recipient> resolved = Stream.of(recipientIdsSet).map(Recipient::resolved).collect(Collectors.toSet());
for (RecipientId recipientId : recipientIdsSet) {
Recipient member = Recipient.resolved(recipientId);
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
if (gv2Capability != Recipient.Capability.SUPPORTED) {
if (!ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob.forRecipient(member.getId()), TimeUnit.SECONDS.toMillis(1000)).isPresent()) {
throw new IOException("Recipient capability was not retrieved in time");
}
}
}
refreshCapabilitiesIfNecessary(resolved);
boolean noSelfGV2Support = false;
int noGv2Count = 0;
int noUuidCount = 0;
for (RecipientId recipientId : recipientIdsSet) {
Recipient member = Recipient.resolved(recipientId);
for (Recipient member : resolved) {
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
if (gv2Capability != Recipient.Capability.SUPPORTED) {

View File

@ -8,6 +8,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.annimon.stream.Stream;
@ -15,14 +16,37 @@ import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.GroupsV2CapabilityChecker;
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
public class CreateGroupActivity extends ContactSelectionActivity {
private static String TAG = Log.tag(CreateGroupActivity.class);
private static final int MINIMUM_GROUP_SIZE = 1;
private static final short REQUEST_CODE_ADD_DETAILS = 17275;
@ -109,10 +133,63 @@ public class CreateGroupActivity extends ContactSelectionActivity {
}
private void handleNextPressed() {
RecipientId[] ids = Stream.of(contactsFragment.getSelectedContacts())
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.toArray(RecipientId[]::new);
Stopwatch stopwatch = new Stopwatch("Recipient Refresh");
AtomicReference<AlertDialog> progressDialog = new AtomicReference<>();
startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
Runnable showDialogRunnable = () -> {
Log.i(TAG, "Taking some time. Showing a progress dialog.");
progressDialog.set(SimpleProgressDialog.show(this));
};
next.postDelayed(showDialogRunnable, 300);
SimpleTask.run(getLifecycle(), () -> {
RecipientId[] ids = Stream.of(contactsFragment.getSelectedContacts())
.map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
.toArray(RecipientId[]::new);
List<Recipient> resolved = Stream.of(ids)
.map(Recipient::resolved)
.toList();
stopwatch.split("resolve");
List<Recipient> registeredChecks = Stream.of(resolved)
.filter(r -> r.getRegistered() == RecipientDatabase.RegisteredState.UNKNOWN)
.toList();
Log.i(TAG, "Need to do " + registeredChecks.size() + " registration checks.");
for (Recipient recipient : registeredChecks) {
try {
DirectoryHelper.refreshDirectoryFor(this, recipient, false);
} catch (IOException e) {
Log.w(TAG, "Failed to refresh registered status for " + recipient.getId(), e);
}
}
stopwatch.split("registered");
if (FeatureFlags.groupsV2()) {
try {
new GroupsV2CapabilityChecker().refreshCapabilitiesIfNecessary(resolved);
} catch (IOException e) {
Log.w(TAG, "Failed to refresh all recipient capabilities.", e);
}
}
stopwatch.split("capabilities");
return ids;
}, ids -> {
if (progressDialog.get() != null) {
progressDialog.get().dismiss();
}
next.removeCallbacks(showDialogRunnable);
stopwatch.stop(TAG);
startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
});
}
}

View File

@ -49,9 +49,8 @@ import java.util.Objects;
public class AddGroupDetailsFragment extends LoggingFragment {
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
private static final short REQUEST_CODE_AVATAR = 27621;
private static final String ARG_RECIPIENT_IDS = "recipient_ids";
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
private static final short REQUEST_CODE_AVATAR = 27621;
private CircularProgressButton create;
private Callback callback;
@ -71,16 +70,6 @@ public class AddGroupDetailsFragment extends LoggingFragment {
}
}
public static Fragment create(@NonNull RecipientId[] recipientIds) {
AddGroupDetailsFragment fragment = new AddGroupDetailsFragment();
Bundle arguments = new Bundle();
arguments.putParcelableArray(ARG_RECIPIENT_IDS, recipientIds);
fragment.setArguments(arguments);
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,

View File

@ -9,17 +9,17 @@ import java.util.Collection;
*/
final class AdvanceGroupStateResult {
@NonNull private final Collection<GroupLogEntry> processedLogEntries;
@NonNull private final GlobalGroupState newGlobalGroupState;
@NonNull private final Collection<LocalGroupLogEntry> processedLogEntries;
@NonNull private final GlobalGroupState newGlobalGroupState;
AdvanceGroupStateResult(@NonNull Collection<GroupLogEntry> processedLogEntries,
AdvanceGroupStateResult(@NonNull Collection<LocalGroupLogEntry> processedLogEntries,
@NonNull GlobalGroupState newGlobalGroupState)
{
this.processedLogEntries = processedLogEntries;
this.newGlobalGroupState = newGlobalGroupState;
}
@NonNull Collection<GroupLogEntry> getProcessedLogEntries() {
@NonNull Collection<LocalGroupLogEntry> getProcessedLogEntries() {
return processedLogEntries;
}

View File

@ -13,31 +13,42 @@ import java.util.List;
*/
final class GlobalGroupState {
@Nullable private final DecryptedGroup localState;
@NonNull private final List<GroupLogEntry> history;
@Nullable private final DecryptedGroup localState;
@NonNull private final List<ServerGroupLogEntry> serverHistory;
GlobalGroupState(@Nullable DecryptedGroup localState,
@NonNull List<GroupLogEntry> serverStates)
@NonNull List<ServerGroupLogEntry> serverHistory)
{
this.localState = localState;
this.history = serverStates;
this.localState = localState;
this.serverHistory = serverHistory;
}
@Nullable DecryptedGroup getLocalState() {
return localState;
}
@NonNull Collection<GroupLogEntry> getHistory() {
return history;
@NonNull Collection<ServerGroupLogEntry> getServerHistory() {
return serverHistory;
}
int getEarliestRevisionNumber() {
if (localState != null) {
return localState.getRevision();
} else {
if (serverHistory.isEmpty()) {
throw new AssertionError();
}
return serverHistory.get(0).getRevision();
}
}
int getLatestRevisionNumber() {
if (history.isEmpty()) {
if (serverHistory.isEmpty()) {
if (localState == null) {
throw new AssertionError();
}
return localState.getRevision();
}
return history.get(history.size() - 1).getGroup().getRevision();
return serverHistory.get(serverHistory.size() - 1).getRevision();
}
}

View File

@ -3,16 +3,24 @@ package org.thoughtcrime.securesms.groups.v2.processing;
import androidx.annotation.NonNull;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Objects;
final class GroupStateMapper {
private static final String TAG = Log.tag(GroupStateMapper.class);
static final int LATEST = Integer.MAX_VALUE;
private static final Comparator<GroupLogEntry> BY_REVISION = (o1, o2) -> Integer.compare(o1.getGroup().getRevision(), o2.getGroup().getRevision());
private static final Comparator<ServerGroupLogEntry> BY_REVISION = (o1, o2) -> Integer.compare(o1.getRevision(), o2.getRevision());
private GroupStateMapper() {
}
@ -27,37 +35,88 @@ final class GroupStateMapper {
static @NonNull AdvanceGroupStateResult partiallyAdvanceGroupState(@NonNull GlobalGroupState inputState,
int maximumRevisionToApply)
{
final ArrayList<GroupLogEntry> statesToApplyNow = new ArrayList<>(inputState.getHistory().size());
final ArrayList<GroupLogEntry> statesToApplyLater = new ArrayList<>(inputState.getHistory().size());
final DecryptedGroup newLocalState;
final GlobalGroupState newGlobalGroupState;
ArrayList<LocalGroupLogEntry> appliedChanges = new ArrayList<>(inputState.getServerHistory().size());
HashMap<Integer, ServerGroupLogEntry> statesToApplyNow = new HashMap<>(inputState.getServerHistory().size());
ArrayList<ServerGroupLogEntry> statesToApplyLater = new ArrayList<>(inputState.getServerHistory().size());
DecryptedGroup current = inputState.getLocalState();
for (GroupLogEntry entry : inputState.getHistory()) {
if (inputState.getLocalState() != null &&
inputState.getLocalState().getRevision() >= entry.getGroup().getRevision())
{
if (inputState.getServerHistory().isEmpty()) {
return new AdvanceGroupStateResult(Collections.emptyList(), new GlobalGroupState(current, Collections.emptyList()));
}
for (ServerGroupLogEntry entry : inputState.getServerHistory()) {
if (entry.getRevision() > maximumRevisionToApply) {
statesToApplyLater.add(entry);
} else {
statesToApplyNow.put(entry.getRevision(), entry);
}
}
Collections.sort(statesToApplyLater, BY_REVISION);
final int from = inputState.getEarliestRevisionNumber();
final int to = Math.min(inputState.getLatestRevisionNumber(), maximumRevisionToApply);
for (int revision = from; revision >= 0 && revision <= to; revision++) {
ServerGroupLogEntry entry = statesToApplyNow.get(revision);
if (entry == null) {
Log.w(TAG, "Could not find group log on server V" + revision);
continue;
}
if (entry.getGroup().getRevision() > maximumRevisionToApply) {
statesToApplyLater.add(entry);
} else {
statesToApplyNow.add(entry);
DecryptedGroup groupAtRevision = entry.getGroup();
DecryptedGroupChange changeAtRevision = entry.getChange();
if (current == null) {
Log.w(TAG, "No local state, accepting server state for V" + revision);
current = groupAtRevision;
if (groupAtRevision != null) {
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, changeAtRevision));
}
continue;
}
if (current.getRevision() + 1 != revision) {
Log.w(TAG, "Detected gap V" + revision);
}
if (changeAtRevision == null) {
Log.w(TAG, "Reconstructing change for V" + revision);
changeAtRevision = GroupChangeReconstruct.reconstructGroupChange(current, Objects.requireNonNull(groupAtRevision));
}
DecryptedGroup groupWithChangeApplied;
try {
groupWithChangeApplied = DecryptedGroupUtil.applyWithoutRevisionCheck(current, changeAtRevision);
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
Log.w(TAG, "Unable to apply V" + revision, e);
continue;
}
if (groupAtRevision == null) {
Log.w(TAG, "Reconstructing state for V" + revision);
groupAtRevision = groupWithChangeApplied;
}
if (current.getRevision() != groupAtRevision.getRevision()) {
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, changeAtRevision));
} else {
DecryptedGroupChange sameRevisionDelta = GroupChangeReconstruct.reconstructGroupChange(current, groupAtRevision);
if (!DecryptedGroupUtil.changeIsEmpty(sameRevisionDelta)) {
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, sameRevisionDelta));
Log.w(TAG, "Inserted repair change for mismatch V" + revision);
}
}
DecryptedGroupChange missingChanges = GroupChangeReconstruct.reconstructGroupChange(groupWithChangeApplied, groupAtRevision);
if (!DecryptedGroupUtil.changeIsEmpty(missingChanges)) {
appliedChanges.add(new LocalGroupLogEntry(groupAtRevision, missingChanges));
Log.w(TAG, "Inserted repair change for gap V" + revision);
}
current = groupAtRevision;
}
Collections.sort(statesToApplyNow, BY_REVISION);
Collections.sort(statesToApplyLater, BY_REVISION);
if (statesToApplyNow.size() > 0) {
newLocalState = statesToApplyNow.get(statesToApplyNow.size() - 1)
.getGroup();
} else {
newLocalState = inputState.getLocalState();
}
newGlobalGroupState = new GlobalGroupState(newLocalState, statesToApplyLater);
return new AdvanceGroupStateResult(statesToApplyNow, newGlobalGroupState);
return new AdvanceGroupStateResult(appliedChanges, new GlobalGroupState(current, statesToApplyLater));
}
}

View File

@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
@ -99,7 +100,7 @@ public final class GroupsV2StateProcessor {
public static class GroupUpdateResult {
private final GroupState groupState;
@Nullable private DecryptedGroup latestServer;
@Nullable private final DecryptedGroup latestServer;
GroupUpdateResult(@NonNull GroupState groupState, @Nullable DecryptedGroup latestServer) {
this.groupState = groupState;
@ -152,13 +153,17 @@ public final class GroupsV2StateProcessor {
localState.getRevision() + 1 == signedGroupChange.getRevision() &&
revision == signedGroupChange.getRevision())
{
try {
Log.i(TAG, "Applying P2P group change");
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
Log.w(TAG, "Ignoring P2P group change by setting");
} else {
try {
Log.i(TAG, "Applying P2P group change");
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new GroupLogEntry(newState, signedGroupChange)));
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
Log.w(TAG, "Unable to apply P2P group change", e);
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange)));
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
Log.w(TAG, "Unable to apply P2P group change", e);
}
}
}
@ -186,7 +191,7 @@ public final class GroupsV2StateProcessor {
persistLearnedProfileKeys(inputGroupState);
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
if (remainingWork.getHistory().size() > 0) {
if (remainingWork.getServerHistory().size() > 0) {
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, not applying at this time, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber()));
}
@ -270,18 +275,22 @@ public final class GroupsV2StateProcessor {
}
}
private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) {
for (GroupLogEntry entry : processedLogEntries) {
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
private void insertUpdateMessages(long timestamp, Collection<LocalGroupLogEntry> processedLogEntries) {
for (LocalGroupLogEntry entry : processedLogEntries) {
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) {
Log.d(TAG, "Skipping profile key changes only update message");
} else {
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
}
}
}
private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupState) {
final ProfileKeySet profileKeys = new ProfileKeySet();
for (GroupLogEntry entry : globalGroupState.getHistory()) {
for (ServerGroupLogEntry entry : globalGroupState.getServerHistory()) {
Optional<UUID> editor = DecryptedGroupUtil.editorUuid(entry.getChange());
if (editor.isPresent()) {
if (editor.isPresent() && entry.getGroup() != null) {
profileKeys.addKeysFromGroupState(entry.getGroup(), editor.get());
}
}
@ -297,9 +306,9 @@ public final class GroupsV2StateProcessor {
private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState, boolean latestOnly)
throws IOException, GroupNotAMemberException
{
DecryptedGroup latestServerGroup;
List<GroupLogEntry> history;
UUID selfUuid = Recipient.self().getUuid().get();
UUID selfUuid = Recipient.self().getUuid().get();
DecryptedGroup latestServerGroup;
List<ServerGroupLogEntry> history;
try {
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
@ -310,7 +319,7 @@ public final class GroupsV2StateProcessor {
}
if (latestOnly || !GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) {
history = Collections.singletonList(new GroupLogEntry(latestServerGroup, null));
history = Collections.singletonList(new ServerGroupLogEntry(latestServerGroup, null));
} else {
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfUuid);
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
@ -321,13 +330,18 @@ public final class GroupsV2StateProcessor {
return new GlobalGroupState(localState, history);
}
private List<GroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFromRevision) throws IOException {
private List<ServerGroupLogEntry> getFullMemberHistory(@NonNull UUID selfUuid, int logsNeededFromRevision) throws IOException {
try {
Collection<DecryptedGroupHistoryEntry> groupStatesFromRevision = groupsV2Api.getGroupHistory(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
ArrayList<GroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupStatesFromRevision.size());
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
if (ignoreServerChanges) {
Log.w(TAG, "Server change logs are ignored by setting");
}
for (DecryptedGroupHistoryEntry entry : groupStatesFromRevision) {
history.add(new GroupLogEntry(entry.getGroup(), entry.getChange()));
history.add(new ServerGroupLogEntry(entry.getGroup(), ignoreServerChanges ? null : entry.getChange()));
}
return history;
@ -339,12 +353,7 @@ public final class GroupsV2StateProcessor {
private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
Optional<UUID> editor = getEditor(decryptedGroupV2Context);
if (!editor.isPresent() || UuidUtil.UNKNOWN_UUID.equals(editor.get())) {
Log.w(TAG, "Cannot determine editor of change, can't insert message");
return;
}
boolean outgoing = Recipient.self().requireUuid().equals(editor.get());
boolean outgoing = !editor.isPresent() || Recipient.self().requireUuid().equals(editor.get());
if (outgoing) {
try {

View File

@ -6,17 +6,21 @@ import androidx.annotation.Nullable;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import java.util.Objects;
/**
* Pair of a group state and optionally the corresponding change.
* <p>
* Similar to {@link ServerGroupLogEntry} but guaranteed to have a group state.
* <p>
* Changes are typically not available for pending members.
*/
final class GroupLogEntry {
final class LocalGroupLogEntry {
@NonNull private final DecryptedGroup group;
@Nullable private final DecryptedGroupChange change;
GroupLogEntry(@NonNull DecryptedGroup group, @Nullable DecryptedGroupChange change) {
LocalGroupLogEntry(@NonNull DecryptedGroup group, @Nullable DecryptedGroupChange change) {
if (change != null && group.getRevision() != change.getRevision()) {
throw new AssertionError();
}
@ -32,4 +36,20 @@ final class GroupLogEntry {
@Nullable DecryptedGroupChange getChange() {
return change;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LocalGroupLogEntry)) return false;
LocalGroupLogEntry other = (LocalGroupLogEntry) o;
return group.equals(other.group) && Objects.equals(change, other.change);
}
@Override
public int hashCode() {
int result = group.hashCode();
result = 31 * result + (change != null ? change.hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.groups.v2.processing;
import androidx.annotation.Nullable;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.logging.Log;
/**
* Pair of a group state and optionally the corresponding change from the server.
* <p>
* Either the group or change may be empty.
* <p>
* Changes are typically not available for pending members.
*/
final class ServerGroupLogEntry {
private static final String TAG = Log.tag(ServerGroupLogEntry.class);
@Nullable private final DecryptedGroup group;
@Nullable private final DecryptedGroupChange change;
ServerGroupLogEntry(@Nullable DecryptedGroup group, @Nullable DecryptedGroupChange change) {
if (change != null && group != null && group.getRevision() != change.getRevision()) {
Log.w(TAG, "Ignoring change with revision number not matching group");
change = null;
}
if (change == null && group == null) {
throw new AssertionError();
}
this.group = group;
this.change = change;
}
@Nullable DecryptedGroup getGroup() {
return group;
}
@Nullable DecryptedGroupChange getChange() {
return change;
}
int getRevision() {
if (group != null) return group.getRevision();
else if (change != null) return change.getRevision();
else throw new AssertionError();
}
}

View File

@ -145,7 +145,7 @@ public final class AttachmentCompressionJob extends BaseJob {
if (!constraints.isSatisfied(context, attachment)) {
throw new UndeliverableMessageException("Size constraints could not be met on video!");
}
} else if (MediaUtil.isHeic(attachment)) {
} else if (MediaUtil.isHeic(attachment) || MediaUtil.isHeif(attachment)) {
MediaStream converted = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, converted, false);
attachmentDatabase.markAttachmentAsTransformed(attachmentId);

View File

@ -200,6 +200,7 @@ public class AttachmentDownloadJob extends BaseJob {
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
attachment.isBorderless(),
Optional.absent(),
Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash),
attachment.getUploadTimestamp());

View File

@ -165,6 +165,7 @@ public final class AttachmentUploadJob extends BaseJob {
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withVoiceNote(attachment.isVoiceNote())
.withBorderless(attachment.isBorderless())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withUploadTimestamp(System.currentTimeMillis())

View File

@ -85,7 +85,7 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob {
attachment.deleteOnExit();
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent(), System.currentTimeMillis());
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, false, Optional.absent(), Optional.absent(), System.currentTimeMillis());
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream);

View File

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob;
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
@ -134,6 +135,7 @@ public final class JobManagerFactories {
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
// Dead jobs
put(FailingJob.KEY, new FailingJob.Factory());

View File

@ -229,7 +229,7 @@ public class MmsDownloadJob extends BaseJob {
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length, name, false, false, null, null, null, null, null));
part.getData().length, name, false, false, false, null, null, null, null, null));
}
}
}

View File

@ -14,12 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.util.StreamDetails;
public final class ProfileUploadJob extends BaseJob {
@ -52,15 +47,10 @@ public final class ProfileUploadJob extends BaseJob {
protected void onRun() throws Exception {
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
ProfileName profileName = Recipient.self().getProfileName();
String avatarPath = null;
String avatarPath;
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.versionedProfiles()) {
avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), avatar).orNull();
} else {
accountManager.setProfileName(profileKey, profileName.serialize());
avatarPath = accountManager.setProfileAvatar(profileKey, avatar).orNull();
}
avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), avatar).orNull();
}
DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), avatarPath);
@ -85,11 +75,10 @@ public final class ProfileUploadJob extends BaseJob {
public void onFailure() {
}
public static class Factory implements Job.Factory {
public static class Factory implements Job.Factory<ProfileUploadJob> {
@NonNull
@Override
public Job create(@NonNull Parameters parameters, @NonNull Data data) {
public @NonNull ProfileUploadJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new ProfileUploadJob(parameters);
}
}

View File

@ -1632,6 +1632,7 @@ public final class PushProcessMessageJob extends BaseJob {
String.valueOf(new SecureRandom().nextLong()),
false,
false,
false,
null,
stickerLocator,
null,

View File

@ -136,6 +136,7 @@ public abstract class PushSendJob extends SendJob {
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withVoiceNote(attachment.isVoiceNote())
.withBorderless(attachment.isBorderless())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
@ -206,6 +207,7 @@ public abstract class PushSendJob extends SendJob {
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
attachment.isBorderless(),
Optional.fromNullable(attachment.getCaption()),
Optional.fromNullable(attachment.getBlurHash()).transform(BlurHash::getHash),
attachment.getUploadTimestamp());

View File

@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
@ -93,7 +92,7 @@ public class RefreshOwnProfileJob extends BaseJob {
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.versionedProfiles() && !recipient.hasProfileKeyCredential()
return !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}

View File

@ -101,24 +101,11 @@ public class RetrieveProfileJob extends BaseJob {
*/
@WorkerThread
public static void enqueue(@NonNull Collection<RecipientId> recipientIds) {
Context context = ApplicationDependencies.getApplication();
JobManager jobManager = ApplicationDependencies.getJobManager();
List<RecipientId> combined = new LinkedList<>();
JobManager jobManager = ApplicationDependencies.getJobManager();
for (RecipientId recipientId : recipientIds) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isLocalNumber()) {
jobManager.add(new RefreshOwnProfileJob());
} else if (recipient.isGroup()) {
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
combined.addAll(Stream.of(recipients).map(Recipient::getId).toList());
} else {
combined.add(recipientId);
}
for (Job job : forRecipients(recipientIds)) {
jobManager.add(job);
}
jobManager.add(new RetrieveProfileJob(combined));
}
/**
@ -140,6 +127,33 @@ public class RetrieveProfileJob extends BaseJob {
}
}
/**
* Works for any RecipientId, whether it's an individual, group, or yourself.
*/
@WorkerThread
public static @NonNull List<Job> forRecipients(@NonNull Collection<RecipientId> recipientIds) {
Context context = ApplicationDependencies.getApplication();
List<RecipientId> combined = new LinkedList<>();
List<Job> jobs = new LinkedList<>();
for (RecipientId recipientId : recipientIds) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isLocalNumber()) {
jobs.add(new RefreshOwnProfileJob());
} else if (recipient.isGroup()) {
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
combined.addAll(Stream.of(recipients).map(Recipient::getId).toList());
} else {
combined.add(recipientId);
}
}
jobs.add(new RetrieveProfileJob(combined));
return jobs;
}
/**
* Will fetch some profiles to ensure we're decently up-to-date if we haven't done so within a
* certain time period.
@ -293,7 +307,7 @@ public class RetrieveProfileJob extends BaseJob {
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.versionedProfiles() && !recipient.hasProfileKeyCredential()
return !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}

View File

@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.StreamDetails;
@ -57,16 +56,11 @@ public class RotateProfileKeyJob extends BaseJob {
recipientDatabase.setProfileKey(self.getId(), profileKey);
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.versionedProfiles()) {
accountManager.setVersionedProfile(self.getUuid().get(),
profileKey,
Recipient.self().getProfileName().serialize(),
avatarStream);
} else {
accountManager.setProfileName(profileKey, Recipient.self().getProfileName().serialize());
accountManager.setProfileAvatar(profileKey, avatarStream);
}
}
accountManager.setVersionedProfile(self.getUuid().get(),
profileKey,
Recipient.self().getProfileName().serialize(),
avatarStream);
}
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.keyvalue;
import org.thoughtcrime.securesms.util.FeatureFlags;
public final class InternalValues extends SignalStoreValues {
public static final String GV2_FORCE_INVITES = "internal.gv2.force_invites";
public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes";
public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes";
InternalValues(KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
public synchronized boolean forceGv2Invites() {
return FeatureFlags.internalUser() && getBoolean(GV2_FORCE_INVITES, false);
}
/**
* The Server will leave out changes that can only be described by a future protocol level that
* an older client cannot understand. Ignoring those changes by nulling them out simulates that
* scenario for testing.
* <p>
* In conjunction with {@link #gv2IgnoreP2PChanges()} it means no group changes are coming into
* the client and it will generate changes by group state comparison, and those changes will not
* have an editor and so will be in the passive voice.
*/
public synchronized boolean gv2IgnoreServerChanges() {
return FeatureFlags.internalUser() && getBoolean(GV2_IGNORE_SERVER_CHANGES, false);
}
/**
* Signed group changes are sent P2P, if the client ignores them, it will then ask the server
* directly which allows testing of certain testing scenarios.
*/
public synchronized boolean gv2IgnoreP2PChanges() {
return FeatureFlags.internalUser() && getBoolean(GV2_IGNORE_P2P_CHANGES, false);
}
}

View File

@ -23,6 +23,7 @@ public final class SignalStore {
private final UiHints uiHints;
private final TooltipValues tooltipValues;
private final MiscellaneousValues misc;
private final InternalValues internalValues;
private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore();
@ -34,6 +35,7 @@ public final class SignalStore {
this.uiHints = new UiHints(store);
this.tooltipValues = new TooltipValues(store);
this.misc = new MiscellaneousValues(store);
this.internalValues = new InternalValues(store);
}
public static void onFirstEverAppLaunch() {
@ -45,6 +47,7 @@ public final class SignalStore {
uiHints().onFirstEverAppLaunch();
tooltips().onFirstEverAppLaunch();
misc().onFirstEverAppLaunch();
internalValues().onFirstEverAppLaunch();
}
public static @NonNull KbsValues kbsValues() {
@ -79,6 +82,10 @@ public final class SignalStore {
return INSTANCE.misc;
}
public static @NonNull InternalValues internalValues() {
return INSTANCE.internalValues;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore());
}

View File

@ -4,7 +4,8 @@ import androidx.annotation.NonNull;
public class UiHints extends SignalStoreValues {
private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast";
private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast";
private static final String HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE = "uihints.has_confirmed_delete_for_everyone_once";
UiHints(@NonNull KeyValueStore store) {
super(store);
@ -22,4 +23,12 @@ public class UiHints extends SignalStoreValues {
public boolean hasSeenGroupSettingsMenuToast() {
return getBoolean(HAS_SEEN_GROUP_SETTINGS_MENU_TOAST, false);
}
public void markHasConfirmedDeleteForEveryoneOnce() {
putBoolean(HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE, true);
}
public boolean hasConfirmedDeleteForEveryoneOnce() {
return getBoolean(HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE, false);
}
}

View File

@ -173,6 +173,7 @@ public class LinkPreviewRepository {
null,
false,
false,
false,
null,
null,
null,
@ -248,6 +249,7 @@ public class LinkPreviewRepository {
null,
false,
false,
false,
null,
null,
null,

View File

@ -18,16 +18,18 @@ public class SignalPinReminders {
private static final String TAG = Log.tag(SignalPinReminders.class);
private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1);
private static final long THREE_DAYS = TimeUnit.DAYS.toMillis(3);
private static final long ONE_WEEK = TimeUnit.DAYS.toMillis(7);
private static final long TWO_WEEKS = TimeUnit.DAYS.toMillis(14);
private static final long ONE_DAY = TimeUnit.DAYS.toMillis(1);
private static final long THREE_DAYS = TimeUnit.DAYS.toMillis(3);
private static final long ONE_WEEK = TimeUnit.DAYS.toMillis(7);
private static final long TWO_WEEKS = TimeUnit.DAYS.toMillis(14);
private static final long FOUR_WEEKS = TimeUnit.DAYS.toMillis(28);
private static final NavigableSet<Long> INTERVALS = new TreeSet<Long>() {{
add(ONE_DAY);
add(THREE_DAYS);
add(ONE_WEEK);
add(TWO_WEEKS);
add(FOUR_WEEKS);
}};
private static final Map<Long, Integer> STRINGS = new HashMap<Long, Integer>() {{
@ -35,6 +37,7 @@ public class SignalPinReminders {
put(THREE_DAYS, R.string.SignalPinReminders_well_remind_you_again_in_a_few_days);
put(ONE_WEEK, R.string.SignalPinReminders_well_remind_you_again_in_a_week);
put(TWO_WEEKS, R.string.SignalPinReminders_well_remind_you_again_in_a_couple_weeks);
put(FOUR_WEEKS, R.string.SignalPinReminders_well_remind_you_again_in_a_month);
}};
public static final long INITIAL_INTERVAL = INTERVALS.first();

View File

@ -112,6 +112,7 @@ public class MediaPreviewViewModel extends ViewModel {
mediaRecord.getAttachment().getHeight(),
mediaRecord.getAttachment().getSize(),
0,
mediaRecord.getAttachment().isBorderless(),
Optional.absent(),
Optional.fromNullable(mediaRecord.getAttachment().getCaption()),
Optional.absent());

View File

@ -85,6 +85,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
height,
data.length,
0,
false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent()));

View File

@ -49,7 +49,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption(), Optional.absent());
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, media.getBucketId(), media.getCaption(), Optional.absent());
} catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
return media;

View File

@ -19,13 +19,14 @@ public class Media implements Parcelable {
public static final String ALL_MEDIA_BUCKET_ID = "org.thoughtcrime.securesms.ALL_MEDIA";
private final Uri uri;
private final String mimeType;
private final long date;
private final int width;
private final int height;
private final long size;
private final long duration;
private final Uri uri;
private final String mimeType;
private final long date;
private final int width;
private final int height;
private final long size;
private final long duration;
private final boolean borderless;
private Optional<String> bucketId;
private Optional<String> caption;
@ -38,6 +39,7 @@ public class Media implements Parcelable {
int height,
long size,
long duration,
boolean borderless,
Optional<String> bucketId,
Optional<String> caption,
Optional<AttachmentDatabase.TransformProperties> transformProperties)
@ -49,21 +51,23 @@ public class Media implements Parcelable {
this.height = height;
this.size = size;
this.duration = duration;
this.borderless = borderless;
this.bucketId = bucketId;
this.caption = caption;
this.transformProperties = transformProperties;
}
protected Media(Parcel in) {
uri = in.readParcelable(Uri.class.getClassLoader());
mimeType = in.readString();
date = in.readLong();
width = in.readInt();
height = in.readInt();
size = in.readLong();
duration = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
uri = in.readParcelable(Uri.class.getClassLoader());
mimeType = in.readString();
date = in.readLong();
width = in.readInt();
height = in.readInt();
size = in.readLong();
duration = in.readLong();
borderless = in.readInt() == 1;
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
try {
String json = in.readString();
transformProperties = json == null ? Optional.absent() : Optional.fromNullable(JsonUtil.fromJson(json, AttachmentDatabase.TransformProperties.class));
@ -100,6 +104,10 @@ public class Media implements Parcelable {
return duration;
}
public boolean isBorderless() {
return borderless;
}
public Optional<String> getBucketId() {
return bucketId;
}
@ -130,6 +138,7 @@ public class Media implements Parcelable {
dest.writeInt(height);
dest.writeLong(size);
dest.writeLong(duration);
dest.writeInt(borderless ? 1 : 0);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
dest.writeString(transformProperties.transform(JsonUtil::toJson).orNull());

View File

@ -217,7 +217,7 @@ public class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0;
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent(), Optional.absent()));
media.add(new Media(uri, mimetype, date, width, height, size, duration, false, Optional.of(bucketId), Optional.absent(), Optional.absent()));
}
}
@ -311,7 +311,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.getBucketId(), media.getCaption(), Optional.absent());
}
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
@ -337,7 +337,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.getBucketId(), media.getCaption(), Optional.absent());
}
private static class FolderResult {

View File

@ -411,21 +411,20 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
long length = getLength.apply(data);
Uri uri = createBlobBuilder.apply(BlobProvider.getInstance(), data, length)
.withMimeType(mimeType)
.createForSingleSessionOnDisk(this);
.withMimeType(mimeType)
.createForSingleSessionOnDisk(this);
return new Media(
uri,
mimeType,
System.currentTimeMillis(),
width,
height,
length,
0,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent()
);
return new Media(uri,
mimeType,
System.currentTimeMillis(),
width,
height,
length,
0,
false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent());
} catch (IOException e) {
return null;
}

View File

@ -303,7 +303,7 @@ class MediaSendViewModel extends ViewModel {
captionVisible = false;
List<Media> uncaptioned = Stream.of(getSelectedMediaOrDefault())
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent(), Optional.absent()))
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.isBorderless(), m.getBucketId(), Optional.absent(), Optional.absent()))
.toList();
selectedMedia.setValue(uncaptioned);
@ -406,7 +406,7 @@ class MediaSendViewModel extends ViewModel {
}
void onVideoBeginEdit(@NonNull Uri uri) {
cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, Optional.absent(), Optional.absent(), Optional.absent()));
cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, false, Optional.absent(), Optional.absent(), Optional.absent()));
}
void onMediaCaptured(@NonNull Media media) {
@ -485,7 +485,7 @@ class MediaSendViewModel extends ViewModel {
if (splitMessage.getTextSlide().isPresent()) {
Slide slide = splitMessage.getTextSlide().get();
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent(), Optional.absent()), recipient);
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, slide.isBorderless(), Optional.absent(), Optional.absent(), Optional.absent()), recipient);
}
uploadRepository.applyMediaUpdates(oldToNew, recipient);

View File

@ -193,9 +193,9 @@ class MediaUploadRepository {
if (MediaUtil.isVideoType(media.getMimeType())) {
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment();
} else if (MediaUtil.isGif(media.getMimeType())) {
return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull()).asAttachment();
return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull()).asAttachment();
} else if (MediaUtil.isImageType(media.getMimeType())) {
return new ImageSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull(), null).asAttachment();
return new ImageSlide(context, media.getUri(), media.getMimeType(), 0, media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull(), null).asAttachment();
} else if (MediaUtil.isTextType(media.getMimeType())) {
return new TextSlide(context, media.getUri(), null, media.getSize()).asAttachment();
} else {

View File

@ -26,6 +26,7 @@ public final class VideoTrimTransform implements MediaTransform {
media.getHeight(),
media.getSize(),
media.getDuration(),
media.isBorderless(),
media.getBucketId(),
media.getCaption(),
Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs)));

View File

@ -16,15 +16,11 @@ class PinsForAllSchedule implements MegaphoneSchedule {
private static final String TAG = Log.tag(PinsForAllSchedule.class);
@VisibleForTesting
static final long DAYS_UNTIL_FULLSCREEN = 8L;
static final long DAYS_UNTIL_FULLSCREEN = 4L;
private final MegaphoneSchedule schedule = new RecurringSchedule(TimeUnit.HOURS.toMillis(2));
static boolean shouldDisplayFullScreen(long firstVisible, long currentTime) {
if (!FeatureFlags.pinsForAllMandatory()) {
return false;
}
if (firstVisible == 0L) {
return false;
}
@ -52,10 +48,6 @@ class PinsForAllSchedule implements MegaphoneSchedule {
return false;
}
if (FeatureFlags.pinsForAllMegaphoneKillSwitch()) {
return false;
}
if (pinCreationFailedDuringRegistration()) {
return true;
}

View File

@ -1,20 +1,18 @@
package org.thoughtcrime.securesms.messagerequests;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
@ -25,56 +23,45 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.concurrent.TimeUnit;
public class CalleeMustAcceptMessageRequestDialogFragment extends DialogFragment {
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final String ARG_RECIPIENT_ID = "arg.recipient.id";
public class CalleeMustAcceptMessageRequestActivity extends BaseActivity {
private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final String RECIPIENT_ID_EXTRA = "extra.recipient.id";
private TextView description;
private AvatarImageView avatar;
private View okay;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable dismisser = this::dismiss;
private final Runnable finisher = this::finish;
public static DialogFragment create(@NonNull RecipientId recipientId) {
DialogFragment fragment = new CalleeMustAcceptMessageRequestDialogFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_RECIPIENT_ID, recipientId);
fragment.setArguments(args);
return fragment;
public static Intent createIntent(@NonNull Context context, @NonNull RecipientId recipientId) {
Intent intent = new Intent(context, CalleeMustAcceptMessageRequestActivity.class);
intent.setFlags(FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(RECIPIENT_ID_EXTRA, recipientId);
return intent;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.callee_must_accept_message_request_dialog_fragment);
setStyle(DialogFragment.STYLE_NO_FRAME, R.style.TextSecure_DarkNoActionBar);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.callee_must_accept_message_request_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
description = view.findViewById(R.id.description);
avatar = view.findViewById(R.id.avatar);
okay = view.findViewById(R.id.okay);
description = findViewById(R.id.description);
avatar = findViewById(R.id.avatar);
okay = findViewById(R.id.okay);
avatar.setFallbackPhotoProvider(new FallbackPhotoProvider());
okay.setOnClickListener(v -> dismiss());
okay.setOnClickListener(v -> finish());
RecipientId recipientId = requireArguments().getParcelable(ARG_RECIPIENT_ID);
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_ID_EXTRA);
CalleeMustAcceptMessageRequestViewModel.Factory factory = new CalleeMustAcceptMessageRequestViewModel.Factory(recipientId);
CalleeMustAcceptMessageRequestViewModel viewModel = ViewModelProviders.of(this, factory).get(CalleeMustAcceptMessageRequestViewModel.class);
viewModel.getRecipient().observe(getViewLifecycleOwner(), recipient -> {
description.setText(getString(R.string.CalleeMustAcceptMessageRequestDialogFragment__s_will_get_a_message_request_from_you, recipient.getDisplayName(requireContext())));
viewModel.getRecipient().observe(this, recipient -> {
description.setText(getString(R.string.CalleeMustAcceptMessageRequestDialogFragment__s_will_get_a_message_request_from_you, recipient.getDisplayName(this)));
avatar.setAvatar(GlideApp.with(this), recipient, false);
});
}
@ -83,14 +70,14 @@ public class CalleeMustAcceptMessageRequestDialogFragment extends DialogFragment
public void onResume() {
super.onResume();
handler.postDelayed(dismisser, TIMEOUT_MS);
handler.postDelayed(finisher, TIMEOUT_MS);
}
@Override
public void onPause() {
super.onPause();
handler.removeCallbacks(dismisser);
handler.removeCallbacks(finisher);
}
private static class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {

View File

@ -39,7 +39,7 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
public static final int CURRENT_VERSION = 15;
public static final int CURRENT_VERSION = 16;
private static final class Version {
static final int LEGACY = 1;
@ -53,10 +53,11 @@ public class ApplicationMigrations {
//static final int TEST_ARGON2 = 9;
static final int SWOON_STICKERS = 10;
static final int STORAGE_SERVICE = 11;
static final int STORAGE_KEY_ROTATE = 12;
//static final int STORAGE_KEY_ROTATE = 12;
static final int REMOVE_AVATAR_ID = 13;
static final int STORAGE_CAPABILITY = 14;
static final int PIN_REMINDER = 15;
static final int VERSIONED_PROFILE = 16;
}
/**
@ -231,6 +232,10 @@ public class ApplicationMigrations {
jobs.put(Version.PIN_REMINDER, new PinReminderMigrationJob());
}
if (lastSeenVersion < Version.VERSIONED_PROFILE) {
jobs.put(Version.VERSIONED_PROFILE, new ProfileMigrationJob());
}
return jobs;
}

View File

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.migrations;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.logging.Log;
/**
* Schedules a re-upload of the users profile.
*/
public final class ProfileMigrationJob extends MigrationJob {
private static final String TAG = Log.tag(ProfileMigrationJob.class);
public static final String KEY = "ProfileMigrationJob";
ProfileMigrationJob() {
this(new Parameters.Builder().build());
}
private ProfileMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public boolean isUiBlocking() {
return false;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void performMigration() {
Log.i(TAG, "Scheduling profile upload job");
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
public static class Factory implements Job.Factory<ProfileMigrationJob> {
@Override
public @NonNull ProfileMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new ProfileMigrationJob(parameters);
}
}
}

View File

@ -522,7 +522,19 @@ public class AttachmentManager {
}
public enum MediaType {
IMAGE, GIF, AUDIO, VIDEO, DOCUMENT, VCARD;
IMAGE(MediaUtil.IMAGE_JPEG),
GIF(MediaUtil.IMAGE_GIF),
AUDIO(MediaUtil.AUDIO_AAC),
VIDEO(MediaUtil.VIDEO_MP4),
DOCUMENT(MediaUtil.UNKNOWN),
VCARD(MediaUtil.VCARD);
private final String fallbackMimeType;
MediaType(String fallbackMimeType) {
this.fallbackMimeType = fallbackMimeType;
}
public @NonNull Slide createSlide(@NonNull Context context,
@NonNull Uri uri,
@ -559,5 +571,8 @@ public class AttachmentManager {
return DOCUMENT;
}
public String toFallbackMimeType() {
return fallbackMimeType;
}
}
}

View File

@ -35,11 +35,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false, false));
}
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null, null, null, null));
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, null, null, null, null, null));
}
public AudioSlide(Context context, Attachment attachment) {

View File

@ -20,7 +20,7 @@ public class DocumentSlide extends Slide {
@NonNull String contentType, long size,
@Nullable String fileName)
{
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false));
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false, false));
}
@Override

View File

@ -10,22 +10,29 @@ import org.thoughtcrime.securesms.util.MediaUtil;
public class GifSlide extends ImageSlide {
private final boolean borderless;
public GifSlide(Context context, Attachment attachment) {
super(context, attachment);
this.borderless = attachment.isBorderless();
}
public GifSlide(Context context, Uri uri, long size, int width, int height) {
this(context, uri, size, width, height, null);
this(context, uri, size, width, height, false, null);
}
public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, false));
public GifSlide(Context context, Uri uri, long size, int width, int height, boolean borderless, @Nullable String caption) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, borderless, false));
this.borderless = borderless;
}
@Override
@Nullable
public Uri getThumbnailUri() {
public @Nullable Uri getThumbnailUri() {
return getUri();
}
@Override
public boolean isBorderless() {
return borderless;
}
}

View File

@ -30,19 +30,23 @@ import org.thoughtcrime.securesms.util.MediaUtil;
public class ImageSlide extends Slide {
private final boolean borderless;
@SuppressWarnings("unused")
private static final String TAG = ImageSlide.class.getSimpleName();
public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) {
super(context, attachment);
this.borderless = attachment.isBorderless();
}
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable BlurHash blurHash) {
this(context, uri, size, width, height, null, blurHash);
this(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, false, null, blurHash);
}
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption, @Nullable BlurHash blurHash) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, blurHash, null, false, false));
public ImageSlide(Context context, Uri uri, String contentType, long size, int width, int height, boolean borderless, @Nullable String caption, @Nullable BlurHash blurHash) {
super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false));
this.borderless = borderless;
}
@Override
@ -65,6 +69,11 @@ public class ImageSlide extends Slide {
return getPlaceholderBlur() != null;
}
@Override
public boolean isBorderless() {
return borderless;
}
@NonNull
@Override
public String getContentDescription() {

View File

@ -110,6 +110,10 @@ public abstract class Slide {
return false;
}
public boolean isBorderless() {
return false;
}
public @NonNull String getContentDescription() { return ""; }
public @NonNull Attachment asAttachment() {
@ -158,9 +162,10 @@ public abstract class Slide {
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
boolean voiceNote,
boolean borderless,
boolean quote)
{
return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, audioHash, voiceNote, quote, null);
return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, audioHash, voiceNote, borderless, quote, null);
}
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
@ -176,6 +181,7 @@ public abstract class Slide {
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
boolean voiceNote,
boolean borderless,
boolean quote,
@Nullable AttachmentDatabase.TransformProperties transformProperties)
{
@ -191,6 +197,7 @@ public abstract class Slide {
fileName,
fastPreflightId,
voiceNote,
borderless,
quote,
caption,
stickerLocator,

View File

@ -23,7 +23,7 @@ public class StickerSlide extends Slide {
}
public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false));
}
@Override

View File

@ -17,6 +17,6 @@ public class TextSlide extends Slide {
}
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false, false));
}
}

View File

@ -37,7 +37,7 @@ public class VideoSlide extends Slide {
}
public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, null, false, false, transformProperties));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, null, false, false, false, transformProperties));
}
public VideoSlide(Context context, Attachment attachment) {

View File

@ -16,19 +16,19 @@ import androidx.appcompat.app.AlertDialog;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import com.google.firebase.iid.FirebaseInstanceId;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
@ -42,6 +42,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
private static final String PUSH_MESSAGING_PREF = "pref_toggle_push_messaging";
private static final String SUBMIT_DEBUG_LOG_PREF = "pref_submit_debug_logs";
private static final String INTERNAL_PREF = "pref_internal";
private static final int PICK_IDENTITY_CONTACT = 1;
@ -54,6 +55,22 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
Preference submitDebugLog = this.findPreference(SUBMIT_DEBUG_LOG_PREF);
submitDebugLog.setOnPreferenceClickListener(new SubmitDebugLogListener());
submitDebugLog.setSummary(getVersion(getActivity()));
Preference internalPreference = this.findPreference(INTERNAL_PREF);
internalPreference.setVisible(FeatureFlags.internalUser());
internalPreference.setOnPreferenceClickListener(preference -> {
if (FeatureFlags.internalUser()) {
requireActivity().getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, new InternalOptionsPreferenceFragment())
.addToBackStack(null)
.commit();
return true;
} else {
return false;
}
});
}
@Override

View File

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.preferences;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceDataStore;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.keyvalue.InternalValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragment {
private static final String TAG = Log.tag(InternalOptionsPreferenceFragment.class);
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_internal);
PreferenceDataStore preferenceDataStore = SignalStore.getPreferenceDataStore();
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().forceGv2Invites());
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges());
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges());
findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> {
ApplicationDependencies.getJobManager()
.startChain(new RefreshAttributesJob())
.then(new RefreshOwnProfileJob())
.enqueue();
Toast.makeText(getContext(), "Scheduled attribute refresh", Toast.LENGTH_SHORT).show();
return true;
});
findPreference("pref_rotate_profile_key").setOnPreferenceClickListener(preference -> {
ApplicationDependencies.getJobManager().add(new RotateProfileKeyJob());
Toast.makeText(getContext(), "Scheduled profile key rotation", Toast.LENGTH_SHORT).show();
return true;
});
}
private void initializeSwitchPreference(@NonNull PreferenceDataStore preferenceDataStore,
@NonNull String key,
boolean checked)
{
SwitchPreferenceCompat forceGv2Preference = (SwitchPreferenceCompat) findPreference(key);
forceGv2Preference.setPreferenceDataStore(preferenceDataStore);
forceGv2Preference.setChecked(checked);
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__internal_preferences);
}
}

View File

@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.guava.Optional;
@ -27,7 +28,7 @@ class EditProfileViewModel extends ViewModel {
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
private final LiveData<Boolean> isFormValid = Transformations.map(givenName, name -> !name.isEmpty());
private final LiveData<Boolean> isFormValid = Transformations.map(givenName, name -> !StringUtil.isVisuallyEmpty(name));
private final EditProfileRepository repository;
private final GroupId groupId;

View File

@ -40,7 +40,6 @@ public final class LiveRecipient {
private final AtomicReference<Recipient> recipient;
private final RecipientDatabase recipientDatabase;
private final GroupDatabase groupDatabase;
private final String unnamedGroupName;
LiveRecipient(@NonNull Context context, @NonNull MutableLiveData<Recipient> liveData, @NonNull Recipient defaultRecipient) {
this.context = context.getApplicationContext();
@ -48,7 +47,6 @@ public final class LiveRecipient {
this.recipient = new AtomicReference<>(defaultRecipient);
this.recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
this.unnamedGroupName = context.getString(R.string.RecipientProvider_unnamed_group);
this.observers = new CopyOnWriteArraySet<>();
this.foreverObserver = recipient -> {
for (RecipientForeverObserver o : observers) {
@ -175,21 +173,13 @@ public final class LiveRecipient {
private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) {
RecipientSettings settings = recipientDatabase.getRecipientSettings(id);
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)
: getIndividualRecipientDetails(settings);
: RecipientDetails.forIndividual(context, settings);
Recipient recipient = new Recipient(id, details);
Recipient recipient = new Recipient(id, details, true);
RecipientIdCache.INSTANCE.put(recipient);
return recipient;
}
private @NonNull RecipientDetails getIndividualRecipientDetails(RecipientSettings settings) {
boolean systemContact = !TextUtils.isEmpty(settings.getSystemDisplayName());
boolean isLocalNumber = (settings.getE164() != null && settings.getE164().equals(TextSecurePreferences.getLocalNumber(context))) ||
(settings.getUuid() != null && settings.getUuid().equals(TextSecurePreferences.getLocalUuid(context)));
return new RecipientDetails(context, null, Optional.absent(), systemContact, isLocalNumber, settings, null);
}
@WorkerThread
private @NonNull RecipientDetails getGroupRecipientDetails(@NonNull RecipientSettings settings) {
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(settings.getId());
@ -199,21 +189,17 @@ public final class LiveRecipient {
List<Recipient> members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList();
Optional<Long> avatarId = Optional.absent();
if (settings.getGroupId() != null && settings.getGroupId().isPush() && title == null) {
title = unnamedGroupName;
}
if (groupRecord.get().hasAvatar()) {
avatarId = Optional.of(groupRecord.get().getAvatarId());
}
return new RecipientDetails(context, title, avatarId, false, false, settings, members);
return new RecipientDetails(title, avatarId, false, false, settings, members);
}
return new RecipientDetails(context, unnamedGroupName, Optional.absent(), false, false, settings, null);
return new RecipientDetails(null, Optional.absent(), false, false, settings, null);
}
private synchronized void set(@NonNull Recipient recipient) {
synchronized void set(@NonNull Recipient recipient) {
this.recipient.set(recipient);
this.liveData.postValue(recipient);
}

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.AnyThread;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -32,11 +34,14 @@ public final class LiveRecipientCache {
private static final int CACHE_MAX = 1000;
private static final int CACHE_WARM_MAX = 500;
private static final Object SELF_LOCK = new Object();
private final Context context;
private final RecipientDatabase recipientDatabase;
private final Map<RecipientId, LiveRecipient> recipients;
private final LiveRecipient unknown;
@GuardedBy("SELF_LOCK")
private RecipientId localRecipientId;
private boolean warmedUp;
@ -75,8 +80,42 @@ public final class LiveRecipientCache {
return live;
}
/**
* Adds a recipient to the cache if we don't have an entry. This will also update a cache entry
* if the provided recipient is resolved, or if the existing cache entry is unresolved.
*
* If the recipient you add is unresolved, this will enqueue a resolve on a background thread.
*/
@AnyThread
public synchronized void addToCache(@NonNull Collection<Recipient> newRecipients) {
for (Recipient recipient : newRecipients) {
LiveRecipient live = recipients.get(recipient.getId());
boolean needsResolve = false;
if (live == null) {
live = new LiveRecipient(context, new MutableLiveData<>(), recipient);
recipients.put(recipient.getId(), live);
needsResolve = recipient.isResolving();
} else if (live.get().isResolving() || !recipient.isResolving()) {
live.set(recipient);
needsResolve = recipient.isResolving();
}
if (needsResolve) {
MissingRecipientException prettyStackTraceError = new MissingRecipientException(recipient.getId());
SignalExecutors.BOUNDED.execute(() -> {
try {
recipient.resolve();
} catch (MissingRecipientException e) {
throw prettyStackTraceError;
}
});
}
}
}
@NonNull Recipient getSelf() {
synchronized (this) {
synchronized (SELF_LOCK) {
if (localRecipientId == null) {
UUID localUuid = TextSecurePreferences.getLocalUuid(context);
String localE164 = TextSecurePreferences.getLocalNumber(context);
@ -107,23 +146,22 @@ public final class LiveRecipientCache {
}
SignalExecutors.BOUNDED.execute(() -> {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
List<Recipient> recipients = new ArrayList<>();
try (ThreadDatabase.Reader reader = threadDatabase.readerFor(threadDatabase.getConversationList())) {
int i = 0;
ThreadRecord record = null;
List<Recipient> recipients = new ArrayList<>();
int i = 0;
ThreadRecord record = null;
while ((record = reader.getNext()) != null && i < CACHE_WARM_MAX) {
recipients.add(record.getRecipient());
i++;
}
Log.d(TAG, "Warming up " + recipients.size() + " recipients.");
Collections.reverse(recipients);
Stream.of(recipients).map(Recipient::getId).forEach(this::getLive);
}
Log.d(TAG, "Warming up " + recipients.size() + " recipients.");
Collections.reverse(recipients);
Stream.of(recipients).map(Recipient::getId).forEach(this::getLive);
});
}

View File

@ -10,6 +10,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
@ -54,7 +56,7 @@ import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBann
public class Recipient {
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails());
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails(), true);
private static final FallbackPhotoProvider DEFAULT_FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private static final String TAG = Log.tag(Recipient.class);
@ -177,7 +179,7 @@ public class Recipient {
} else if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.uuids()) {
if (FeatureFlags.cds()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
@ -185,7 +187,7 @@ public class Recipient {
return resolved(recipient.getId());
} else if (uuid != null) {
if (FeatureFlags.uuids() || e164 != null) {
if (FeatureFlags.uuidOnlyContacts() || e164 != null) {
RecipientId id = db.getOrInsertFromUuid(uuid);
db.markRegistered(id, uuid);
@ -195,7 +197,7 @@ public class Recipient {
return resolved(id);
} else {
if (!FeatureFlags.uuids() && FeatureFlags.groupsV2()) {
if (!FeatureFlags.uuidOnlyContacts() && FeatureFlags.groupsV2()) {
throw new RuntimeException(new UuidRecipientError());
} else {
throw new UuidRecipientError();
@ -207,7 +209,7 @@ public class Recipient {
if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.uuids()) {
if (FeatureFlags.cds()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
@ -270,7 +272,7 @@ public class Recipient {
if (UuidUtil.isUuid(identifier)) {
UUID uuid = UuidUtil.parseOrThrow(identifier);
if (FeatureFlags.uuids()) {
if (FeatureFlags.uuidOnlyContacts()) {
id = db.getOrInsertFromUuid(uuid);
} else {
Optional<RecipientId> possibleId = db.getByUuid(uuid);
@ -278,7 +280,7 @@ public class Recipient {
if (possibleId.isPresent()) {
id = possibleId.get();
} else {
if (!FeatureFlags.uuids() && FeatureFlags.groupsV2()) {
if (!FeatureFlags.uuidOnlyContacts() && FeatureFlags.groupsV2()) {
throw new RuntimeException(new UuidRecipientError());
} else {
throw new UuidRecipientError();
@ -344,9 +346,9 @@ public class Recipient {
this.identityStatus = VerifiedStatus.DEFAULT;
}
Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details) {
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
this.id = id;
this.resolving = false;
this.resolving = !resolved;
this.uuid = details.uuid;
this.username = details.username;
this.e164 = details.e164;
@ -408,9 +410,11 @@ public class Recipient {
}
return Util.join(names, ", ");
} else if (name == null && groupId != null && groupId.isPush()) {
return context.getString(R.string.RecipientProvider_unnamed_group);
} else {
return this.name;
}
return this.name;
}
/**
@ -631,6 +635,10 @@ public class Recipient {
return groupId != null && groupId.isV2();
}
public boolean isActiveGroup() {
return Stream.of(getParticipants()).anyMatch(Recipient::isLocalNumber);
}
public @NonNull List<Recipient> getParticipants() {
return new ArrayList<>(participants);
}
@ -657,7 +665,7 @@ public class Recipient {
public @NonNull FallbackContactPhoto getFallbackContactPhoto(@NonNull FallbackPhotoProvider fallbackPhotoProvider) {
if (localNumber) return fallbackPhotoProvider.getPhotoForLocalNumber();
if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient();
else if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient();
else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup();
else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup();
else if (!TextUtils.isEmpty(name)) return fallbackPhotoProvider.getPhotoForRecipientWithName(name);
@ -746,7 +754,7 @@ public class Recipient {
if (FeatureFlags.usernames()) {
return true;
} else {
return FeatureFlags.uuids() && uuidCapability == Capability.SUPPORTED;
return FeatureFlags.uuidOnlyContacts() && uuidCapability == Capability.SUPPORTED;
}
}

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -66,13 +67,12 @@ public class RecipientDetails {
final byte[] identityKey;
final VerifiedStatus identityStatus;
RecipientDetails(@NonNull Context context,
@Nullable String name,
@NonNull Optional<Long> groupAvatarId,
boolean systemContact,
boolean isLocalNumber,
@NonNull RecipientSettings settings,
@Nullable List<Recipient> participants)
public RecipientDetails(@Nullable String name,
@NonNull Optional<Long> groupAvatarId,
boolean systemContact,
boolean isLocalNumber,
@NonNull RecipientSettings settings,
@Nullable List<Recipient> participants)
{
this.groupAvatarId = groupAvatarId;
this.systemContactPhoto = Util.uri(settings.getSystemContactPhotoUri());
@ -161,4 +161,12 @@ public class RecipientDetails {
this.identityKey = null;
this.identityStatus = VerifiedStatus.DEFAULT;
}
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) {
boolean systemContact = !TextUtils.isEmpty(settings.getSystemDisplayName());
boolean isLocalNumber = (settings.getE164() != null && settings.getE164().equals(TextSecurePreferences.getLocalNumber(context))) ||
(settings.getUuid() != null && settings.getUuid().equals(TextSecurePreferences.getLocalUuid(context)));
return new RecipientDetails(null, Optional.absent(), systemContact, isLocalNumber, settings, null);
}
}

View File

@ -46,7 +46,7 @@ public class RecipientUtil {
throw new AssertionError(recipient.getId() + " - No UUID or phone number!");
}
if (FeatureFlags.uuids() && !recipient.getUuid().isPresent()) {
if (FeatureFlags.cds() && !recipient.getUuid().isPresent()) {
Log.i(TAG, recipient.getId() + " is missing a UUID...");
try {
RegisteredState state = DirectoryHelper.refreshDirectoryFor(context, recipient, false);
@ -184,13 +184,13 @@ public class RecipientUtil {
@WorkerThread
private static boolean isMessageRequestAccepted(@NonNull Context context, long threadId, @NonNull Recipient threadRecipient) {
return threadRecipient.isLocalNumber() ||
threadRecipient.isProfileSharing() ||
threadRecipient.isSystemContact() ||
threadRecipient.isForceSmsSelection() ||
!threadRecipient.isRegistered() ||
hasSentMessageInThread(context, threadId) ||
noSecureMessagesInThread(context, threadId) ||
return threadRecipient.isLocalNumber() ||
threadRecipient.isProfileSharing() ||
threadRecipient.isSystemContact() ||
threadRecipient.isForceSmsSelection() ||
!threadRecipient.isRegistered() ||
hasSentMessageInThread(context, threadId) ||
noSecureMessagesAndNoCallsInThread(context, threadId) ||
isPreMessageRequestThread(context, threadId);
}
@ -200,8 +200,9 @@ public class RecipientUtil {
}
@WorkerThread
private static boolean noSecureMessagesInThread(@NonNull Context context, long threadId) {
return DatabaseFactory.getMmsSmsDatabase(context).getSecureConversationCount(threadId) == 0;
private static boolean noSecureMessagesAndNoCallsInThread(@NonNull Context context, long threadId) {
return DatabaseFactory.getMmsSmsDatabase(context).getSecureConversationCount(threadId) == 0 &&
!DatabaseFactory.getThreadDatabase(context).hasReceivedAnyCallsSince(threadId, 0);
}
@WorkerThread

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