Copione merged onto master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
1145d6000c
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 + ", "
|
||||
|
|
|
@ -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 + ", " +
|
||||
|
|
|
@ -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 + ", " +
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ public class WebRtcViewModel {
|
|||
CALL_RINGING,
|
||||
CALL_BUSY,
|
||||
CALL_DISCONNECTED,
|
||||
CALL_NEEDS_PERMISSION,
|
||||
|
||||
// Error states
|
||||
NETWORK_FAILURE,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1632,6 +1632,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
String.valueOf(new SecureRandom().nextLong()),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
stickerLocator,
|
||||
null,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue