diff --git a/app/build.gradle b/app/build.gradle index 4ef2a8dba..99192008d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,8 +80,8 @@ protobuf { } } -def canonicalVersionCode = 614 -def canonicalVersionName = "4.58.0" +def canonicalVersionCode = 615 +def canonicalVersionName = "4.58.1" def postFixSize = 10 def abiPostFix = ['universal' : 0, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 519568c40..f6dbdfedf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -625,11 +625,6 @@ android:authorities="org.thoughtcrime.securesms.database.stickerpack" android:exported="false" /> - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 6ded0a044..e5f0da4c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -17,17 +17,15 @@ package org.thoughtcrime.securesms; import android.annotation.SuppressLint; - -import androidx.appcompat.app.AppCompatDelegate; -import androidx.camera.camera2.Camera2Config; -import androidx.camera.core.CameraX; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.ProcessLifecycleOwner; import android.content.Context; import android.os.AsyncTask; import android.os.Build; + import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ProcessLifecycleOwner; import androidx.multidex.MultiDexApplication; import com.google.android.gms.security.ProviderInstaller; @@ -47,27 +45,23 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler; -import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; -import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; -import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; import org.thoughtcrime.securesms.ringrtc.RingRtcLogger; import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.LocalBackupListener; -import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; @@ -133,7 +127,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi initializePendingMessages(); initializeBlobProvider(); initializeCleanup(); - initializeCameraX(); + FeatureFlags.init(); NotificationChannels.create(this); RefreshPreKeysJob.scheduleIfNecessary(); @@ -381,19 +375,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi }); } - @SuppressLint("RestrictedApi") - private void initializeCameraX() { - if (CameraXUtil.isSupported()) { - new Thread(() -> { - try { - CameraX.initialize(this, Camera2Config.defaultConfig()); - } catch (Throwable t) { - Log.w(TAG, "Failed to initialize CameraX."); - } - }, "signal-camerax-initialization").start(); - } - } - @Override protected void attachBaseContext(Context base) { super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java index f5a827f64..796f4ab33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -20,6 +20,7 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; @@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BitmapUtil; @@ -76,6 +78,7 @@ import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; @@ -102,7 +105,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private static final short REQUEST_CODE_SELECT_AVATAR = 26165; private static final int PICK_CONTACT = 1; - public static final int AVATAR_SIZE = 210; private EditText groupName; private ListView lv; @@ -321,7 +323,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) .centerCrop() - .override(AVATAR_SIZE, AVATAR_SIZE) + .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS) .into(new SimpleTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, Transition transition) { @@ -362,7 +364,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity } memberAddresses.add(Recipient.self().getId()); - GroupId groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true); + GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses); RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT); @@ -548,16 +550,22 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity @Override protected Optional doInBackground(GroupId... groupIds) { final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity); - final List recipients = db.getGroupMembers(groupIds[0], false); + final List recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); final Optional group = db.getGroup(groupIds[0]); final Set existingContacts = new HashSet<>(recipients.size()); existingContacts.addAll(recipients); if (group.isPresent()) { + Bitmap avatar = null; + try { + avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId())); + } catch (IOException e) { + Log.w(TAG, "Failed to read avatar."); + } return Optional.of(new GroupData(groupIds[0], existingContacts, - BitmapUtil.fromByteArray(group.get().getAvatar()), - group.get().getAvatar(), + avatar, + BitmapUtil.toByteArray(avatar), group.get().getTitle())); } else { return Optional.absent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java index 6112a71da..a25129edc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java @@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.Lifecycle; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.recipients.Recipient; @@ -34,7 +35,7 @@ public final class GroupMembersDialog { public void display() { SimpleTask.run( lifecycle, - () -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), true), + () -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF), members -> { AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.ConversationActivity_group_members) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java index 6cf6deab5..b82463605 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -31,6 +31,7 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.Loader; import org.thoughtcrime.securesms.conversation.ConversationItem; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.logging.Log; import android.view.LayoutInflater; import android.view.MenuItem; @@ -368,7 +369,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity List receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId()); if (receiptInfoList.isEmpty()) { - List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false); + List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient recipient : group) { recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 863ab2d5b..b7f7189f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.KeyValueDatabase; @@ -49,7 +48,6 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -118,9 +116,11 @@ public class FullBackupExporter extends FullBackupBase { stopwatch.split("prefs"); - for (File avatar : AvatarHelper.getAvatarFiles(context)) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); - outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length()); + for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) { + if (avatar != null) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength()); + } } stopwatch.split("avatars"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 3d24f2e19..578eb6d5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -176,7 +176,7 @@ public class FullBackupImporter extends FullBackupBase { private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException { if (avatar.hasRecipientId()) { RecipientId recipientId = RecipientId.from(avatar.getRecipientId()); - inputStream.readAttachmentTo(new FileOutputStream(AvatarHelper.getAvatarFile(context, recipientId)), avatar.getLength()); + inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength()); } else { if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) { Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java index 5700aed5a..1820569b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java @@ -25,7 +25,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow { ViewGroup.LayoutParams.WRAP_CONTENT); this.context = context; this.listener = listener; - this.list = (ViewGroup) getContentView(); + this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container); setBackgroundDrawable(null); setOutsideTouchable(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java index f360fba5c..c806068d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.util.Conversions; import org.whispersystems.libsignal.util.guava.Optional; @@ -33,11 +34,11 @@ public final class GroupRecordContactPhoto implements ContactPhoto { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); Optional groupRecord = groupDatabase.getGroup(groupId); - if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) { - return new ByteArrayInputStream(groupRecord.get().getAvatar()); + if (!groupRecord.isPresent() || !AvatarHelper.hasAvatar(context, groupRecord.get().getRecipientId())) { + throw new IOException("No avatar for group: " + groupId); } - throw new IOException("Couldn't load avatar for group: " + groupId); + return AvatarHelper.getAvatar(context, groupRecord.get().getRecipientId()); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java index b6daaa819..35b512504 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java @@ -31,13 +31,12 @@ public class ProfileContactPhoto implements ContactPhoto { @Override public @NonNull InputStream openInputStream(Context context) throws IOException { - return AvatarHelper.getInputStreamFor(context, recipient.getId()); + return AvatarHelper.getAvatar(context, recipient.getId()); } @Override public @Nullable Uri getUri(@NonNull Context context) { - File avatarFile = AvatarHelper.getAvatarFile(context, recipient.getId()); - return avatarFile.exists() ? Uri.fromFile(avatarFile) : null; + return null; } @Override @@ -72,12 +71,6 @@ public class ProfileContactPhoto implements ContactPhoto { return 0; } - File avatarFile = AvatarHelper.getAvatarFile(ApplicationDependencies.getApplication(), recipient.getId()); - - if (avatarFile.exists()) { - return avatarFile.lastModified(); - } else { - return 0; - } + return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 8c2e864dd..58bfa8d3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -132,6 +132,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; @@ -1574,7 +1575,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (params[0].isGroup()) { recipients.addAll(DatabaseFactory.getGroupDatabase(ConversationActivity.this) - .getGroupMembers(params[0].requireGroupId(), false)); + .getGroupMembers(params[0].requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)); } else { recipients.add(params[0]); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index ae6185788..6a8a0de61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -103,7 +103,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity; import org.thoughtcrime.securesms.revealable.ViewOnceUtil; -import org.thoughtcrime.securesms.sharing.ShareActivity; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; @@ -206,7 +205,7 @@ public class ConversationFragment extends Fragment new ConversationItemSwipeCallback( messageRecord -> actionMode == null && - canReplyToMessage(isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()), + MenuState.canReplyToMessage(MenuState.isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()), this::handleReplyMessage ).attachToRecyclerView(list); @@ -504,78 +503,22 @@ public class ConversationFragment extends Fragment }); } - private void setCorrectMenuVisibility(Menu menu) { + private void setCorrectMenuVisibility(@NonNull Menu menu) { Set messageRecords = getListAdapter().getSelectedItems(); - boolean actionMessage = false; - boolean hasText = false; - boolean sharedContact = false; - boolean viewOnce = false; if (actionMode != null && messageRecords.size() == 0) { actionMode.finish(); return; } - for (MessageRecord messageRecord : messageRecords) { - if (isActionMessage(messageRecord)) - { - actionMessage = true; - } + MenuState menuState = MenuState.getMenuState(messageRecords, messageRequestViewModel.shouldShowMessageRequest()); - if (messageRecord.getBody().length() > 0) { - hasText = true; - } - - if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { - sharedContact = true; - } - - if (messageRecord.isViewOnce()) { - viewOnce = true; - } - } - - if (messageRecords.size() > 1) { - menu.findItem(R.id.menu_context_forward).setVisible(false); - menu.findItem(R.id.menu_context_reply).setVisible(false); - menu.findItem(R.id.menu_context_details).setVisible(false); - menu.findItem(R.id.menu_context_save_attachment).setVisible(false); - menu.findItem(R.id.menu_context_resend).setVisible(false); - } else { - MessageRecord messageRecord = messageRecords.iterator().next(); - - menu.findItem(R.id.menu_context_resend).setVisible(messageRecord.isFailed()); - menu.findItem(R.id.menu_context_save_attachment).setVisible(!actionMessage && - !viewOnce && - messageRecord.isMms() && - !messageRecord.isMmsNotification() && - ((MediaMmsMessageRecord)messageRecord).containsMediaSlide() && - ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null); - - menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage && !sharedContact && !viewOnce); - menu.findItem(R.id.menu_context_details).setVisible(!actionMessage); - menu.findItem(R.id.menu_context_reply).setVisible(canReplyToMessage(actionMessage, messageRecord, messageRequestViewModel.shouldShowMessageRequest())); - } - menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText); - } - - private static boolean canReplyToMessage(boolean actionMessage, MessageRecord messageRecord, boolean isDisplayingMessageRequest) { - return !actionMessage && - !messageRecord.isPending() && - !messageRecord.isFailed() && - !isDisplayingMessageRequest && - messageRecord.isSecure(); - } - - private static boolean isActionMessage(MessageRecord messageRecord) { - return messageRecord.isGroupAction() || - messageRecord.isCallLog() || - messageRecord.isJoined() || - messageRecord.isExpirationTimerUpdate() || - messageRecord.isEndSession() || - messageRecord.isIdentityUpdate() || - messageRecord.isIdentityVerified() || - messageRecord.isIdentityDefault(); + menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction()); + menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction()); + menu.findItem(R.id.menu_context_details).setVisible(menuState.shouldShowDetailsAction()); + menu.findItem(R.id.menu_context_save_attachment).setVisible(menuState.shouldShowSaveAttachmentAction()); + menu.findItem(R.id.menu_context_resend).setVisible(menuState.shouldShowResendAction()); + menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction()); } private ConversationAdapter getListAdapter() { @@ -1432,6 +1375,10 @@ public class ConversationFragment extends Fragment } public void show() { + if (textView.getText() == null || textView.getText().length() == 0) { + return; + } + if (pendingHide) { pendingHide = false; } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java index e0a14e5b6..d93d8a7b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java @@ -33,11 +33,11 @@ import org.thoughtcrime.securesms.components.MaskView; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; +import java.util.Collections; import java.util.List; public final class ConversationReactionOverlay extends RelativeLayout { @@ -427,25 +427,11 @@ public final class ConversationReactionOverlay extends RelativeLayout { } private void setupToolbarMenuItems() { - toolbar.getMenu().findItem(R.id.action_copy).setVisible(shouldShowCopy()); - toolbar.getMenu().findItem(R.id.action_download).setVisible(shouldShowDownload()); - toolbar.getMenu().findItem(R.id.action_forward).setVisible(shouldShowForward()); - } + MenuState menuState = MenuState.getMenuState(Collections.singleton(messageRecord), false); - private boolean shouldShowCopy() { - return !MessageRecordUtil.hasSticker(messageRecord) && - !MessageRecordUtil.isMediaMessage(messageRecord) && - !MessageRecordUtil.hasSharedContact(messageRecord); - } - - private boolean shouldShowDownload() { - return MessageRecordUtil.isMediaMessage(messageRecord) || - MessageRecordUtil.hasLocation(messageRecord); - } - - - private boolean shouldShowForward() { - return !MessageRecordUtil.hasSharedContact(messageRecord); + toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction()); + toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction()); + toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction()); } private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java new file mode 100644 index 000000000..4a74776a4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -0,0 +1,170 @@ +package org.thoughtcrime.securesms.conversation; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; + +import java.util.Set; + +final class MenuState { + + private final boolean forward; + private final boolean reply; + private final boolean details; + private final boolean saveAttachment; + private final boolean resend; + private final boolean copy; + + private MenuState(@NonNull Builder builder) { + forward = builder.forward; + reply = builder.reply; + details = builder.details; + saveAttachment = builder.saveAttachment; + resend = builder.resend; + copy = builder.copy; + } + + boolean shouldShowForwardAction() { + return forward; + } + + boolean shouldShowReplyAction() { + return reply; + } + + boolean shouldShowDetailsAction() { + return details; + } + + boolean shouldShowSaveAttachmentAction() { + return saveAttachment; + } + + boolean shouldShowResendAction() { + return resend; + } + + boolean shouldShowCopyAction() { + return copy; + } + + static MenuState getMenuState(@NonNull Set messageRecords, + boolean shouldShowMessageRequest) + { + + Builder builder = new Builder(); + boolean actionMessage = false; + boolean hasText = false; + boolean sharedContact = false; + boolean viewOnce = false; + + for (MessageRecord messageRecord : messageRecords) { + if (isActionMessage(messageRecord)) + { + actionMessage = true; + } + + if (messageRecord.getBody().length() > 0) { + hasText = true; + } + + if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { + sharedContact = true; + } + + if (messageRecord.isViewOnce()) { + viewOnce = true; + } + } + + if (messageRecords.size() > 1) { + builder.shouldShowForwardAction(false) + .shouldShowReplyAction(false) + .shouldShowDetailsAction(false) + .shouldShowSaveAttachmentAction(false) + .shouldShowResendAction(false); + } else { + MessageRecord messageRecord = messageRecords.iterator().next(); + + builder.shouldShowResendAction(messageRecord.isFailed()) + .shouldShowSaveAttachmentAction(!actionMessage && + !viewOnce && + messageRecord.isMms() && + !messageRecord.isMmsNotification() && + ((MediaMmsMessageRecord)messageRecord).containsMediaSlide() && + ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null) + .shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce) + .shouldShowDetailsAction(!actionMessage) + .shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest)); + } + + return builder.shouldShowCopyAction(!actionMessage && hasText) + .build(); + } + + static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) { + return !actionMessage && + !messageRecord.isPending() && + !messageRecord.isFailed() && + !isDisplayingMessageRequest && + messageRecord.isSecure(); + } + + static boolean isActionMessage(@NonNull MessageRecord messageRecord) { + return messageRecord.isGroupAction() || + messageRecord.isCallLog() || + messageRecord.isJoined() || + messageRecord.isExpirationTimerUpdate() || + messageRecord.isEndSession() || + messageRecord.isIdentityUpdate() || + messageRecord.isIdentityVerified() || + messageRecord.isIdentityDefault(); + } + + private final static class Builder { + + private boolean forward; + private boolean reply; + private boolean details; + private boolean saveAttachment; + private boolean resend; + private boolean copy; + + @NonNull Builder shouldShowForwardAction(boolean forward) { + this.forward = forward; + return this; + } + + @NonNull Builder shouldShowReplyAction(boolean reply) { + this.reply = reply; + return this; + } + + @NonNull Builder shouldShowDetailsAction(boolean details) { + this.details = details; + return this; + } + + @NonNull Builder shouldShowSaveAttachmentAction(boolean saveAttachment) { + this.saveAttachment = saveAttachment; + return this; + } + + @NonNull Builder shouldShowResendAction(boolean resend) { + this.resend = resend; + return this; + } + + @NonNull Builder shouldShowCopyAction(boolean copy) { + this.copy = copy; + return this; + } + + @NonNull + MenuState build() { + return new MenuState(this); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java index ab4efe84a..0affeaf2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java @@ -54,4 +54,7 @@ public class ModernEncryptingPartOutputStream { } } + public static long getPlaintextLength(long cipherTextLength) { + return cipherTextLength - 32; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index ddc929612..a0ada225e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -1,40 +1,50 @@ package org.thoughtcrime.securesms.database; - import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; +import com.google.protobuf.InvalidProtocolBufferException; import net.sqlcipher.database.SQLiteDatabase; +import org.signal.storageservice.protos.groups.Member; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil; import java.io.Closeable; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.UUID; -public class GroupDatabase extends Database { +public final class GroupDatabase extends Database { - @SuppressWarnings("unused") - private static final String TAG = GroupDatabase.class.getSimpleName(); + private static final String TAG = Log.tag(GroupDatabase.class); static final String TABLE_NAME = "groups"; private static final String ID = "_id"; @@ -42,7 +52,6 @@ public class GroupDatabase extends Database { static final String RECIPIENT_ID = "recipient_id"; private static final String TITLE = "title"; private static final String MEMBERS = "members"; - private static final String AVATAR = "avatar"; private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; @@ -52,6 +61,14 @@ public class GroupDatabase extends Database { static final String ACTIVE = "active"; static final String MMS = "mms"; + /* V2 Group columns */ + /** 32 bytes serialized {@link GroupMasterKey} */ + private static final String V2_MASTER_KEY = "master_key"; + /** Increments with every change to the group */ + private static final String V2_REVISION = "revision"; + /** Serialized {@link DecryptedGroup} protobuf */ + private static final String V2_DECRYPTED_GROUP = "decrypted_group"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + @@ -59,7 +76,6 @@ public class GroupDatabase extends Database { RECIPIENT_ID + " INTEGER, " + TITLE + " TEXT, " + MEMBERS + " TEXT, " + - AVATAR + " BLOB, " + AVATAR_ID + " INTEGER, " + AVATAR_KEY + " BLOB, " + AVATAR_CONTENT_TYPE + " TEXT, " + @@ -67,7 +83,10 @@ public class GroupDatabase extends Database { TIMESTAMP + " INTEGER, " + ACTIVE + " INTEGER DEFAULT 1, " + AVATAR_DIGEST + " BLOB, " + - MMS + " INTEGER DEFAULT 0);"; + MMS + " INTEGER DEFAULT 0, " + + V2_MASTER_KEY + " BLOB, " + + V2_REVISION + " BLOB, " + + V2_DECRYPTED_GROUP + " BLOB);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", @@ -75,7 +94,7 @@ public class GroupDatabase extends Database { }; private static final String[] GROUP_PROJECTION = { - GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, + GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, TIMESTAMP, ACTIVE, MMS }; @@ -120,7 +139,7 @@ public class GroupDatabase extends Database { return true; } - boolean noMetadata = group.get().getAvatar() == null && TextUtils.isEmpty(group.get().getTitle()); + boolean noMetadata = !group.get().hasAvatar() && TextUtils.isEmpty(group.get().getTitle()); boolean noMembers = group.get().getMembers().isEmpty() || (group.get().getMembers().size() == 1 && group.get().getMembers().contains(Recipient.self().getId())); return noMetadata && noMembers; @@ -143,19 +162,20 @@ public class GroupDatabase extends Database { return new Reader(cursor); } - public GroupId getOrCreateGroupForMembers(List members, boolean mms) { + public GroupId.Mms getOrCreateMmsGroupForMembers(List members) { Collections.sort(members); Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID}, MEMBERS + " = ? AND " + MMS + " = ?", - new String[] {RecipientId.toSerializedList(members), mms ? "1" : "0"}, + new String[] {RecipientId.toSerializedList(members), "1"}, null, null, null); try { if (cursor != null && cursor.moveToNext()) { - return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))); + return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))) + .requireMms(); } else { - GroupId groupId = allocateGroupId(mms); - create(groupId, null, members, null, null); + GroupId.Mms groupId = GroupId.createMms(new SecureRandom()); + create(groupId, members); return groupId; } } finally { @@ -197,24 +217,62 @@ public class GroupDatabase extends Database { return new Reader(cursor); } - public @NonNull List getGroupMembers(@NonNull GroupId groupId, boolean includeSelf) { - List members = getCurrentMembers(groupId); - List recipients = new LinkedList<>(); + @WorkerThread + public @NonNull List getGroupMembers(@NonNull GroupId groupId, @NonNull MemberSet memberSet) { + if (groupId.isV2()) { + return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMembers(context, memberSet)) + .or(Collections.emptyList()); + } else { + List currentMembers = getCurrentMembers(groupId); + List recipients = new ArrayList<>(currentMembers.size()); - for (RecipientId member : members) { - if (!includeSelf && Recipient.resolved(member).isLocalNumber()) { - continue; + for (RecipientId member : currentMembers) { + if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) { + recipients.add(Recipient.resolved(member)); + } } - recipients.add(Recipient.resolved(member)); + return recipients; } - - return recipients; } - public void create(@NonNull GroupId groupId, @Nullable String title, @NonNull List members, - @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay) + public void create(@NonNull GroupId.V1 groupId, + @Nullable String title, + @NonNull Collection members, + @Nullable SignalServiceAttachmentPointer avatar, + @Nullable String relay) { + create(groupId, title, members, avatar, relay, null, null); + } + + public void create(@NonNull GroupId.Mms groupId, + @NonNull Collection members) + { + create(groupId, null, members, null, null, null, null); + } + + public void create(@NonNull GroupId.V2 groupId, + @Nullable SignalServiceAttachmentPointer avatar, + @Nullable String relay, + @NonNull GroupMasterKey groupMasterKey, + @NonNull DecryptedGroup groupState) + { + create(groupId, groupState.getTitle(), Collections.emptyList(), avatar, relay, groupMasterKey, groupState); + } + + /** + * @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version). + */ + private void create(@NonNull GroupId groupId, + @Nullable String title, + @NonNull Collection memberCollection, + @Nullable SignalServiceAttachmentPointer avatar, + @Nullable String relay, + @Nullable GroupMasterKey groupMasterKey, + @Nullable DecryptedGroup groupState) + { + List members = new ArrayList<>(new HashSet<>(memberCollection)); + Collections.sort(members); ContentValues contentValues = new ContentValues(); @@ -228,12 +286,29 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); + } else { + contentValues.put(AVATAR_ID, 0); } contentValues.put(AVATAR_RELAY, relay); contentValues.put(TIMESTAMP, System.currentTimeMillis()); contentValues.put(ACTIVE, 1); - contentValues.put(MMS, groupId.isMmsGroup()); + contentValues.put(MMS, groupId.isMms()); + + if (groupMasterKey != null) { + if (groupState == null) { + throw new AssertionError("V2 master key but no group state"); + } + groupId.requireV2(); + contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize()); + contentValues.put(V2_REVISION, groupState.getVersion()); + contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray()); + contentValues.put(MEMBERS, serializeV2GroupMembers(context, groupState)); + } else { + if (groupId.isV2()) { + throw new AssertionError("V2 group id but no master key"); + } + } databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); @@ -243,7 +318,10 @@ public class GroupDatabase extends Database { notifyConversationListListeners(); } - public void update(@NonNull GroupId groupId, String title, SignalServiceAttachmentPointer avatar) { + public void update(@NonNull GroupId.V1 groupId, + @Nullable String title, + @Nullable SignalServiceAttachmentPointer avatar) + { ContentValues contentValues = new ContentValues(); if (title != null) contentValues.put(TITLE, title); @@ -252,6 +330,8 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); + } else { + contentValues.put(AVATAR_ID, 0); } databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, @@ -264,7 +344,30 @@ public class GroupDatabase extends Database { notifyConversationListListeners(); } - public void updateTitle(@NonNull GroupId groupId, String title) { + public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) { + update(GroupId.v2(groupMasterKey), decryptedGroup); + } + + public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) { + String title = decryptedGroup.getTitle(); + ContentValues contentValues = new ContentValues(); + + contentValues.put(TITLE, title); + contentValues.put(V2_REVISION, decryptedGroup.getVersion()); + contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray()); + contentValues.put(MEMBERS, serializeV2GroupMembers(context, decryptedGroup)); + + databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, + GROUP_ID + " = ?", + new String[]{ groupId.toString() }); + + RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); + Recipient.live(groupRecipient).refresh(); + + notifyConversationListListeners(); + } + + public void updateTitle(@NonNull GroupId.V1 groupId, String title) { ContentValues contentValues = new ContentValues(); contentValues.put(TITLE, title); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", @@ -274,20 +377,12 @@ public class GroupDatabase extends Database { Recipient.live(groupRecipient).refresh(); } - public void updateAvatar(@NonNull GroupId groupId, @Nullable Bitmap avatar) { - updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); - } - - public void updateAvatar(@NonNull GroupId groupId, @Nullable byte[] avatar) { - long avatarId; - - if (avatar != null) avatarId = Math.abs(new SecureRandom().nextLong()); - else avatarId = 0; - - - ContentValues contentValues = new ContentValues(2); - contentValues.put(AVATAR, avatar); - contentValues.put(AVATAR_ID, avatarId); + /** + * Used to bust the Glide cache when an avatar changes. + */ + public void onAvatarUpdated(@NonNull GroupId.V1 groupId, boolean hasAvatar) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {groupId.toString()}); @@ -357,10 +452,18 @@ public class GroupDatabase extends Database { database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()}); } - public static GroupId allocateGroupId(boolean mms) { - byte[] groupId = new byte[16]; - new SecureRandom().nextBytes(groupId); - return mms ? GroupId.mms(groupId) : GroupId.v1(groupId); + private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) { + List groupMembers = new ArrayList<>(decryptedGroup.getMembersCount()); + + for (DecryptedMember member : decryptedGroup.getMembersList()) { + Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.fromByteString(member.getUuid()), null)); + + groupMembers.add(recipient.getId()); + } + + Collections.sort(groupMembers); + + return RecipientId.toSerializedList(groupMembers); } public static class Reader implements Closeable { @@ -388,14 +491,16 @@ public class GroupDatabase extends Database { RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), cursor.getString(cursor.getColumnIndexOrThrow(TITLE)), cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), - cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR)), cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)), cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1, cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)), - cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1); + cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1, + cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)), + cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)), + cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP))); } @Override @@ -407,27 +512,27 @@ public class GroupDatabase extends Database { public static class GroupRecord { - private final GroupId id; - private final RecipientId recipientId; - private final String title; - private final List members; - private final byte[] avatar; - private final long avatarId; - private final byte[] avatarKey; - private final byte[] avatarDigest; - private final String avatarContentType; - private final String relay; - private final boolean active; - private final boolean mms; + private final GroupId id; + private final RecipientId recipientId; + private final String title; + private final List members; + private final long avatarId; + private final byte[] avatarKey; + private final byte[] avatarDigest; + private final String avatarContentType; + private final String relay; + private final boolean active; + private final boolean mms; + @Nullable private final V2GroupProperties v2GroupProperties; - public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, byte[] avatar, + public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, long avatarId, byte[] avatarKey, String avatarContentType, - String relay, boolean active, byte[] avatarDigest, boolean mms) + String relay, boolean active, byte[] avatarDigest, boolean mms, + @Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes) { this.id = id; this.recipientId = recipientId; this.title = title; - this.avatar = avatar; this.avatarId = avatarId; this.avatarKey = avatarKey; this.avatarDigest = avatarDigest; @@ -436,6 +541,18 @@ public class GroupDatabase extends Database { this.active = active; this.mms = mms; + V2GroupProperties v2GroupProperties = null; + if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { + GroupMasterKey groupMasterKey; + try { + groupMasterKey = new GroupMasterKey(groupMasterKeyBytes); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + v2GroupProperties = new V2GroupProperties(groupMasterKey, groupRevision, decryptedGroupBytes); + } + this.v2GroupProperties = v2GroupProperties; + if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members); else this.members = new LinkedList<>(); } @@ -456,8 +573,8 @@ public class GroupDatabase extends Database { return members; } - public byte[] getAvatar() { - return avatar; + public boolean hasAvatar() { + return avatarId != 0; } public long getAvatarId() { @@ -487,5 +604,97 @@ public class GroupDatabase extends Database { public boolean isMms() { return mms; } + + public boolean isV2Group() { + return v2GroupProperties != null; + } + + public @NonNull V2GroupProperties requireV2GroupProperties() { + if (v2GroupProperties == null) { + throw new AssertionError(); + } + + return v2GroupProperties; + } + + public boolean isAdmin(@NonNull Recipient recipient) { + return isV2Group() && requireV2GroupProperties().isAdmin(recipient); + } + } + + public static class V2GroupProperties { + + @NonNull private final GroupMasterKey groupMasterKey; + private final int groupRevision; + @NonNull private final byte[] decryptedGroupBytes; + private DecryptedGroup decryptedGroup; + + private V2GroupProperties(@NonNull GroupMasterKey groupMasterKey, int groupRevision, @NonNull byte[] decryptedGroup) { + this.groupMasterKey = groupMasterKey; + this.groupRevision = groupRevision; + this.decryptedGroupBytes = decryptedGroup; + } + + public @NonNull GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + + public int getGroupRevision() { + return groupRevision; + } + + public @NonNull DecryptedGroup getDecryptedGroup() { + try { + if (decryptedGroup == null) { + decryptedGroup = DecryptedGroup.parseFrom(decryptedGroupBytes); + } + return decryptedGroup; + } catch (InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + } + + public boolean isAdmin(@NonNull Recipient recipient) { + return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), recipient.getUuid().get()) + .transform(t -> t.getRole() == Member.Role.ADMINISTRATOR) + .or(false); + } + + public List getMembers(@NonNull Context context, @NonNull MemberSet memberSet) { + boolean includeSelf = memberSet.includeSelf; + DecryptedGroup groupV2 = getDecryptedGroup(); + UUID selfUuid = Recipient.self().getUuid().get(); + List recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount()); + + for (UUID uuid : DecryptedGroupUtil.toUuidList(groupV2.getMembersList())) { + if (includeSelf || !selfUuid.equals(uuid)) { + recipients.add(Recipient.externalPush(context, uuid, null)); + } + } + if (memberSet.includePending) { + for (UUID uuid : DecryptedGroupUtil.pendingToUuidList(groupV2.getPendingMembersList())) { + if (includeSelf || !selfUuid.equals(uuid)) { + recipients.add(Recipient.externalPush(context, uuid, null)); + } + } + } + + return recipients; + } + } + + public enum MemberSet { + FULL_MEMBERS_INCLUDING_SELF(true, false), + FULL_MEMBERS_EXCLUDING_SELF(false, false), + FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), + FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true); + + private boolean includeSelf; + private boolean includePending; + + MemberSet(boolean includeSelf, boolean includePending) { + this.includeSelf = includeSelf; + this.includePending = includePending; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 440e4ee3c..fd038b98f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; + import androidx.annotation.NonNull; import net.sqlcipher.database.SQLiteDatabase; @@ -11,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.recipients.RecipientId; +import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -41,7 +43,7 @@ public class GroupReceiptDatabase extends Database { super(context, databaseHelper); } - public void insert(List recipientIds, long mmsId, int status, long timestamp) { + public void insert(Collection recipientIds, long mmsId, int status, long timestamp) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); for (RecipientId recipientId : recipientIds) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 020c1f3b4..a670a70ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1141,8 +1141,8 @@ public class MmsDatabase extends MessagingDatabase { long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener); if (message.getRecipient().isGroup()) { - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false); GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(), messageId, defaultReceiptStatus, message.getSentTimeMillis()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index ce8031b56..2848c72e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -356,7 +356,7 @@ public class RecipientDatabase extends Database { if (result.neededInsert) { ContentValues values = new ContentValues(); - if (groupId.isMmsGroup()) { + if (groupId.isMms()) { values.put(GROUP_TYPE, GroupType.MMS.getId()); } else { values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); @@ -1406,9 +1406,9 @@ public class RecipientDatabase extends Database { db.update(TABLE_NAME, setBlocked, UUID + " = ?", new String[] { uuid }); } - List groupIdStrings = Stream.of(groupIds).map(GroupId::v1).toList(); + List groupIdStrings = Stream.of(groupIds).map(GroupId::v1).toList(); - for (GroupId groupId : groupIdStrings) { + for (GroupId.V1 groupId : groupIdStrings) { db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId.toString() }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java index 01e47b2a0..57f935893 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsMigrator.java @@ -233,7 +233,7 @@ public class SmsMigrator { List recipientIds = Stream.of(ourRecipients).map(Recipient::getId).toList(); - GroupId ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipientIds, true); + GroupId.Mms ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipientIds); RecipientId ourGroupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(ourGroupId); Recipient ourGroupRecipient = Recipient.resolved(ourGroupRecipientId); long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index f4ae251b5..d59af0c7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -21,7 +21,9 @@ import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteOpenHelper; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -51,14 +53,17 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.util.List; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -118,8 +123,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int GROUPS_V2_RECIPIENT_CAPABILITY = 51; private static final int TRANSFER_FILE_CLEANUP = 52; private static final int PROFILE_DATA_MIGRATION = 53; + private static final int AVATAR_LOCATION_MIGRATION = 54; + private static final int GROUPS_V2 = 55; - private static final int DATABASE_VERSION = 53; + private static final int DATABASE_VERSION = 55; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -802,6 +809,55 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + if (oldVersion < AVATAR_LOCATION_MIGRATION) { + File oldAvatarDirectory = new File(context.getFilesDir(), "avatars"); + File[] results = oldAvatarDirectory.listFiles(); + + if (results != null) { + Log.i(TAG, "Preparing to migrate " + results.length + " avatars."); + + for (File file : results) { + if (Util.isLong(file.getName())) { + try { + AvatarHelper.setAvatar(context, RecipientId.from(file.getName()), new FileInputStream(file)); + } catch(IOException e) { + Log.w(TAG, "Failed to copy file " + file.getName() + "! Skipping."); + } + } else { + Log.w(TAG, "Invalid avatar name '" + file.getName() + "'! Skipping."); + } + } + } else { + Log.w(TAG, "No avatar directory files found."); + } + + if (!FileUtils.deleteDirectory(oldAvatarDirectory)) { + Log.w(TAG, "Failed to delete avatar directory."); + } + + try (Cursor cursor = db.rawQuery("SELECT recipient_id, avatar FROM groups", null)) { + while (cursor != null && cursor.moveToNext()) { + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow("recipient_id"))); + byte[] avatar = cursor.getBlob(cursor.getColumnIndexOrThrow("avatar")); + + try { + AvatarHelper.setAvatar(context, recipientId, avatar != null ? new ByteArrayInputStream(avatar) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to copy avatar for " + recipientId + "! Skipping.", e); + } + } + } + + db.execSQL("UPDATE groups SET avatar_id = 0 WHERE avatar IS NULL"); + db.execSQL("UPDATE groups SET avatar = NULL"); + } + + if (oldVersion < GROUPS_V2) { + db.execSQL("ALTER TABLE groups ADD COLUMN master_key"); + db.execSQL("ALTER TABLE groups ADD COLUMN revision"); + db.execSQL("ALTER TABLE groups ADD COLUMN decrypted_group"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java index 769b4bf38..9048e65cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupId.java @@ -3,27 +3,64 @@ package org.thoughtcrime.securesms.groups; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.signal.zkgroup.groups.GroupIdentifier; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.groups.GroupSecretParams; import org.thoughtcrime.securesms.util.Hex; +import org.thoughtcrime.securesms.util.Util; import java.io.IOException; +import java.security.SecureRandom; -public final class GroupId { +public abstract class GroupId { private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!"; private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!"; + private static final int MMS_BYTE_LENGTH = 16; + private static final int V1_MMS_BYTE_LENGTH = 16; + private static final int V2_BYTE_LENGTH = GroupIdentifier.SIZE; + private static final int V2_ENCODED_LENGTH = ENCODED_SIGNAL_GROUP_PREFIX.length() + V2_BYTE_LENGTH * 2; private final String encodedId; - private GroupId(@NonNull String encodedId) { - this.encodedId = encodedId; + private GroupId(@NonNull String prefix, @NonNull byte[] bytes) { + this.encodedId = prefix + Hex.toStringCondensed(bytes); } - public static @NonNull GroupId v1(byte[] gv1GroupIdBytes) { - return new GroupId(ENCODED_SIGNAL_GROUP_PREFIX + Hex.toStringCondensed(gv1GroupIdBytes)); + public static @NonNull GroupId.Mms mms(byte[] mmsGroupIdBytes) { + return new GroupId.Mms(mmsGroupIdBytes); } - public static @NonNull GroupId mms(byte[] mmsGroupIdBytes) { - return new GroupId(ENCODED_MMS_GROUP_PREFIX + Hex.toStringCondensed(mmsGroupIdBytes)); + public static @NonNull GroupId.V1 v1(byte[] gv1GroupIdBytes) { + if (gv1GroupIdBytes.length == V2_BYTE_LENGTH) { + throw new AssertionError(); + } + return new GroupId.V1(gv1GroupIdBytes); + } + + public static GroupId.V1 createV1(@NonNull SecureRandom secureRandom) { + return v1(Util.getSecretBytes(secureRandom, V1_MMS_BYTE_LENGTH)); + } + + public static GroupId.Mms createMms(@NonNull SecureRandom secureRandom) { + return mms(Util.getSecretBytes(secureRandom, MMS_BYTE_LENGTH)); + } + + public static GroupId.V2 v2(@NonNull byte[] bytes) { + if (bytes.length != V2_BYTE_LENGTH) { + throw new AssertionError(); + } + return new GroupId.V2(bytes); + } + + public static GroupId.V2 v2(@NonNull GroupIdentifier groupIdentifier) { + return v2(groupIdentifier.serialize()); + } + + public static GroupId.V2 v2(@NonNull GroupMasterKey masterKey) { + return v2(GroupSecretParams.deriveFromMasterKey(masterKey) + .getPublicParams() + .getGroupIdentifier()); } public static @NonNull GroupId parse(@NonNull String encodedGroupId) { @@ -33,7 +70,11 @@ public final class GroupId { } byte[] bytes = extractDecodedId(encodedGroupId); - return isMmsGroup(encodedGroupId) ? mms(bytes) : v1(bytes); + + if (encodedGroupId.startsWith(ENCODED_MMS_GROUP_PREFIX)) return mms(bytes); + else if (encodedGroupId.length() == V2_ENCODED_LENGTH) return v2(bytes); + else return v1(bytes); + } catch (IOException e) { throw new AssertionError(e); } @@ -55,10 +96,6 @@ public final class GroupId { return Hex.fromStringCondensed(encodedGroupId.split("!", 2)[1]); } - private static boolean isMmsGroup(@NonNull String groupId) { - return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); - } - public byte[] getDecodedId() { try { return extractDecodedId(encodedId); @@ -67,10 +104,6 @@ public final class GroupId { } } - public boolean isMmsGroup() { - return isMmsGroup(encodedId); - } - @Override public boolean equals(@Nullable Object obj) { if (obj instanceof GroupId) { @@ -90,4 +123,109 @@ public final class GroupId { public String toString() { return encodedId; } + + public abstract boolean isMms(); + + public abstract boolean isV1(); + + public abstract boolean isV2(); + + public abstract boolean isPush(); + + public GroupId.Mms requireMms() { + if (this instanceof GroupId.Mms) return (GroupId.Mms) this; + throw new AssertionError(); + } + + public GroupId.V1 requireV1() { + if (this instanceof GroupId.V1) return (GroupId.V1) this; + throw new AssertionError(); + } + + public GroupId.V2 requireV2() { + if (this instanceof GroupId.V2) return (GroupId.V2) this; + throw new AssertionError(); + } + + public GroupId.Push requirePush() { + if (this instanceof GroupId.Push) return (GroupId.Push) this; + throw new AssertionError(); + } + + public static final class Mms extends GroupId { + + private Mms(@NonNull byte[] bytes) { + super(ENCODED_MMS_GROUP_PREFIX, bytes); + } + + @Override + public boolean isMms() { + return true; + } + + @Override + public boolean isV1() { + return false; + } + + @Override + public boolean isV2() { + return false; + } + + @Override + public boolean isPush() { + return false; + } + } + + public static abstract class Push extends GroupId { + private Push(@NonNull byte[] bytes) { + super(ENCODED_SIGNAL_GROUP_PREFIX, bytes); + } + + @Override + public boolean isMms() { + return false; + } + + @Override + public boolean isPush() { + return true; + } + } + + public static final class V1 extends GroupId.Push { + + private V1(@NonNull byte[] bytes) { + super(bytes); + } + + @Override + public boolean isV1() { + return true; + } + + @Override + public boolean isV2() { + return false; + } + } + + public static final class V2 extends GroupId.Push { + + private V2(@NonNull byte[] bytes) { + super(bytes); + } + + @Override + public boolean isV1() { + return false; + } + + @Override + public boolean isV2() { + return true; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 2293513a1..28c8e5014 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -53,7 +53,7 @@ public final class GroupManager { public static boolean leaveGroup(@NonNull Context context, @NonNull Recipient groupRecipient) { GroupId groupId = groupRecipient.requireGroupId(); - return V1GroupManager.leaveGroup(context, groupId, groupRecipient); + return V1GroupManager.leaveGroup(context, groupId.requireV1(), groupRecipient); } public static class GroupActionResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java rename to app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index b257b6773..150e33f94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Collections; @@ -46,22 +47,29 @@ import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer; import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; -public class GroupMessageProcessor { +public final class GroupV1MessageProcessor { - private static final String TAG = GroupMessageProcessor.class.getSimpleName(); + private static final String TAG = Log.tag(GroupV1MessageProcessor.class); public static @Nullable Long process(@NonNull Context context, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, boolean outgoing) { - if (!message.getGroupInfo().isPresent() || message.getGroupInfo().get().getGroupId() == null) { + SignalServiceGroupContext signalServiceGroupContext = message.getGroupContext().get(); + Optional groupV1 = signalServiceGroupContext.getGroupV1(); + + if (signalServiceGroupContext.getGroupV2().isPresent()) { + throw new AssertionError("Cannot process GV2"); + } + + if (!groupV1.isPresent() || groupV1.get().getGroupId() == null) { Log.w(TAG, "Received group message with no id! Ignoring..."); return null; } GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - SignalServiceGroup group = message.getGroupInfo().get(); + SignalServiceGroup group = groupV1.get(); GroupId id = GroupId.v1(group.getGroupId()); Optional record = database.getGroup(id); @@ -85,7 +93,7 @@ public class GroupMessageProcessor { boolean outgoing) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupId id = GroupId.v1(group.getGroupId()); + GroupId.V1 id = GroupId.v1(group.getGroupId()); GroupContext.Builder builder = createGroupContext(group); builder.setType(GroupContext.Type.UPDATE); @@ -119,7 +127,7 @@ public class GroupMessageProcessor { { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); - GroupId id = GroupId.v1(group.getGroupId()); + GroupId.V1 id = GroupId.v1(group.getGroupId()); Set recordMembers = new HashSet<>(groupRecord.getMembers()); Set messageMembers = new HashSet<>(); @@ -278,7 +286,7 @@ public class GroupMessageProcessor { .map(a -> a.getNumber().get()) .toList()); builder.addAllMembers(Stream.of(group.getMembers().get()) - .map(GroupMessageProcessor::createMember) + .map(GroupV1MessageProcessor::createMember) .toList()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index 451ea29a3..0e7743568 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -19,7 +19,10 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult; import org.thoughtcrime.securesms.jobs.LeaveGroupJob; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -32,6 +35,9 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.SecureRandom; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -39,6 +45,8 @@ import java.util.Set; final class V1GroupManager { + private static final String TAG = Log.tag(V1GroupManager.class); + static @NonNull GroupActionResult createGroup(@NonNull Context context, @NonNull Set memberIds, @Nullable Bitmap avatar, @@ -47,18 +55,28 @@ final class V1GroupManager { { final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - final GroupId groupId = GroupDatabase.allocateGroupId(mms); + final SecureRandom secureRandom = new SecureRandom(); + final GroupId groupId = mms ? GroupId.createMms(secureRandom) : GroupId.createV1(secureRandom); final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); final Recipient groupRecipient = Recipient.resolved(groupRecipientId); memberIds.add(Recipient.self().getId()); - groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null); - if (!mms) { - groupDatabase.updateAvatar(groupId, avatarBytes); + if (groupId.isV1()) { + GroupId.V1 groupIdV1 = groupId.requireV1(); + + groupDatabase.create(groupIdV1, name, memberIds, null, null); + + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } + groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); - return sendGroupUpdate(context, groupId, memberIds, name, avatarBytes); + return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes); } else { + groupDatabase.create(groupId.requireMms(), memberIds); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadId); } @@ -71,18 +89,26 @@ final class V1GroupManager { @Nullable String name) throws InvalidNumberException { - final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); + final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); + final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); memberAddresses.add(Recipient.self().getId()); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); - groupDatabase.updateTitle(groupId, name); - groupDatabase.updateAvatar(groupId, avatarBytes); - if (!groupId.isMmsGroup()) { - return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); + if (groupId.isPush()) { + GroupId.V1 groupIdV1 = groupId.requireV1(); + + groupDatabase.updateTitle(groupIdV1, name); + groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null); + + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } + return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes); } else { - RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); return new GroupActionResult(groupRecipient, threadId); @@ -90,7 +116,7 @@ final class V1GroupManager { } private static GroupActionResult sendGroupUpdate(@NonNull Context context, - @NonNull GroupId groupId, + @NonNull GroupId.V1 groupId, @NonNull Set members, @Nullable String groupName, @Nullable byte[] avatar) @@ -104,7 +130,7 @@ final class V1GroupManager { for (RecipientId member : members) { Recipient recipient = Recipient.resolved(member); - uuidMembers.add(GroupMessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient))); + uuidMembers.add(GroupV1MessageProcessor.createMember(RecipientUtil.toSignalServiceAddress(context, recipient))); } GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() @@ -127,11 +153,16 @@ final class V1GroupManager { } @WorkerThread - static boolean leaveGroup(@NonNull Context context, @NonNull GroupId groupId, @NonNull Recipient groupRecipient) { + static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId, @NonNull Recipient groupRecipient) { long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); if (threadId != -1 && leaveMessage.isPresent()) { + try { + DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(leaveMessage.get(), threadId, false, null); + } catch (MmsException e) { + Log.w(TAG, "Failed to insert leave message.", e); + } ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient)); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 90b113d86..0dfbb72fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Hex; @@ -37,9 +38,9 @@ public class AvatarDownloadJob extends BaseJob { private static final String KEY_GROUP_ID = "group_id"; - private @NonNull GroupId groupId; + private @NonNull GroupId.V1 groupId; - public AvatarDownloadJob(@NonNull GroupId groupId) { + public AvatarDownloadJob(@NonNull GroupId.V1 groupId) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(10) @@ -47,7 +48,7 @@ public class AvatarDownloadJob extends BaseJob { groupId); } - private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId groupId) { + private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId.V1 groupId) { super(parameters); this.groupId = groupId; } @@ -90,13 +91,14 @@ public class AvatarDownloadJob extends BaseJob { SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent()); - InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); - Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); + InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + + AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); + DatabaseFactory.getGroupDatabase(context).onAvatarUpdated(groupId, true); - database.updateAvatar(groupId, avatar); inputStream.close(); } - } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { + } catch (NonSuccessfulResponseCodeException | InvalidMessageException e) { Log.w(TAG, e); } finally { if (attachment != null) @@ -116,7 +118,7 @@ public class AvatarDownloadJob extends BaseJob { public static final class Factory implements Job.Factory { @Override public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID))); + return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)).requireV1()); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java index be14b8086..4f91c71b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java @@ -51,7 +51,7 @@ public class LeaveGroupJob extends BaseJob { private static final String KEY_MEMBERS = "members"; private static final String KEY_RECIPIENTS = "recipients"; - private final GroupId groupId; + private final GroupId.Push groupId; private final String name; private final List members; private final List recipients; @@ -60,7 +60,7 @@ public class LeaveGroupJob extends BaseJob { List members = Stream.of(group.resolve().getParticipants()).map(Recipient::getId).toList(); members.remove(Recipient.self().getId()); - return new LeaveGroupJob(group.getGroupId().get(), + return new LeaveGroupJob(group.getGroupId().get().requirePush(), group.resolve().getDisplayName(ApplicationDependencies.getApplication()), members, members, @@ -72,7 +72,7 @@ public class LeaveGroupJob extends BaseJob { .build()); } - private LeaveGroupJob(@NonNull GroupId groupId, + private LeaveGroupJob(@NonNull GroupId.Push groupId, @NonNull String name, @NonNull List members, @NonNull List recipients, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 5468db6da..65eceaaef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -232,7 +232,7 @@ public class MmsDownloadJob extends BaseJob { if (members.size() > 2) { List recipients = new ArrayList<>(members); - group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipients, true)); + group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients)); } IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java index 7371c1858..66979aed4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -25,6 +25,7 @@ import com.klinker.android.send_message.Utils; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.Util; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.security.SecureRandom; import java.util.Arrays; import java.util.List; @@ -231,7 +233,7 @@ public final class MmsSendJob extends SendJob { } if (message.getRecipient().isMmsGroup()) { - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient member : members) { if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) { @@ -271,7 +273,7 @@ public final class MmsSendJob extends SendJob { PduPart part = new PduPart(); if (fileName == null) { - fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong())); + fileName = String.valueOf(Math.abs(new SecureRandom().nextLong())); String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType()); if (fileExtension != null) fileName = fileName + "." + fileExtension; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java index ca4405acc..bf52919e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -100,7 +101,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob { out.write(new DeviceGroup(record.getId().getDecodedId(), Optional.fromNullable(record.getTitle()), members, - getAvatar(record.getAvatar()), + getAvatar(record.getRecipientId()), record.isActive(), expirationTimer, Optional.of(recipient.getColor().serialize()), @@ -151,13 +152,13 @@ public class MultiDeviceGroupUpdateJob extends BaseJob { } - private Optional getAvatar(@Nullable byte[] avatar) { - if (avatar == null) return Optional.absent(); + private Optional getAvatar(@NonNull RecipientId recipientId) throws IOException { + if (!AvatarHelper.hasAvatar(context, recipientId)) return Optional.absent(); return Optional.of(SignalServiceAttachment.newStreamBuilder() - .withStream(new ByteArrayInputStream(avatar)) + .withStream(AvatarHelper.getAvatar(context, recipientId)) .withContentType("image/*") - .withLength(avatar.length) + .withLength(AvatarHelper.getAvatarLength(context, recipientId)) .build()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index 670a87c70..72d0ae9fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -29,13 +29,13 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.util.guava.Optional; @@ -233,7 +233,7 @@ public final class PushDecryptMessageJob extends BaseJob { return new PushProcessMessageJob.ExceptionMetadata(sender, e.getSenderDevice(), - e.getGroup().transform(g -> GroupId.v1(g.getGroupId())).orNull()); + e.getGroup().transform(GroupUtil::idFromGroupContext).orNull()); } private static PushProcessMessageJob.ExceptionMetadata toExceptionMetadata(@NonNull ProtocolException e) throws NoSenderException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 833659ac5..f95cdb331 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; @@ -299,7 +300,7 @@ public class PushGroupSendJob extends PushSendJob { List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId); if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList(); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); return Stream.of(members).map(Recipient::getId).toList(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index 54cb01b52..4fb269e55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -80,16 +81,16 @@ public class PushGroupUpdateJob extends BaseJob { Optional record = groupDatabase.getGroup(groupId); SignalServiceAttachment avatar = null; - if (record == null) { + if (record == null || !record.isPresent()) { Log.w(TAG, "No information for group record info request: " + groupId.toString()); return; } - if (record.get().getAvatar() != null) { + if (AvatarHelper.hasAvatar(context, record.get().getRecipientId())) { avatar = SignalServiceAttachmentStream.newStreamBuilder() .withContentType("image/jpeg") - .withStream(new ByteArrayInputStream(record.get().getAvatar())) - .withLength(record.get().getAvatar().length) + .withStream(AvatarHelper.getAvatar(context, record.get().getRecipientId())) + .withLength(AvatarHelper.getAvatarLength(context, record.get().getRecipientId())) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 26be5a3bb..daf53f6c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupMessageProcessor; +import org.thoughtcrime.securesms.groups.GroupV1MessageProcessor; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; @@ -75,6 +75,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; @@ -86,6 +87,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; @@ -263,17 +265,24 @@ public final class PushProcessMessageJob extends BaseJob { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); + Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); + boolean isGv2Message = groupId.isPresent() && groupId.get().isV2(); - if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), toEncodedId(message.getGroupInfo()), content.getTimestamp(), smsMessageId); + if (isGv2Message) { + Log.w(TAG, "Ignoring GV2 message."); + return; + } + + if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId); else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); - else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); + else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); else if (message.getReaction().isPresent()) handleReaction(content, message); else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId); - else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId); + else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId); - if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getGroupInfo().get().getGroupId()))) { - handleUnknownGroupMessage(content, message.getGroupInfo().get()); + if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { + handleUnknownGroupMessage(content, message.getGroupContext().get()); } if (message.getProfileKey().isPresent()) { @@ -327,10 +336,6 @@ public final class PushProcessMessageJob extends BaseJob { } } - private static @NonNull Optional toEncodedId(@NonNull Optional groupInfo) { - return groupInfo.transform(g -> GroupId.v1(g.getGroupId())); - } - private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional smsMessageId) { switch (messageState) { @@ -415,7 +420,7 @@ public final class PushProcessMessageJob extends BaseJob { { Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size()); - ArrayList iceCandidates = new ArrayList(messages.size()); + ArrayList iceCandidates = new ArrayList<>(messages.size()); long callId = -1; for (IceUpdateMessage iceMessage : messages) { iceCandidates.add(new IceCandidateParcel(iceMessage)); @@ -526,12 +531,12 @@ public final class PushProcessMessageJob extends BaseJob { return threadId; } - private void handleGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId) + private void handleGroupV1Message(@NonNull SignalServiceContent content, + @NonNull SignalServiceDataMessage message, + @NonNull Optional smsMessageId) throws StorageFailedException { - GroupMessageProcessor.process(context, content, message, false); + GroupV1MessageProcessor.process(context, content, message, false); if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) { handleExpirationUpdate(content, message, Optional.absent()); @@ -543,12 +548,17 @@ public final class PushProcessMessageJob extends BaseJob { } private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceGroup group) + @NonNull SignalServiceGroupContext group) { - if (group.getType() != SignalServiceGroup.Type.REQUEST_INFO) { - ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(group.getGroupId()))); + if (group.getGroupV1().isPresent()) { + SignalServiceGroup groupV1 = group.getGroupV1().get(); + if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) { + ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId()))); + } else { + Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring."); + } } else { - Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring."); + Log.w(TAG, "Received a message for a group we don't know about without a GV1 context. Ignoring."); } } @@ -567,7 +577,7 @@ public final class PushProcessMessageJob extends BaseJob { false, content.isNeedsReceipt(), Optional.absent(), - message.getGroupInfo(), + message.getGroupContext(), Optional.absent(), Optional.absent(), Optional.absent(), @@ -729,8 +739,8 @@ public final class PushProcessMessageJob extends BaseJob { handleGroupRecipientUpdate(message); } else if (message.getMessage().isEndSession()) { threadId = handleSynchronizeSentEndSessionMessage(message); - } else if (message.getMessage().isGroupUpdate()) { - threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true); + } else if (message.getMessage().isGroupV1Update()) { + threadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true); } else if (message.getMessage().isExpirationUpdate()) { threadId = handleSynchronizeSentExpirationUpdate(message); } else if (message.getMessage().getReaction().isPresent()) { @@ -743,8 +753,8 @@ public final class PushProcessMessageJob extends BaseJob { threadId = handleSynchronizeSentTextMessage(message); } - if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId()))) { - handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); + if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) { + handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get()); } if (message.getMessage().getProfileKey().isPresent()) { @@ -854,7 +864,7 @@ public final class PushProcessMessageJob extends BaseJob { message.isViewOnce(), content.isNeedsReceipt(), message.getBody(), - message.getGroupInfo(), + message.getGroupContext(), message.getAttachments(), quote, sharedContacts, @@ -1020,7 +1030,7 @@ public final class PushProcessMessageJob extends BaseJob { private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) { GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); List messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList(); - List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, false); + List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); Map localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId)) .collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); @@ -1040,7 +1050,8 @@ public final class PushProcessMessageJob extends BaseJob { private void handleTextMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, - @NonNull Optional smsMessageId) + @NonNull Optional smsMessageId, + @NonNull Optional groupId) throws StorageFailedException { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); @@ -1053,7 +1064,7 @@ public final class PushProcessMessageJob extends BaseJob { Long threadId; - if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { + if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) { threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; } else { notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice()); @@ -1061,7 +1072,7 @@ public final class PushProcessMessageJob extends BaseJob { IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), message.getTimestamp(), body, - toEncodedId(message.getGroupInfo()), + groupId, message.getExpiresInSeconds() * 1000L, content.isNeedsReceipt()); @@ -1488,20 +1499,20 @@ public final class PushProcessMessageJob extends BaseJob { return database.insertMessageInbox(textMessage); } - private Recipient getSyncMessageDestination(SentTranscriptMessage message) { - if (message.getMessage().getGroupInfo().isPresent()) { - return Recipient.externalGroup(context, GroupId.v1(message.getMessage().getGroupInfo().get().getGroupId())); - } else { - return Recipient.externalPush(context, message.getDestination().get()); - } + private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) { + return getGroupRecipient(message.getMessage().getGroupContext()) + .or(() -> Recipient.externalPush(context, message.getDestination().get())); } - private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.getGroupInfo().isPresent()) { - return Recipient.externalGroup(context, GroupId.v1(message.getGroupInfo().get().getGroupId())); - } else { - return Recipient.externalPush(context, content.getSender()); - } + private Recipient getMessageDestination(@NonNull SignalServiceContent content, + @NonNull SignalServiceDataMessage message) + { + return getGroupRecipient(message.getGroupContext()) + .or(() -> Recipient.externalPush(context, content.getSender())); + } + + private Optional getGroupRecipient(Optional message) { + return message.transform(groupContext -> Recipient.externalGroup(context, GroupUtil.idFromGroupContext(groupContext))); } private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) { @@ -1530,8 +1541,7 @@ public final class PushProcessMessageJob extends BaseJob { return true; } else if (conversation.isGroup()) { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - Optional groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupId.v1(message.getGroupInfo().get().getGroupId())) - : Optional.absent(); + Optional groupId = message.getGroupContext().transform(GroupUtil::idFromGroupContext); if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { return false; @@ -1540,9 +1550,9 @@ public final class PushProcessMessageJob extends BaseJob { boolean isTextMessage = message.getBody().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent(); boolean isExpireMessage = message.isExpirationUpdate(); - boolean isContentMessage = !message.isGroupUpdate() && !isExpireMessage && (isTextMessage || isMediaMessage); + boolean isContentMessage = !message.isGroupV1Update() && !isExpireMessage && (isTextMessage || isMediaMessage); boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); - boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; + boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT; return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index d41eff7bf..cce8b3181 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.jobs; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -64,6 +66,11 @@ public class RefreshOwnProfileJob extends BaseJob { @Override protected void onRun() throws Exception { + if (!TextSecurePreferences.isPushRegistered(context) || TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) { + Log.w(TAG, "Not yet registered!"); + return; + } + Recipient self = Recipient.self(); ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, self, getRequestType(self)); SignalServiceProfile profile = profileAndCredential.getProfile(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 96bf2fdc7..087b9a015 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -97,17 +97,10 @@ public class RetrieveProfileAvatarJob extends BaseJob { File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); try { - SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); - File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); + SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); + InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); - try { - Util.copy(avatarStream, new FileOutputStream(decryptDestination)); - } catch (AssertionError e) { - throw new IOException("Failed to copy stream. Likely a Conscrypt issue.", e); - } - - decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getId())); + AvatarHelper.setAvatar(context, recipient.getId(), avatarStream); } catch (PushNetworkException e) { if (e.getCause() instanceof NonSuccessfulResponseCodeException) { Log.w(TAG, "Removing profile avatar (no image available) for: " + recipient.getId().serialize()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index f6528f6b7..a115ca367 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -10,6 +10,7 @@ import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -137,7 +138,7 @@ public class RetrieveProfileJob extends BaseJob { } private void handleGroupRecipient(Recipient group) throws IOException { - List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), false); + List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); for (Recipient recipient : recipients) { handleIndividualRecipient(recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java index fa3cd4b90..0ed2f8950 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -6,6 +6,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -85,7 +86,7 @@ public class TypingSendJob extends BaseJob { Optional groupId = Optional.absent(); if (recipient.isGroup()) { - recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), false); + recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); groupId = Optional.of(recipient.requireGroupId().getDecodedId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index 37343f5b8..2acb0bc4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -17,6 +17,7 @@ import androidx.lifecycle.ViewModelProviders; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOptions; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; import org.thoughtcrime.securesms.util.MediaUtil; @@ -27,7 +28,7 @@ import java.util.Collections; public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaPickerFolderFragment.Controller, MediaPickerItemFragment.Controller { - private static final Point AVATAR_DIMENSIONS = new Point(1024, 1024); + private static final Point AVATAR_DIMENSIONS = new Point(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS); private static final String IMAGE_CAPTURE = "IMAGE_CAPTURE"; private static final String IMAGE_EDITOR = "IMAGE_EDITOR"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Controller.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Controller.java index 69d7b0e0a..5432030b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Controller.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Controller.java @@ -168,6 +168,7 @@ class Camera1Controller { while (i < sizes.size() && (sizes.get(i).width * sizes.get(i).height) < (width * height)) { i++; } + i++; return sizes.get(Math.min(i, sizes.size() - 1)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java index 439f061f3..508ac58fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.mediasend; import android.annotation.SuppressLint; import androidx.annotation.NonNull; -import androidx.camera.core.CameraX; import androidx.fragment.app.Fragment; import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; @@ -14,7 +13,7 @@ public interface CameraFragment { @SuppressLint("RestrictedApi") static Fragment newInstance() { - if (CameraXUtil.isSupported() && CameraX.isInitialized()) { + if (CameraXUtil.isSupported()) { return CameraXFragment.newInstance(); } else { return Camera1Fragment.newInstance(); @@ -23,7 +22,7 @@ public interface CameraFragment { @SuppressLint("RestrictedApi") static Fragment newInstanceForAvatarCapture() { - if (CameraXUtil.isSupported() && CameraX.isInitialized()) { + if (CameraXUtil.isSupported()) { return CameraXFragment.newInstanceForAvatarCapture(); } else { return Camera1Fragment.newInstance(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java index e1966f70a..4396df831 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Util; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -64,7 +65,7 @@ public class AvatarMigrationJob extends MigrationJob { Recipient recipient = Recipient.external(context, file.getName()); byte[] data = Util.readFully(new FileInputStream(file)); - AvatarHelper.setAvatar(context, recipient.getId(), data); + AvatarHelper.setAvatar(context, recipient.getId(), new ByteArrayInputStream(data)); } else { Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index c7905f062..de4575065 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -8,9 +8,10 @@ import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import java.util.Collections; import java.util.LinkedList; @@ -68,7 +69,7 @@ public class IncomingMediaMessage { boolean viewOnce, boolean unidentified, Optional body, - Optional group, + Optional group, Optional> attachments, Optional quote, Optional> sharedContacts, @@ -86,7 +87,7 @@ public class IncomingMediaMessage { this.quote = quote.orNull(); this.unidentified = unidentified; - if (group.isPresent()) this.groupId = GroupId.v1(group.get().getGroupId()); + if (group.isPresent()) this.groupId = GroupUtil.idFromGroupContext(group.get()); else this.groupId = null; this.attachments.addAll(PointerAttachment.forPointers(attachments)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 596015d6c..e6b881621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -537,7 +537,7 @@ public class MessageNotifier { } if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck)); + notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, timestamp, slideDeck, false)); } } @@ -571,7 +571,7 @@ public class MessageNotifier { } if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), null)); + notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), timestamp, null, true)); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java index 987c7f593..d76910c1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -3,43 +3,53 @@ package org.thoughtcrime.securesms.notifications; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.net.Uri; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.TaskStackBuilder; import org.thoughtcrime.securesms.conversation.ConversationActivity; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; public class NotificationItem { - private final long id; - private final boolean mms; - private final @NonNull Recipient conversationRecipient; - private final @NonNull Recipient individualRecipient; - private final @Nullable Recipient threadRecipient; - private final long threadId; - private final @Nullable CharSequence text; - private final long timestamp; - private final @Nullable SlideDeck slideDeck; + private final long id; + private final boolean mms; + @NonNull private final Recipient conversationRecipient; + @NonNull private final Recipient individualRecipient; + @Nullable private final Recipient threadRecipient; + private final long threadId; + @Nullable private final CharSequence text; + private final long notificationTimestamp; + private final long messageReceivedTimestamp; + @Nullable private final SlideDeck slideDeck; + private final boolean jumpToMessage; - public NotificationItem(long id, boolean mms, - @NonNull Recipient individualRecipient, - @NonNull Recipient conversationRecipient, - @Nullable Recipient threadRecipient, - long threadId, @Nullable CharSequence text, long timestamp, - @Nullable SlideDeck slideDeck) + public NotificationItem(long id, + boolean mms, + @NonNull Recipient individualRecipient, + @NonNull Recipient conversationRecipient, + @Nullable Recipient threadRecipient, + long threadId, + @Nullable CharSequence text, + long notificationTimestamp, + long messageReceivedTimestamp, + @Nullable SlideDeck slideDeck, + boolean jumpToMessage) { - this.id = id; - this.mms = mms; - this.individualRecipient = individualRecipient; - this.conversationRecipient = conversationRecipient; - this.threadRecipient = threadRecipient; - this.text = text; - this.threadId = threadId; - this.timestamp = timestamp; - this.slideDeck = slideDeck; + this.id = id; + this.mms = mms; + this.individualRecipient = individualRecipient; + this.conversationRecipient = conversationRecipient; + this.threadRecipient = threadRecipient; + this.text = text; + this.threadId = threadId; + this.notificationTimestamp = notificationTimestamp; + this.messageReceivedTimestamp = messageReceivedTimestamp; + this.slideDeck = slideDeck; + this.jumpToMessage = jumpToMessage; } public @NonNull Recipient getRecipient() { @@ -55,7 +65,7 @@ public class NotificationItem { } public long getTimestamp() { - return timestamp; + return notificationTimestamp; } public long getThreadId() { @@ -67,12 +77,9 @@ public class NotificationItem { } public PendingIntent getPendingIntent(Context context) { - Intent intent = new Intent(context, ConversationActivity.class); - Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient; - - intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, notifyRecipients.getId()); - intent.putExtra("thread_id", threadId); - intent.setData((Uri.parse("custom://"+System.currentTimeMillis()))); + Recipient recipient = threadRecipient != null ? threadRecipient : conversationRecipient; + int startingPosition = jumpToMessage ? getStartingPosition(context, threadId, messageReceivedTimestamp) : -1; + Intent intent = ConversationActivity.buildIntent(context, recipient.getId(), threadId, 0, -1, startingPosition); return TaskStackBuilder.create(context) .addNextIntentWithParentStack(intent) @@ -86,4 +93,8 @@ public class NotificationItem { public boolean isMms() { return mms; } + + private static int getStartingPosition(@NonNull Context context, long threadId, long receivedTimestampMs) { + return DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionInConversation(threadId, receivedTimestampMs); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 93db338ad..d91951748 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -6,81 +6,191 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.annimon.stream.Stream; - +import org.thoughtcrime.securesms.crypto.AttachmentSecret; +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.ByteUnit; import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.util.StreamDetails; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.LinkedList; -import java.util.List; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Iterator; public class AvatarHelper { + private static final String TAG = Log.tag(AvatarHelper.class); + + public static int AVATAR_DIMENSIONS = 1024; + public static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(10); + private static final String AVATAR_DIRECTORY = "avatars"; - public static InputStream getInputStreamFor(@NonNull Context context, @NonNull RecipientId recipientId) - throws IOException - { - return new FileInputStream(getAvatarFile(context, recipientId)); - } - - public static List getAvatarFiles(@NonNull Context context) { - File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); + /** + * Retrieves an iterable set of avatars. Only intended to be used during backup. + */ + public static Iterable getAvatars(@NonNull Context context) { + File avatarDirectory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE); File[] results = avatarDirectory.listFiles(); - if (results == null) return new LinkedList<>(); - else return Stream.of(results).toList(); + if (results == null) { + return Collections.emptyList(); + } + + return () -> { + return new Iterator() { + int i = 0; + @Override + public boolean hasNext() { + return i < results.length; + } + + @Override + public Avatar next() { + File file = results[i]; + try { + return new Avatar(getAvatar(context, RecipientId.from(file.getName())), + file.getName(), + ModernEncryptingPartOutputStream.getPlaintextLength(file.length())); + } catch (IOException e) { + return null; + } finally { + i++; + } + } + }; + }; } + /** + * Deletes and avatar. + */ public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) { getAvatarFile(context, recipientId).delete(); } - public static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { - File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); - avatarDirectory.mkdirs(); - - return new File(avatarDirectory, new File(recipientId.serialize()).getName()); + /** + * Whether or not an avatar is present for the given recipient. + */ + public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) { + return getAvatarFile(context, recipientId).exists(); } - public static void setAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable byte[] data) - throws IOException + /** + * Retrieves a stream for an avatar. If there is no avatar, the stream will likely throw an + * IOException. It is recommended to call {@link #hasAvatar(Context, RecipientId)} first. + */ + public static @NonNull InputStream getAvatar(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + File avatarFile = getAvatarFile(context, recipientId); + + return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0); + } + + /** + * Returns the size of the avatar on disk. + */ + public static long getAvatarLength(@NonNull Context context, @NonNull RecipientId recipientId) { + return ModernEncryptingPartOutputStream.getPlaintextLength(getAvatarFile(context, recipientId).length()); + } + + /** + * Saves the contents of the input stream as the avatar for the specified recipient. If you pass + * in null for the stream, the avatar will be deleted. + */ + public static void setAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream) + throws IOException { - if (data == null) { + if (inputStream == null) { delete(context, recipientId); - } else { - FileOutputStream out = new FileOutputStream(getAvatarFile(context, recipientId)); - out.write(data); - out.close(); + return; + } + + OutputStream outputStream = null; + try { + outputStream = getOutputStream(context, recipientId); + Util.copy(inputStream, outputStream); + } finally { + Util.close(outputStream); } } - public static @NonNull StreamDetails avatarStream(@NonNull byte[] data) { - return new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length); + /** + * Retrieves an output stream you can write to that will be saved as the avatar for the specified + * recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}. + */ + public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + File targetFile = getAvatarFile(context, recipientId); + return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second; } - public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) { - File avatarFile = getAvatarFile(context, Recipient.self().getId()); + /** + * Returns the timestamp of when the avatar was last modified, or zero if the avatar doesn't exist. + */ + public static long getLastModified(@NonNull Context context, @NonNull RecipientId recipientId) { + File file = getAvatarFile(context, recipientId); - if (avatarFile.exists() && avatarFile.length() > 0) { - try { - FileInputStream stream = new FileInputStream(avatarFile); - - return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length()); - } catch (FileNotFoundException e) { - throw new AssertionError(e); - } + if (file.exists()) { + return file.lastModified(); } else { + return 0; + } + } + + /** + * Returns a {@link StreamDetails} for the local user's own avatar, or null if one does not exist. + */ + public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) { + RecipientId selfId = Recipient.self().getId(); + + if (!hasAvatar(context, selfId)) { + return null; + } + + try { + InputStream stream = getAvatar(context, selfId); + return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, getAvatarLength(context, selfId)); + } catch (IOException e) { + Log.w(TAG, "Failed to read own avatar!", e); return null; } } + + private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { + File directory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE); + return new File(directory, recipientId.serialize()); + } + + public static class Avatar { + private final InputStream inputStream; + private final String filename; + private final long length; + + public Avatar(@NonNull InputStream inputStream, @NonNull String filename, long length) { + this.inputStream = inputStream; + this.filename = filename; + this.length = length; + } + + public @NonNull InputStream getInputStream() { + return inputStream; + } + + public @NonNull String getFilename() { + return filename; + } + + public long getLength() { + return length; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java index dbfcce1aa..26ee62ca0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.SecureRandom; import java.util.Arrays; @@ -76,10 +77,10 @@ class EditProfileRepository { void getCurrentAvatar(@NonNull Consumer avatarConsumer) { RecipientId selfId = Recipient.self().getId(); - if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) { + if (AvatarHelper.hasAvatar(context, selfId)) { SimpleTask.run(() -> { try { - return Util.readFully(AvatarHelper.getInputStreamFor(context, selfId)); + return Util.readFully(AvatarHelper.getAvatar(context, selfId)); } catch (IOException e) { Log.w(TAG, e); return null; @@ -106,7 +107,7 @@ class EditProfileRepository { DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName); try { - AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar); + AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar != null ? new ByteArrayInputStream(avatar) : null); } catch (IOException e) { return UploadResult.ERROR_FILE_IO; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index 8b584f81b..eb91a9701 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -197,11 +197,11 @@ public final class LiveRecipient { List members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchRecipientFromDisk).toList(); Optional avatarId = Optional.absent(); - if (settings.getGroupId() != null && !settings.getGroupId().isMmsGroup() && title == null) { + if (settings.getGroupId() != null && settings.getGroupId().isPush() && title == null) { title = unnamedGroupName; } - if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) { + if (groupRecord.get().hasAvatar()) { avatarId = Optional.of(groupRecord.get().getAvatarId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 6d92dfdcc..6350dcace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -381,7 +381,7 @@ public class Recipient { } public @Nullable String getName(@NonNull Context context) { - if (this.name == null && groupId != null && groupId.isMmsGroup()) { + if (this.name == null && groupId != null && groupId.isMms()) { List names = new LinkedList<>(); for (Recipient recipient : participants) { @@ -567,12 +567,12 @@ public class Recipient { public boolean isMmsGroup() { GroupId groupId = resolve().groupId; - return groupId != null && groupId.isMmsGroup(); + return groupId != null && groupId.isMms(); } public boolean isPushGroup() { GroupId groupId = resolve().groupId; - return groupId != null && !groupId.isMmsGroup(); + return groupId != null && groupId.isPush(); } public @NonNull List getParticipants() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index f8cfc0dc6..0ba0f5571 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -1690,7 +1690,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, RemotePeer remotePeer = (RemotePeer)remote; Intent intent = new Intent(this, WebRtcCallService.class); - ArrayList iceCandidateParcels = new ArrayList(iceCandidates.size()); + ArrayList iceCandidateParcels = new ArrayList<>(iceCandidates.size()); for (IceCandidate iceCandidate : iceCandidates) { iceCandidateParcels.add(new IceCandidateParcel(iceCandidate)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java index 1937296c4..865325b5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java @@ -49,13 +49,13 @@ public final class FileUtils { } } - public static void deleteDirectory(@Nullable File directory) { + public static boolean deleteDirectory(@Nullable File directory) { if (directory == null || !directory.exists() || !directory.isDirectory()) { - return; + return false; } deleteDirectoryContents(directory); - directory.delete(); + return directory.delete(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java index 710408690..8650e46d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -34,6 +35,26 @@ public final class GroupUtil { private static final String TAG = Log.tag(GroupUtil.class); + /** + * Result may be a v1 or v2 GroupId. + */ + public static GroupId idFromGroupContext(@NonNull SignalServiceGroupContext groupContext) { + if (groupContext.getGroupV1().isPresent()) { + return GroupId.v1(groupContext.getGroupV1().get().getGroupId()); + } else if (groupContext.getGroupV2().isPresent()) { + return GroupId.v2(groupContext.getGroupV2().get().getMasterKey()); + } else { + throw new AssertionError(); + } + } + + /** + * Result may be a v1 or v2 GroupId. + */ + public static @NonNull Optional idFromGroupContext(@NonNull Optional groupContext) { + return groupContext.transform(GroupUtil::idFromGroupContext); + } + @WorkerThread public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { GroupId encodedGroupId = groupRecipient.requireGroupId(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 01ed44172..a72c47940 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -224,7 +224,9 @@ public class Util { } } - public static void close(Closeable closeable) { + public static void close(@Nullable Closeable closeable) { + if (closeable == null) return; + try { closeable.close(); } catch (IOException e) { @@ -426,13 +428,13 @@ public class Util { } public static byte[] getSecretBytes(int size) { - byte[] secret = new byte[size]; - getSecureRandom().nextBytes(secret); - return secret; + return getSecretBytes(new SecureRandom(), size); } - public static SecureRandom getSecureRandom() { - return new SecureRandom(); + public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size) { + byte[] secret = new byte[size]; + secureRandom.nextBytes(secret); + return secret; } public static int getDaysTillBuildExpiry() { @@ -607,4 +609,12 @@ public class Util { return concat; } + public static boolean isLong(String value) { + try { + Long.parseLong(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } } diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index b8f1d21b1..faedaac1d 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -208,7 +208,8 @@ android:tint="?conversation_list_compose_icon_tint" app:layout_constraintBottom_toTopOf="@id/megaphone_container" app:layout_constraintEnd_toEndOf="parent" - app:srcCompat="@drawable/ic_compose_solid_24" /> + app:srcCompat="@drawable/ic_compose_solid_24" + app:backgroundTint="@color/core_ultramarine"/> diff --git a/app/src/main/res/layout/emoji_variation_selector.xml b/app/src/main/res/layout/emoji_variation_selector.xml index 249ed5de6..9fcdf73b4 100644 --- a/app/src/main/res/layout/emoji_variation_selector.xml +++ b/app/src/main/res/layout/emoji_variation_selector.xml @@ -1,9 +1,14 @@ - + android:layout_height="wrap_content"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_right_summary_widget.xml b/app/src/main/res/layout/preference_right_summary_widget.xml index a217344e8..e8f175d5e 100644 --- a/app/src/main/res/layout/preference_right_summary_widget.xml +++ b/app/src/main/res/layout/preference_right_summary_widget.xml @@ -10,6 +10,6 @@ android:layout_gravity="right|center_vertical" android:gravity="right|center_vertical" android:textSize="16sp" - android:textColor="@color/core_ultramarine"/> + android:textColor="?colorAccent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/quote_view.xml b/app/src/main/res/layout/quote_view.xml index 2f76c04d9..532411da8 100644 --- a/app/src/main/res/layout/quote_view.xml +++ b/app/src/main/res/layout/quote_view.xml @@ -178,6 +178,6 @@ android:layout_gravity="top|end" android:background="@drawable/dismiss_background" android:src="@drawable/ic_close_white_18dp" - android:tint="@color/core_grey_70" /> + android:tint="?quote_dismiss_button_tint" /> \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9159a4943..56b86322e 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -395,34 +395,6 @@ هذا الهاتف لا يدعم خدمات Play. أنقر لإلغاء تفعيل ميزة الأداء الأمثل للبطارية والتي تمنع Signal من استقبال الرسائل عند عدم النشاط. شارك مع - - - - - - - - - - - - - - - - - - - - - - - - - - - - جار استعادة رسالة… @@ -1710,8 +1682,6 @@ جارٍ نقل قاعدة بيانات Signal رسالة مُقفلة جديدة فك القفل لعرض الرسائل المعلّقة - - العبارة السرّية للنسخة الإحتياطية سيتم حفظ النسخ الاحتياطية في سعة التخزين الخارجيّة وستكون مشفرة بالعبارة السرية أدناه. من الضروري حفظ هذه العبارة من أجل استعادة أي نسخة احتياطيّة. قمت بتدوين هذه العبارة السرية. بدونها، لن يمكنني استعادة أي نسخة احتياطية. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 03c638b1b..0b50b9fd0 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -331,30 +331,6 @@ Това устройство не поддържа Play услуги. Натиснете, за да изключите оптимизациите на батерият, за да преустановите опитите на Signal да изтегля съобщения, когато не е активен. Споделяне с - - - - - - - - - - - - - - - - - - - - - - - - Изтегляне на съобщение… @@ -1345,8 +1321,6 @@ Мигриране на базата данни на Signal Ново заключено съобщение Отключете, за да прегледате новите съобщения - - Парола на архива Архивите ще бъдат запазени на външно хранилище и криптирани с паролата задена по-долу. Трябва да използвате същата парола за въстановяване от архива. Записах тази парола. Без нея, няма да мога да възстановя архива. diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index b7d73581c..36880710f 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -322,34 +322,6 @@ এই ডিভাইস প্লে সার্ভিস সমর্থন করে না। সিস্টেম ব্যাটারি অপ্টিমাইজেশন অক্ষম করতে আলতো চাপুন, যা নিষ্ক্রিয় অবস্থায় বার্তা পুনরুদ্ধার থেকে Signal -কে বাধা দেয়। এর সাথে শেয়ার করুন - - - - - - - - - - - - - - - - - - - - - - - - - - - - একটি বার্তা আনয়ন করা হচ্ছে… @@ -1504,8 +1476,6 @@ Signal ডাটাবেস ডাটাবেস স্থানান্তরিত হচ্ছে নতুন লক্ করা বার্তা মুলতুবি থাকা বার্তাগুলি দেখার জন্য লক্ খুলুন - - পাসফ্রেজ ব্যাকঅাপ করুন ব্যাকআপগুলি বাহ্যিক স্টোরেজে সংরক্ষণ করা হবে এবং নীচের পাসফ্রেজের সাহায্যে এনক্রিপ্ট করা হবে। ব্যাকঅাপ পুনরুদ্ধার করতে আপনার অবশ্যই এই পাসফ্রেজ থাকতে হবে। আমি এই পাসফ্রেজ লিখে রেখেছি। এটি ছাড়া, আমি একটি ব্যাকঅাপ পুনরুদ্ধার করতে পারব না। diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 2b13e2102..d3a149766 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -334,34 +334,6 @@ Ovaj uređaj ne podržava Play Services. Pritisnite ovdje kako biste onemogućili sistemske optimizacije korištenja baterije koje sprečavaju Signal da prima poruke dok je aplikacija zatvorena. Dijeli sa - - - - - - - - - - - - - - - - - - - - - - - - - - - - Primam poruku… @@ -1492,8 +1464,6 @@ Prenosim Signalovu bazu podataka Nova zaključana poruka Otključajte kako biste vidjeli poruke na čekanju - - Lozinka za rezervne kopije Rezervne kopije bit će pohranjene u memoriju uređaja i šifrirane lozinkom navedenom dolje. Morate znati lozinku da biste mogli vratiti podatke iz rezervne kopije. Zapisao/la sam ovu lozinku. Bez nje neću moći vratiti podatke iz rezervne kopije. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 7c052b2f8..27e700e02 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -24,17 +24,17 @@ Això reduirà immediatament totes les converses als %d missatges més recents. Suprimeix - Voleu inhabilitar la contrasenya? + Voleu desactivar la contrasenya? El Signal i les notificacions es desblocaran permanentment. - Inhabilita + Desactiva Cancel·lació del registre Es cancel·la el registre per als missatges i les trucades del Signal… - Voleu inhabilitar els missatges i les trucades del Signal? - Inhabiliteu els missatges i les trucades del Signal cancel·lant el registre del servidor. Haureu de tornar a registrar el número de telèfon per usar-los de nou. + Voleu desactivarr els missatges i les trucades del Signal? + Desactiveu els missatges i les trucades del Signal cancel·lant el registre del servidor. Haureu de tornar a registrar el número de telèfon per usar-los de nou. S\'ha produït un error en connectar amb el servidor. - SMS habilitats + SMS activats Toqueu per canviar l\'aplicació d\'SMS predeterminada - SMS inhabilitats + SMS desactivats Toqueu perquè el Signal sigui l\'aplicació d\'SMS predeterminada actiu Actiu @@ -65,10 +65,10 @@ Permet-hi l\'accés No s\'ha trobat cap aplicació compatible. - El Signal necessita el permís de l\'emmagatzematge per tal d\'adjuntar fotografies, vídeos o àudio, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi l\'emmagatzematge. - El Signal necessita el permís de l\'aplicació dels contactes per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi els contactes. - El Signal necessita el permís de la ubicació per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi la ubicació. - El Signal necessita el permís de la càmera per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi la càmera. + El Signal necessita el permís de l\'emmagatzematge per tal d\'adjuntar fotografies, vídeos o àudio, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi l\'emmagatzematge. + El Signal necessita el permís de l\'aplicació dels contactes per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi els contactes. + El Signal necessita el permís de la ubicació per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi la ubicació. + El Signal necessita el permís de la càmera per tal d\'adjuntar-ne informació, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi la càmera. Es penja el contingut… Es comprimeix el vídeo… @@ -189,14 +189,14 @@ No s\'ha pogut enregistrar l\'àudio. No hi ha cap aplicació que pugui obrir aquest enllaç. Per enviar missatges d\'àudio, permeteu que el Signal tingui accés al micròfon. - El Signal necessita el permís del micròfon per tal d\'enviar missatges d\'àudio, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi el micròfon. + El Signal necessita el permís del micròfon per tal d\'enviar missatges d\'àudio, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi el micròfon. Per trucar a %s, el Signal necessita accés al micròfon i a la càmera. - El Signal necessita el permís del micròfon i de la càmera per tal de trucar a %s, però s\'han denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi el micròfon i la càmera. + El Signal necessita el permís del micròfon i de la càmera per tal de trucar a %s, però s\'han denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi el micròfon i la càmera. Per captar fotografies i vídeos, permeteu que el Signal tingui accés a la càmera. - El Signal necessita el permís de la càmera per tal de fer fotografies i vídeos, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi la càmera. + El Signal necessita el permís de la càmera per tal de fer fotografies i vídeos, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi la càmera. El Signal necessita el permís de la càmera per fer fotografies i vídeos. - Habiliteu el permís del micròfon per capturar vídeos amb so. - El Signal necessita el permís del micròfon per gravar vídeos, però s\'han denegat. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi el micròfon i la càmera. + Activeu el permís del micròfon per capturar vídeos amb so. + El Signal necessita el permís del micròfon per gravar vídeos, però s\'han denegat. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi el micròfon i la càmera. El Signal necessita el permís del telèfon per gravar vídeos. %1$s %2$s El Signal no pot enviar missatges SMS / MMS perquè no és la vostra aplicació d\'SMS predeterminada. Voleu canviar-ho a la configuració d\'Android? @@ -343,43 +343,15 @@ Fitxer sense nom Optimitza per a la no presència dels Play Services - El dispositiu no és compatible amb Play Services. Toqueu per inhabilitar les optimitzacions de la bateria que impedeixen que el Signal rebi missatges quan estigui inactiu. + El dispositiu no és compatible amb Play Services. Toqueu per desactivar les optimitzacions de la bateria que impedeixen que el Signal rebi missatges quan estigui inactiu. Comparteix amb Els adjunts múltiples només s\'admeten per a imatges i vídeos. - - - - - - - - - - - - - - - - - - - - - - - - - - - - Es recupera un missatge… La comunicació del Signal falla contínuament! - El Signal no s\'ha pogut registrar al Google Play Services. Els missatges i les trucades del Signal s\'han inhabilitat, registreu-vos de nou a Configuració > Avançada. + El Signal no s\'ha pogut registrar al Google Play Services. Els missatges i les trucades del Signal s\'han desactivat, registreu-vos de nou a Configuració > Avançada. S\'ha produït un error en recuperar el GIF a resolució completa @@ -391,7 +363,7 @@ Nom del grup Grup de MMS nou Heu seleccionat un contacte que no pot gestionar els grups del Signal, per tant aquest grup serà per MMS. - No esteu registrat per als missatges i les trucades del Signal; per tant, els grups del Signal estan inhabilitats. Torneu a registrar-vos a Configuració > Avançada.l + No esteu registrat per als missatges i les trucades del Signal; per tant, els grups del Signal estan desactivats. Torneu a registrar-vos a Configuració > Avançada.l Cal almenys una persona al grup. Un dels membres del grup té un número que no es pot llegir correctament. Corregiu-ho o suprimiu aquest contacte i torneu a provar-ho. Avatar del grup @@ -444,7 +416,7 @@ El vostre missatge Signal - S\'ha habilitat la connexió en segon pla + S\'ha activat la connexió en segon pla S\'ha produït un error en llegir la configuració d\'MMS de l\'operador @@ -520,7 +492,7 @@ Missatge Seleccioneu destinataris El Signal necessita accés als contactes per tal de mostrar-los. - El Signal necessita el permís de l\'aplicació de contactes per tal de mostrar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi els contactes. + El Signal necessita el permís de l\'aplicació de contactes per tal de mostrar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi els contactes. No podeu compartir més de %d elements. No podeu compartir més de %d elements. @@ -541,8 +513,8 @@ Heu trucat a %s Trucada perduda de %s %s és al Signal! - Heu inhabilitat els missatges efímers. - %1$s ha inhabilitat els missatges efímers. + Heu desactivat els missatges efímers. + %1$s ha desactivat els missatges efímers. Heu establert el temporitzador dels missatges efímers a %1$s. %1$s ha establert el temporitzador dels missatges efímers a %2$s. El número de seguretat amb %s ha canviat. @@ -566,6 +538,10 @@ %1$d membre %1$d membres + + %1$d membre (+%2$d convidat/s) + %1$d membres (+%2$d convidat/s) + %d més %d més @@ -592,7 +568,7 @@ Això no és un codi QR amb enllaç vàlid. Voleu enllaçar un dispositiu de Signal? Sembla que esteu provant d\'enllaçar un dispositiu de Signal amb un escàner extern. Per seguretat, torneu a escanejar el codi dins del Signal. - El Signal necessita el permís de la càmera per tal d\'escanejar un codi QR, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi la càmera. + El Signal necessita el permís de la càmera per tal d\'escanejar un codi QR, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi la càmera. No es pot escanejar un codi QR sense permís de la càmera. Missatges efímers @@ -637,8 +613,8 @@ Els membres existents us podran afegir de nou al grup. Error en abandonar el grup Desbloca - Habilitat - Inhabilitat + Activat + Desactivat Disponible una vegada s\'ha enviat o rebut un missatge. Grup sense nom @@ -777,7 +753,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Vídeo d\'una sola visualització Contingut d\'una sola visualització %s és al Signal! - Missatges efímers inhabilitats + Missatges efímers desactivats El temps del missatge efímer s\'ha establit a %s El número de seguretat ha canviat El número de seguretat amb %s ha canviat. @@ -824,7 +800,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no El nostre número de seguretat: Sembla que no teniu cap aplicació on compartir-ho. No s\'ha trobat cap número de seguretat al porta-retalls - El Signal necessita el permís de la càmera per tal d\'escanejar un codi QR, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi la càmera. + El Signal necessita el permís de la càmera per tal d\'escanejar un codi QR, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi la càmera. No es pot escanejar un codi QR sense permís de la càmera. @@ -851,7 +827,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Jo Tipus de fitxer no compatible Esborrany - El Signal necessita el permís de l\'emmagatzematge per tal de desar en un emmagatzematge extern, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi l\'emmagatzematge. + El Signal necessita el permís de l\'emmagatzematge per tal de desar en un emmagatzematge extern, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi l\'emmagatzematge. No es pot desar en un emmagatzematge extern sense permís. Voleu suprimir el missatge? Aquest missatge se suprimirà permanentment. @@ -927,7 +903,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no S\'ha produït un error en reproduir el vídeo Per respondre una trucada de %s, permeteu que el Signal tingui accés al micròfon. - El Signal necessita el permís del micròfon i de la càmera per tal de fer o rebre trucades, però s\'han denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi el micròfon i la càmera. + El Signal necessita el permís del micròfon i de la càmera per tal de fer o rebre trucades, però s\'han denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi el micròfon i la càmera. El número de seguretat de la conversa amb %1$s ha canviat. Això pot significar o bé que algú està provant d\'interceptar la comunicació, o bé que %2$s ha reinstal·lat el Signal. Potser voleu verificar el número de seguretat amb aquest contacte. @@ -935,7 +911,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Accepta Acaba la trucada - Toqueu per inhabilitar el vídeo + Toqueu per activar el vídeo Foto del contacte Altaveu @@ -978,7 +954,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Foto del contacte - El Signal necessita el permís de l\'aplicació dels contactes per tal de mostrar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi els contactes. + El Signal necessita el permís de l\'aplicació dels contactes per tal de mostrar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi els contactes. Error en recuperar els contactes. Comproveu la connexió de xarxa. No s\'ha trobat el nom d\'usuari. %1$s no és un usuari del Signal. Si us plau, comproveu-ne el nom i torneu-ho a provar. @@ -1001,7 +977,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Desplega la pestanya d\'adjunts de càmera ràpida Enregistra i envia un àudio adjunt Bloqueja la gravació d\'un adjunt d\'àudio - Habilita el Signal per als SMS + Activa el Signal per als SMS Llisqueu per cancel·lar-ho Cancel·la @@ -1043,7 +1019,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no continua Aquí hi ha les confirmacions de lectura Opcionalment vegeu i compartiu quan s\'han llegit els missatges - Inhabilita les confirmacions de lectura + Activa les confirmacions de lectura Inactiu @@ -1243,8 +1219,8 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Silencia-ho durant 7 dies Silencia-ho durant 1 any Configuració predeterminada - Habilitat - Inhabilitat + Activat + Desactivat Nom i missatge Només el nom Sense nom ni missatge @@ -1278,7 +1254,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Trieu els contactes de la llista de contactes. Canvia la contrasenya Canvia la contrasenya - Habilita la contrasenya del blocatge de la pantalla + Activa la contrasenya del blocatge de la pantalla Bloca la pantalla i les notificacions amb una contrasenya Seguretat de la pantalla Bloca les captures de pantalla a les llistes de recents i dins de l\'aplicació @@ -1342,13 +1318,13 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Missatges i trucades privades gratuïtes per als usuaris del Signal Envia un registre de depuració Compatibilitat «Trucada per WiFi» - Habiliteu-ho si el dispositiu fa servir SMS / MMS per WiFi (feu-ho només si la «Trucada per WiFi» està habilitada al dispositiu) + Activeu-ho si el dispositiu fa servir SMS / MMS per WiFi (feu-ho només si la «Trucada per WiFi» està activada al dispositiu) Teclat d\'incògnit Confirmació de lectura - Si les confirmacions de recepció estan inhabilitades, no podreu veure les dels altres. + Si les confirmacions de recepció estan desactivbades, no podreu veure les dels altres. Indicadors de tecleig - Si els indicadors de tecleig estan inhabilitats, no podreu veure els indicadors de tecleig dels altres. - Demana el teclat per inhabilitar l\'aprenentatge personalitzat + Si els indicadors de tecleig estan desactivats, no podreu veure els indicadors de tecleig dels altres. + Demana el teclat per desactivar l\'aprenentatge personalitzat Contactes blocats En usar dades mòbils En usar Wi-Fi @@ -1362,8 +1338,8 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Àudio Revisa l\'emmagatzematge Usa els emoji del sistema - Inhabilita els emojis inclosos al Signal - Retransmet totes les trucades a través del servidor del Signal per evitar revelar l\'adreça IP al contacte. Habilitar-ho reduirà la qualitat de la trucada. + Desactiva els emojis inclosos al Signal + Retransmet totes les trucades a través del servidor del Signal per evitar revelar l\'adreça IP al contacte. Activar-ho reduirà la qualitat de la trucada. Retransmet sempre les trucades Accés de l\'aplicació Comunicació @@ -1383,7 +1359,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Indicadors de pantalla Mostra una icona d\'estat si seleccioneu «Detalls del missatge» als missatges que s\'han entregat usant el remitent segellat. Permet-ho per a tothom - Habilita el remitent segellat per a missatges rebuts de persones que no teniu als contactes o de persones amb qui no heu compartit el vostre perfil. + Activa el remitent segellat per a missatges rebuts de persones que no teniu als contactes o de persones amb qui no heu compartit el vostre perfil. Més informació @@ -1474,7 +1450,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Premeu per establir el Signal com a aplicació predeterminada per a SMS. Importació dels SMS del sistema Premeu per copiar els missatges SMS del telèfon a la base de dades encriptada del Signal. - Habilita els missatges i les trucades del Signal + Activa els missatges i les trucades del Signal Actualitza l\'experiència comunicativa. Convida al Signal Eleva la conversa amb %1$s al nivell següent. @@ -1623,16 +1599,14 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Es carrega… Es connecta… Cal permís - El Signal necessita el permís d\'SMS per tal d\'enviar missatges SMS, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi l\'SMS. + El Signal necessita el permís d\'SMS per tal d\'enviar missatges SMS, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi l\'SMS. Continua Ara no - El Signal necessita el permís de l\'aplicació dels contactes per tal de cercar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi els contactes. - HABILITA ELS MISSATGES DE SIGNAL + El Signal necessita el permís de l\'aplicació dels contactes per tal de cercar-ne els vostres, però s\'ha denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi els contactes. + ACTIVA ELS MISSATGES DEL SIGNAL Migració de la base de dades del Signal Missatge blocat nou Desbloqueu-ho per veure els missatges pendents - - Contrasenya de la còpia de seguretat Les còpies de seguretat es desaran a l\'emmagatzematge extern i s\'encriptaran amb la contrasenya següent. Heu de tenir aquesta contrasenya per tal de restaurar una còpia de seguretat. He anotat aquesta contrasenya. Sense, no podré restaurar cap còpia de seguretat. @@ -1654,18 +1628,18 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Restaureu els missatges i els continguts multimèdia des d\'una còpia de seguretat local. Si no ho feu ara, no ho podreu fer més tard. Mida de la còpia de seguretat: %s Marca horària de la còpia de seguretat: %s - Voleu habilitar les còpies de seguretat locals? - Habilita les còpies de seguretat + Voleu activar les còpies de seguretat locals? + Activa les còpies de seguretat Si us plau, reconeixeu que ho heu entès marcant la casella de confirmació. Voleu suprimir les còpies de seguretat? - Voleu inhabilitar i suprimir totes les còpies de seguretat locals? + Voleu desactivarr i suprimir totes les còpies de seguretat locals? Suprimeix les còpies de seguretat Copiat al porta-retalls Escriviu la contrasenya de còpia de seguretat per verificar-la. Verificació Heu escrit correctament la contrasenya de còpia de seguretat. La contrasenya no és correcta. - El Signal necessita el permís de l\'emmagatzematge extern per tal de crear còpies de seguretat. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i habiliteu-hi l\'emmagatzematge. + El Signal necessita el permís de l\'emmagatzematge extern per tal de crear còpies de seguretat. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi l\'emmagatzematge. Darrera còpia de seguretat: %s En curs Es crea una còpia de seguretat… @@ -1695,7 +1669,7 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Confirmeu-lo Escriviu el PIN de bloqueig del registre Escriviu el PIN - Habiliteu un PIN de bloqueig de registre que es demanarà per tornar a registrar aquest número de telèfon al Signal. + Activeu un PIN de bloqueig de registre que es demanarà per tornar a registrar aquest número de telèfon al Signal. PIN de bloqueig del registre Bloqueig del registre Heu d\'escriure el PIN de bloqueig del registre @@ -1707,18 +1681,18 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Oh, no! El registre d\'aquest número de telèfon serà possible sense el PIN de bloqueig del registre 7 dies després des de l\'últim cop que el número es va usar al Signal. Us manquen %d dies. PIN de bloqueig del registre - Aquest número de telèfon té el bloqueig de registre habilitat. Si us plau, escriviu el PIN de bloqueig del registre. - El bloqueig de registre està habilitat per al vostre número de telèfon. Per a ajudar-vos a memoritzar el PIN de bloqueig de registre, el Signal us demanarà periòdicament que el confirmeu. + Aquest número de telèfon té el bloqueig de registre activat. Si us plau, escriviu el PIN de bloqueig del registre. + El bloqueig de registre està activat per al vostre número de telèfon. Per a ajudar-vos a memoritzar el PIN de bloqueig de registre, el Signal us demanarà periòdicament que el confirmeu. He oblidat el PIN. PIN oblidat? - El bloqueig de registre ajuda a protegir el número de telèfon d\'intents de registre no autoritzats. Aquesta funció es pot inhabilitar en qualsevol moment a la configuració de privadesa del Signal. + El bloqueig de registre ajuda a protegir el número de telèfon d\'intents de registre no autoritzats. Aquesta funció es pot desactivar en qualsevol moment a la configuració de privadesa del Signal. Bloqueig del registre - Habilita + Activa El PIN de bloqueig del registre ha de tenir com a mínim %d dígits. Els dos PIN que heu introduït no coincideixen. Error en connectar al servei - Voleu inhabilitar el PIN de bloqueig del registre? - Inhabilita + Voleu desactivar el PIN de bloqueig del registre? + Desactiva PIN incorrecte Us resten %d intents. Còpies de seguretat diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b0aa2e3c7..03827b1f1 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -375,34 +375,6 @@ Sdílet s Vícero příloh je podporováno pouze pro obrázky a videa - - - - - - - - - - - - - - - - - - - - - - - - - - - - Načítám zprávu… @@ -1720,8 +1692,6 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Přesouvám Signal databázi Nová uzamčená zpráva Pro zobrazení čekajících zpráv odemkněte - - Heslo pro zálohy Zálohy budou uloženy na externí úložiště a zašifrovány níže uvedeným heslem. Toto heslo musíte znát, abyste mohli zálohu obnovit. Heslo jsem si zapsal. Bez něj nebudu schopen obnovit data ze zálohy. diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 5ee5c4cbe..965e45e4f 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -377,34 +377,6 @@ Send neges heb ei ddiogelu? Rhannu â Dim ond ar gyfer delweddau a fideos y mae atodiadau lluosog yn cael eu cynnal - - - - - - - - - - - - - - - - - - - - - - - - - - - - Estyn neges… @@ -612,6 +584,12 @@ Send neges heb ei ddiogelu? %1$d aelod %1$d aelod + + %1$daelod (+ %2$d wedi eu gwahodd) + %1$daelod (+ %2$d wedi eu gwahodd) + %1$d aelod (+ %2$dwedi eu gwahodd) + %1$d aelod (+ %2$d wedi eu gwahodd) + %d arall %d RLL @@ -1723,8 +1701,6 @@ Send neges heb ei ddiogelu? Mudo cronfa ddata Signal Neges wedi\'i gloi newydd Datgloi i weld negeseuon sy\'n aros - - Cyfrinymadrodd cadw wrth gefn Bydd copïau wrth gefn yn cael eu cadw i storfa allanol ac wedi\'u hamgryptio gyda\'r cyfrinymadrodd isod. Rhaid i chi fod â\'r cyfrinymadrodd hwn er mwyn adfer y copi wrth gefn. Rwy wedi ysgrifennu\'r cyfrinymadrodd hwn i lawr. Hebddo, fydda i ddim yn gallu adfer copi wrth gefn. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a4feec0ef..4eb461b90 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -348,34 +348,6 @@ Del med Det er kun muligt at vedhæfte flere billeder og videoer - - - - - - - - - - - - - - - - - - - - - - - - - - - - Modtager besked… @@ -1601,8 +1573,6 @@ Modtog en nøgle besked, for en ugyldig protokol-version. Migrerer Signals database Ny låst besked Lås op for at læse indkommende beskeder - - Backup kodeord Backups vil blive gemt til ekstern placering og krypteret med koden herunder. Du skal kende denne kode for at kunne genskabe fra backup\'en Jeg har nedskrevet koden. Uden den vil jeg ikke have mulighed for at genskabe fra backup diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1ce33c72c..0dd8b711e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -347,34 +347,6 @@ Teilen mit Mehrere Anhänge werden nur für Bilder und Videos unterstützt - - - - - - - - - - - - - - - - - - - - - - - - - - - - Nachricht wird abgerufen … @@ -402,7 +374,7 @@ Gruppendetails werden geladen … Du bist bereits in der Gruppe. - Deinen Profilnamen und dein Profilfoto mit dieser Gruppe teilen? + Profilname und -foto mit dieser Gruppe teilen? Möchtest du deinen Profilnamen und dein Profilfoto für alle derzeitigen und zukünftigen Mitglieder dieser Gruppe sichtbar machen? Sichtbar machen @@ -566,6 +538,10 @@ %1$d Mitglied %1$d Mitglieder + + %1$d Mitglied (+%2$d eingeladen) + %1$d Mitglieder (+%2$d eingeladen) + %d anderer %d andere @@ -1126,7 +1102,7 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangen Gruppenname lautet jetzt »%1$s«. - Deinen Profilnamen und dein Profilfoto für diese Gruppe sichtbar machen? + Antippen, um deinen Profilnamen und dein Profilfoto für diese Gruppe sichtbar zu machen Entsperren @@ -1201,7 +1177,7 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenFoto entfernen Unterhaltungsanfragen - Nutzer können nun wählen, ob sie eine neue Unterhaltung zulassen möchten. Profilnamen informieren darüber, wer geschrieben hat. + Du kannst nun wählen, ob du eine neue Unterhaltung zulassen möchtest. Profilnamen lassen dich wissen, wer dir schreibt. Profilname hinzufügen Neu: Unterhaltungsanfragen Name hinzufügen @@ -1626,8 +1602,6 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenSignal-Datenbank wird migriert … Neue gesperrte Nachricht Entsperren, um ausstehende Nachrichten anzuzeigen - - Sicherungspassphrase Datensicherungen werden im Gerätespeicher gespeichert und mit der unten genannten Passphrase verschlüsselt. Die Passphrase ist notwendig, um die Sicherung später wiederherzustellen. Ich habe mir diese Passphrase notiert. Ohne sie können keine Sicherungen wiederhergestellt werden. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a1c75ecf5..5f0b25585 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -326,34 +326,6 @@ Διαμοιρασμός με Τα πολλαπλά συνημμένα υποστηρίζονται μόνο για εικόνες και βίντεο - - - - - - - - - - - - - - - - - - - - - - - - - - - - Λαμβάνουμε ένα μήνυμα… @@ -1573,8 +1545,6 @@ Μετεγκατάσταση της βάσης δεδομένων του Signal Νέο κλειδωμένο μήνυμα Ξεκλειδωστε για να δείτε τα εκκρεμή μηνύματα - - Συνθηματικό αντίγραφου ασφαλείας Τα αντίγραφα ασφαλείας αποθηκεύονται στην εξωτερική μνήμη και κρυπτογραφούνται με το παρακάτω συνθηματικό. Πρέπει να γνωρίζετε αυτό το συνθηματικό για να επανακτήσετε το αντίγραφο ασφαλείας. Σημείωσα σε χαρτί αυτό το συνθηματικό. Χωρίς αυτό, δε θα μπορέσω να επανακτήσω το αντίγραφο ασφαλείας. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 651dcbc9e..b599cc6e1 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -347,34 +347,6 @@ Kunhavigi kun Pluraj kunsendaĵoj nur eblas kun bildoj kaj videaĵoj - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ricevo de mesaĝo… @@ -1636,8 +1608,6 @@ Ricevis mesaĝon pri interŝanĝo de ŝlosiloj por nevalida protokola versio. Transigo de la Signal-datumbazo Nova ŝlosita mesaĝo Malŝlosi por vidi pritraktotajn mesaĝojn - - Savkopii pasfrazon Savkopioj konserviĝos al ekstera konservejo kaj ĉifriĝos per la suba pasfrazo. Vi devos havi tiun ĉi pasfrazon por restaŭri savkopion. Mi skribis tiun ĉi pasfrazon. Sen ĝi, mi ne povos restaŭri savkopion. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3d2c3833b..93bc15008 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -347,34 +347,6 @@ Compartir con Sólo fotos y vídeos se pueden enviar como múltiples adjuntos - - - - - - - - - - - - - - - - - - - - - - - - - - - - Recuperando mensaje … @@ -566,6 +538,10 @@ %1$d participante %1$d participantes + + %1$d participante (+%2$d pendiente) + %1$d participantes (+%2$d pendientes) + %d otro %d otros @@ -1638,8 +1614,6 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Migrar base de datos de Signal Nuevo mensaje bloqueado Desbloquear para ver mensajes pendientes - - Clave de la copia de seguridad Las copias de seguridad se guardarán en una unidad de almacenamiento externo y se cifrarán con la clave de debajo. Necesitas esta clave para poder restaurar una copia de seguridad. He anotado esta clave. Sé que sin ella, no podré restaurar la copia de seguridad. diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 2cb25a47c..03706c3f7 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -326,34 +326,6 @@ See seade ei toeta Play teenuseid. Koputa, et keelata süsteemi aku optimeerimine, kuna see ei lase Signalil sõnumeid hankida, kui seade on ebaaktiivne. Jaga inimesega - - - - - - - - - - - - - - - - - - - - - - - - - - - - Sõnumi hankimine… @@ -1535,8 +1507,6 @@ Migreerin Signali andmebaasi Uus lukustatud sõnum Ava lukk ootel sõnumite vaatamiseks - - Varunduse salaväljend Varundused salvestatakse välismällu ja krüptitakse valitud salaväljendiga. Sul peab see salaväljend olemas olema, et taastada antud varundust. Ma olen salaväljendi üles kirjutanud. Ilma selleta ei saa ma varundust taastada. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d70ec5bf4..fe8cfb275 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -344,34 +344,6 @@ Norekin konpartitu Eranskin anitzak bakarrik irudi eta bideoetan onartzen dira - - - - - - - - - - - - - - - - - - - - - - - - - - - - Mezu bat berreskuratzen… @@ -1621,8 +1593,6 @@ Inportatu \'SMSBackup and Restorekin\' bateragarria den enkriptatu gabeko babesk Signal-en datu-basea migratzen Blokeatutako mezu berria Desblokeatu begiratu gabeko mezuak ikusteko - - Babeskopiaren pasaesaldia Babeskopiak kanpoko memoria batean gordeko dira beheko pasaesaldiarekin enkriptatuta. Pasaesaldi hau eduki behar duzu babeskopia bat berreskuratzeko. Pasaesaldia gorde dut. Hori gabe, ezin izango dut babeskopia bat berreskuratu. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 9ba3ff834..e8377d997 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -347,34 +347,6 @@ اشتراک گذاری با انتخاب چندگانهٔ پیوست‌ها فقط برای عکس‌ها و ویدئوها پشتیبانی می‌شود. - - - - - - - - - - - - - - - - - - - - - - - - - - - - در حال بازیابی یک پیام… @@ -628,14 +600,14 @@ مسدود کردن این مخاطب؟ شما ديگر هیچ تماس و پيامی از این مخاطب دريافت نخواهید کرد. - مسدود سازی و ترک این گروه؟ - مسدود سازی این گروه؟ + مسدودسازی و ترک این گروه؟ + مسدودسازی این گروه؟ شما دیگر از این گروه پیام یا به‌روزرسانی دریافت نخواهید کرد. مسدود سازی رفع مسدودیت این مخاطب؟ - شما مجددا قادر به دریافت پیام و تماس از این مخاطب خواهید بود. + شما دوباره قادر به دریافت پیام و تماس از این مخاطب خواهید بود. رفع مسدودیت این گروه؟ - اعضا موجود قادر خواهند بود مجددا شما را به گروه اضافه کنند. + اعضا فعلی قادر خواهند بود دوباره شما را به گروه اضافه کنند. خطا در ترک گروه رفع مسدودیت فعال @@ -653,7 +625,7 @@ گیرنده در دسترس نیست خرابی شبکه! شماره ثبت نشده است! - شماره ای که گرفتید از تماس امن پشتیبانی نمی کند! + شماره‌ای که گرفتید از تماس امن پشتیبانی نمی کند! فهمیدم کشور خود را انتخاب کنید @@ -683,7 +655,7 @@ شما %d قدم از ارسال گزارش اشکال‌زدایی فاصله دارید. ما باید تایید کنیم که شما یک انسان هستید. - عدم موفقیت در تایید کپچا + عدم موفقیت در تأیید کپچا بعدی ادامه ادامه (%d کوشش باقی مانده است) @@ -782,8 +754,8 @@ %s به Signal پیوست! پیام های ناپدید شونده غیر فعال شدند زمان پیام ناپدید شونده روی %s تنظیم شد - شماره امنیتی تغییر یافت - شماره امنیتی شما با %s تغییر یافت. + شمارهٔ امنیتی تغییر یافت + شمارهٔ امنیتی شما با %s تغییر یافت. شما تایید شده علامت زدید شما تایید نشده علامت زدید پیام قابل پردازش نمی باشد @@ -813,9 +785,9 @@ نام کاربری با موفقیت حذف شد. خطای شبکه رخ داد. این نام کاربری گرفته شده است. - این نام کاربری در دسترس می باشد. + این نام کاربری در دسترس است. نام‌های کاربری فقط می توانند شامل a-Z ،0-9 و _ باشند. - نام‌ های کاربری نمی توانند با یک عدد آغاز شوند. + نام‌های کاربری نمی‌توانند با عدد آغاز شوند. نام کاربری نامعتبر است. نام‌های کاربری باید بین %1$d و %2$d کاراکتر باشند. دیگر کاربران Signal می‌توانند از طریق نام کاربری و بدون نیاز به دانستن شمارهٔ همراه شما درخواستِ پیام ارسال کنند. انتخاب نام کاربری اختیاری است. @@ -1063,7 +1035,7 @@ %d ساعت %d ساعت - %dس + %d ساعت %d روز %d روز @@ -1265,7 +1237,7 @@ %dس - %dس + %d ساعت پیامک و فراپیام @@ -1408,7 +1380,7 @@ انتخاب چندگانه - ذخیره پیوست + ذخیره‌سازی پیوست پیام‌های ناپدید شونده @@ -1418,8 +1390,8 @@ حذف انتخاب شده(ها) انتخاب همه - بایگانی انتخاب شده - خارج کردن از بایگانی انتخاب شده (ها) + بایگانی موارد انتخاب شده + خارج کردن موارد انتخاب شده از بایگانی میانبر تنظیمات جستجو @@ -1431,7 +1403,7 @@ مکالمه جدید باز کردن دوربین - صندوق ورودی خود را پر کنید. اینکار را با پیام دادن به یک دوست می توانید شروع کنید. + صندوق ورودی خود را پر کنید. اینکار را با پیام دادن به یک دوست می‌توانید شروع کنید. تنظیم مجدد نشست امن @@ -1480,9 +1452,9 @@ فعال‌سازی پیام‌ها و تماس‌های Signal تجربهٔ ارتباطی خود را ارتقا دهید. دعوت به Signal - مکالمه خود با %1$s را به مرحله بعد ببرید. + مکالمهٔ خود با %1$s را به مرحله بعد ببرید. دوستان خود را دعوت کنید! - هر چه دوستان بیشتری از Signal استفاده کنند، بهتر خواهد شد. + هر چه دوستان بیشتری از Signal استفاده کنند، بهتر می‌شود. Signal با مشکلات فنی روبرو است. ما سخت در تلاش برای بازگردانی هرچه سریع‌تر سرویس هستیم. آخرین قابلیت‌های Signal روی این نسخه از اندروید کار نخواهند کرد. لطفاً این دستگاه را ارتقا دهید تا به‌روزرسانی‌های بعدی Signal را دریافت کنید. %%%1$d @@ -1563,7 +1535,7 @@ پین خود را ایجاد کنید پین Signal خود را وارد کنید - برای کمک به شما در به خاطر سپردن پین خود، ما به صورت دوره ای از شما خواهیم خواست تا آن را وارد کنید. با گذر زمان آن را کمتر از شما خواهیم خواست. + برای کمک به شما در به‌خاطرسپاری پین، آن را به صورت دوره‌ای از شما می‌پرسیم. با گذر زمان این‌ کار را کمتر انجام خواهیم داد. پرش ارسال پین را فراموش کرده اید؟ @@ -1634,8 +1606,6 @@ در حال انتقال پایگاه داده Signal پیام قفل شده جدید جهت دیدن پیام های در حال انتظار قفل را باز کنید - - گذرواژهٔ پشتیبان‌گیری پشتیبان‌ها روی یک حافظهٔ خارجی ذخیره و با گذرواژهٔ زیر رمزنگاری خواهند شد. شما برای بازگردانی پشتیبان خود باید گذرواژهٔ زیر را داشته باشید. من این گذرواژه را یادداشت کرده‌ام. بدون آن، قادر به بازگردانی پشتیبان خود نخواهم بود. @@ -1659,7 +1629,7 @@ برچسب زمان پشتیبان: %s فعال‌سازی پشتیبان‌های محلی؟ فعال‌سازی پشتیبان‌ها - لطفاً فهم خود را با علامت زدن جعبه نشان دهید. + لطفاً تأیید خود را با علامت زدن جعبه نشان دهید. حذف پشتیبان‌ها؟ غیرفعال کردن و پاک کردن تمامی پشتیبان‌های محلی؟ حذف پشتیبان‌ها diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3c3553395..708574325 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -347,34 +347,6 @@ Jaa Useiden tiedostojen liittämistä tuetaan vain kuvilla ja videoilla - - - - - - - - - - - - - - - - - - - - - - - - - - - - Haetaan viestiä… @@ -1631,8 +1603,6 @@ Vastaanotetiin avaintenvaihtoviesti, joka kuuluu väärälle protokollaversiolle Signalin tietokantaa siirretään Uusi lukittu viesti Avaa nähdäksesi odottavat viestit - - Varmuuskopion salalause Varmuuskopiot tallennetaan laitteesi tallennustilaan ja ne salataan allaolevalla salalauseella. Ilman tätä salalausetta et voi palauttaa varmuuskopiota. Olen kirjoittanut ylös tämän salalauseen. Ilman sitä, en pysty palauttamaan varmuuskopiota. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a5442c721..375a76a67 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -346,34 +346,6 @@ Partager avec Les fichiers joints multiples ne sont pris en charge que pour les images et les vidéos - - - - - - - - - - - - - - - - - - - - - - - - - - - - Récupération d’un message… @@ -1618,8 +1590,6 @@ Migration de la base de données de Signal Nouveau message verrouillé Déverrouiller pour visualiser les messages en attente - - Phrase de passe de la sauvegarde Les sauvegardes sont enregistrées dans la mémoire externe et chiffrées avec la phrase de passe ci-dessous. Vous devez avoir cette phrase de passe afin de restaurer la sauvegarde. J’ai noté cette phrase de passe. Sans elle, je ne pourrai pas restaurer une sauvegarde. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 25ed1d582..1daede512 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -331,30 +331,6 @@ Este dispositivo non é compatible con Servizos Play. Toca para desactivar as optimizacións da batería do sistema que evitan que Signal recupere mensaxes mentres está inactivo. Compartir con - - - - - - - - - - - - - - - - - - - - - - - - Recuperando unha mensaxe… @@ -1414,8 +1390,6 @@ Migrando a base de datos de Signal Nova mensaxe bloqueada Desbloquear para ver as mensaxes pendentes - - Frase de acceso da copia de seguranza As copias de seguranza gardaranse no almacenamento externo e cifraranse cunha frase de acceso. A devandita frase é necesaria para restaurar unha copia de seguranza. Escribín esta frase de acceso. Sen ela non poderei restaurar unha copia de seguranza. diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 859c65972..b217647c5 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -322,34 +322,6 @@ આ ડિવાઇસ Play Servicesનું સમર્થન કરતું નથી. સિસ્ટમ બેટરી ઓપ્ટિમાઇઝેશંસને અક્ષમ કરવા માટે ટેપ કરો જે Signal ને નિષ્ક્રિય હોવા પર મેસેજ પ્રાપ્ત કરવામાં અટકાવે છે. સાથે શેર કરો - - - - - - - - - - - - - - - - - - - - - - - - - - - - મેસેજ પ્રાપ્ત કરી રહ્યા છીએ… @@ -1499,8 +1471,6 @@ માઈગે્ટ Signal ડેટાબેસ નવો લૉક મેસેજ બાકી મેસેજ જોવા માટે અનલૉક કરો - - બેકઅપ પાસફ્રેઝ બૅકઅપ બાહ્ય સ્ટોરેજ માં સાચવવામાં આવશે અને નીચે પાસફ્રેઝ થી એન્ક્રિપ્ટ થશે. બેકઅપને રિસ્ટોર કરવા માટે તમારી પાસે આ પાસફ્રેઝ હોવો આવશ્યક છે. મેં આ પાસફ્રેઝ લખ્યો છે. તેના વિના, હું બૅકઅપ ને રિસ્ટોર કરવામાં અસમર્થ હોઈશ. diff --git a/app/src/main/res/values-ha/strings.xml b/app/src/main/res/values-ha/strings.xml index 569da79dd..a69b0b2b9 100644 --- a/app/src/main/res/values-ha/strings.xml +++ b/app/src/main/res/values-ha/strings.xml @@ -322,34 +322,6 @@ Wannan na\'urar baya daukan Google Play Services. Taba nan domin kashe fasalin batiri da yake hana Signal karban sakwanni idan ba\'a aiki dashi. Tura wa - - - - - - - - - - - - - - - - - - - - - - - - - - - - Diban sakwanni. @@ -1503,8 +1475,6 @@ Kauracewar shiryayyen bayanain Signal Sabon kulellen sako Bude domin karanta tsayyayen sakonni - - Kalmomin sirrin tsimi Za\'a ajiye tsimi a ajiyar waje kuma za\'a tsaresu da kalmomin sirri da suke kasa. Dole sai ka/kinada kalmomin sirri zaka/ki samu damar yin tsimi ko kuma maida na\'ura tsarin asali. Na rike wannan kalmomin sirri. In ba tare dashi ba, bazan iya yin tsimi ko kuma maida tsarin asali. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index ca25b9f76..d172c4d97 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -322,34 +322,6 @@ यह डिवाइस Play सेवाओं का समर्थन नहीं करता है। सिस्टम बैटरी ऑप्टिमाइज़ेशन को अक्षम करने के लिए टैप करें, जो निष्क्रिय होने पर संदेशों को पुनर्प्राप्त करने से Signal को रोकता है। साझा करें - - - - - - - - - - - - - - - - - - - - - - - - - - - - मेसेज मंगाया जा रहा है़… @@ -1502,8 +1474,6 @@ Signal डेटाबेस माइग्रेट किया जा रहा है नया लॉक मेसेज पड़ा हुए मेसेजों को देखने के लिए अनलॉक करें - - बैकअप पासफ्रेज बैकअप को बाहरी स्टोरेज में सहेजा जाएगा और नीचे पासफ्रेज से एन्क्रिप्ट किया जाएगा। बैकअप को पुनर्स्थापित करने के लिए आपके पास यह पासफ्रेज़ होना चाहिए। मैंने इस पासफ्रेज को लिखा है। इसके बिना, मैं बैकअप को पुनर्स्थापित करने में असमर्थ हूं। diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ccda36b38..656cc486a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -348,34 +348,6 @@ Megosztás… Több csatolmányt használata csak fényképek és videók esetén támogatott. - - - - - - - - - - - - - - - - - - - - - - - - - - - - Üzenet letöltése… @@ -567,6 +539,10 @@ %1$d tag %1$d tag + + %1$d tag (+%2$d meghívott) + %1$d tag (+%2$d meghívott) + %d további %d további @@ -1640,8 +1616,6 @@ Kulcs-csere üzenet érkezett érvénytelen protokoll verzióhoz. Signal adatbázis migrálása Új zárolt üzenet Oldd fel a függő üzenetek megtekintéséhez - - Biztonsági mentés jelmondat A biztonsági mentések külső tárolóra lesznek mentve, és az alábbi jelmondattal lesznek titkosítva. Ezt a jelmondatot kell megadnod a biztonsági mentés visszaállításához. Feljegyeztem ezt a jelmondatot. Enélkül nem leszek képes visszaállítani biztonsági mentést. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 93ef34fef..c4bc29558 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -333,34 +333,6 @@ Berbagi dengan Lampiran jamak hanya berupa gambar dan video - - - - - - - - - - - - - - - - - - - - - - - - - - - - Mengambil pesan… @@ -1595,8 +1567,6 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid. Memindahkan basis data Signal Pesan terkunci baru Buka kunci untuk melihat pesan tertunda - - Frasa sandi cadangan Cadangan akan disimpan ke penyimpanan eksternal dan terenkripsi dengan frasa di bawah. Anda mesti memiliki frasa sandi ini untuk memulihkan cadangan. Saya telah mencatat frasa ini. Tanpa ini, saya tidak dapat memulihkan cadangan. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2457c2d7d..b3217bc19 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -347,34 +347,6 @@ Condividi con Gli allegati multipli sono supportati solo per immagini e video - - - - - - - - - - - - - - - - - - - - - - - - - - - - Recuperando un messaggio… @@ -566,6 +538,10 @@ %1$d membro %1$d membri + + %1$d membro (+%2$d invitato/i) + %1$d membri (+%2$d invitato/i) + %d altro altri %d @@ -1637,8 +1613,6 @@ Migrazione del database di Signal Nuovo messaggio bloccato Sblocca per leggere i messaggi in sospeso - - Passphrase di backup I backup verranno salvati nella memoria esterna e crittografati con la passphrase qui sotto. Sarà necessario avere questa passphrase per ripristinare il backup. Ho scritto questa passphrase. Senza di questa, non sarò in grado di ripristinare un backup. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 0f789bd5d..be46c8707 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -375,34 +375,6 @@ שתף עם צרופות מרובות נתמכות רק עבור תמונות וסרטונים - - - - - - - - - - - - - - - - - - - - - - - - - - - - מאחזר הודעה… @@ -1721,8 +1693,6 @@ מהגר מסד נתונים של Signal הודעה נעולה חדשה בטל נעילה כדי להציג הודעות ממתינות - - גבה משפט־סיסמה גיבויים יישמרו בהתקן חיצוני ויוצפנו עם משפט־הסיסמה למטה. חייב להיות לך משפט־סיסמה זה על מנת לשחזר גיבוי. כתבתי משפט־סיסמה זה. בלעדיו, לא אוכל לשחזר גיבוי. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 71d25034a..5446482b0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -326,34 +326,6 @@ このデバイスは Play Service をサポートしていません。Signal が非アクティブ状態でもメッセージを受信できるよう、タップしてバッテリー最適化機能を無効にしてください。 共有先 - - - - - - - - - - - - - - - - - - - - - - - - - - - - メッセージを取得しています… @@ -1554,8 +1526,6 @@ Signal のデータベースを移行する ロックされた新着メッセージ ロックを解除してメッセージを見る - - バックアップ用パスフレーズ バックアップは外部ストレージに保存され、下のパスフレーズで暗号化されます。バックアップを復元する場合は、このパスフレーズが必要になります。 パスフレーズを書き留めました。パスフレーズがなければ、バックアップは復元できません。 diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index c201fd661..fe9a04a53 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -308,34 +308,6 @@ Pirantos niki mboten ndukung Play Services. Tutul kangge nonaktifaken paningkatan baterai sistem ingkang ngewatesi Signal saking nampi pesen kala mboten aktif. Diunjukaken kangge - - - - - - - - - - - - - - - - - - - - - - - - - - - - Taksih nampi pesen… @@ -1472,8 +1444,6 @@ Taksih mindahi database Signal Pesen ingkang dipunkunci enggal Uculaken kunci kangge ningali pesen-pesen ingkang ketundha - - Tembung sandi serep Serep bakal dipunsimpen ing penyimpenan njawi lan dipunenkripsi ngginakaken tembung sandi ing ngandhap. Sampeyan wajib ngeling tembung sandi kasebut kangge mulihaken serep. Kula sampun nyatet tembung sandi niki. Tanpa tembung sandi kasebut, kula mboten saged mulihaken serep. diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index 9f3341ffc..d00bb453d 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -313,34 +313,6 @@ ឧបករណ៍នេះ មិនគាំទ្រ Play Services។ ចុច ដើម្បីបិទការបង្កើនប្រសិទ្ធិភាពប្រព័ន្ធថ្ម ដែលរារាំង Signal ពីទាញយកសារនានា នៅពេលឧបករណ៍ទុកចោល។ ចែកទៅកាន់ - - - - - - - - - - - - - - - - - - - - - - - - - - - - សារកំពុងទៅដល់… @@ -1527,8 +1499,6 @@ ផ្លាស់ទីទិន្នន័យ Signal សារចាក់សោរថ្មី ដោះសោរដើម្បីមើលសាររង់ចាំ - - បម្រុងទុកឃ្លាសម្ងាត់ ការបម្រុងទុក នឹងត្រូវរក្សាទុកនៅលើអង្គរក្សាទុកខាងក្រៅ និងកូដនីយកម្មជាមួយឃ្លាសម្ងាត់ខាងក្រោម។ អ្នកត្រូវមានឃ្លាសម្ងាត់នេះដើម្បីស្តារការបម្រុងទុកឡើងវិញ។ ខ្ញុំបានកត់ឃ្លាសម្ងាត់ទុកហើយ។ បើគ្មានវា ខ្ញុំនឹងមិនអាចស្តារការបម្រុងទុកបានទេ។ diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 2395b4c89..d9e43749b 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -346,34 +346,6 @@ ಜೊತೆ ಹಂಚಿಕೊಳ್ಳಿ ಬಹು ಲಗತ್ತುಗಳನ್ನು ಕೇವಲ ಚಿತ್ರಗಳು ಮತ್ತು ವೀಡಿಯೋಗಳಿಗೆ ಬೆಂಬಲಿಸಲಾಗಿರುತ್ತದೆ - - - - - - - - - - - - - - - - - - - - - - - - - - - - ಸಂದೇಶವನ್ನು ಮರುಪಡೆಯಲಾಗುತ್ತಿದೆ… @@ -1612,8 +1584,6 @@ Signal ಡೇಟಾಬೇಸ್ ಮೈಗ್ರೇಟ್ ಮಾಡಲಾಗುತ್ತಿದೆ ಲಾಕ್ ಮಾಡಿರುವ ಹೊಸ ಸಂದೇಶ ಬಾಕಿ ಉಳಿದಿರುವ ಸಂದೇಶಗಳನ್ನು ನೋಡಲು ಅನ್ಲಾಕ್ ಮಾಡಿ - - ಬ್ಯಾಕಪ್ ಪಾಸ್‌ಫ್ರೇಸ್ ಬ್ಯಾಕಪ್‌ಗಳನ್ನು ಬಾಹ್ಯ ಸ್ಟೊರೇಜ್‌ಗೆ ಉಳಿಸಲಾಗುತ್ತದೆ ಮತ್ತು ಕೆಳಗಿನ ಪಾಸ್‌ಫ್ರೇಸ್‌ನೊಂದಿಗೆ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಲಾಗುತ್ತದೆ. ಬ್ಯಾಕಪ್ ಅನ್ನು ಪುನಃ ಸ್ಥಾಪಿಸಲು ನೀವು ಈ ಪಾಸ್‌ಫ್ರೇಸ್ ಹೊಂದಿರಬೇಕು. ನಾನು ಈ ಪಾಸ್‌ಫ್ರೇಸ್ ಬರೆದಿದ್ದೇನೆ. ಅದು ಇಲ್ಲದೆ, ನನಗೆ ಬ್ಯಾಕಪ್ ಮರುಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 9de69a50c..362df7ca3 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -333,34 +333,6 @@ 공유자 이미지와 동영상만 여러 개 첨부할 수 있습니다. - - - - - - - - - - - - - - - - - - - - - - - - - - - - 메시지를 받는 중… @@ -1566,8 +1538,6 @@ Signal 데이터베이스 이전 중 새 잠금 메세지 잠금 해제하여 보류 중인 메시지 보기 - - 암호 백업 백업은 외부 저장 공간에 보관되며 암호로 암호화됩니다. 백업에서 복원하려면 다음에 표시되는 암호가 필요합니다. 암호를 적었습니다. 비밀번호를 잊었을 때 백업을 복구할 수 없다는 점에 동의합니다. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 2f2e1ea21..302bd2bb2 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -375,34 +375,6 @@ Bendrinti su Keli priedai yra palaikomi tik paveikslams ir vaizdo įrašams - - - - - - - - - - - - - - - - - - - - - - - - - - - - Atgaunama žinutė… @@ -1721,8 +1693,6 @@ Perkeliama Signal duomenų bazė Nauja užrakinta žinutė Atrakinkite, norėdami matyti laukiančias žinutes - - Atsarginės kopijos slaptafrazė Atsarginės kopijos bus įrašytos į išorinę saugyklą ir bus užšifruotos, naudojant žemiau esančią slaptafrazę. Norėdami atkurti atsarginę kopiją, privalėsite turėti šią slaptafrazę. Aš užsirašiau šią slaptafrazę. Be jos aš negalėsiu atkurti atsarginės kopijos. diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 154b3ed98..714c46d11 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -361,34 +361,6 @@ Dalīties ar Vairāku pielikumu sūtīšana ir atbalstīta tikai attēliem un video - - - - - - - - - - - - - - - - - - - - - - - - - - - - Notiek ziņas ielāde… @@ -1662,8 +1634,6 @@ Saņemts nederīgas protokola versijas atslēgas apmaiņas ziņojums. Tiek migrēta/pārcelta Signāla datubāze Jauna bloķēta ziņa Atbloķējiet, lai redzētu gaidošās ziņas - - Dublikāta (rezerves kopijas) paroles frāze Dublikāti tiks saglabāti ārējā noliktavā un šifrēti, izmantojot tālāk esošo paroles frāzi. Lai atgrieztu/restaurētu no dublikāta (rezerves kopijas), būs nepieciešama šī paroles frāze. Esmu sev pierakstījis šo paroles frāzi. Bez tās nebūs iespējams atgriezt/restaurēt dublikātu (rezerves kopiju). diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 2db9f3532..ff84210d6 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -322,34 +322,6 @@ हा डिव्हाईस प्ले सेवांचे समर्थन करत नाही. निष्क्रिय असल्यावर संदेश पुनर्प्राप्त करताना अवरोधित करणारे सिस्टिम बॅटरी ऑप्टीमाईझेशन अक्षम करण्यासाठी टॅप करा. यासोबत सामायिक करा - - - - - - - - - - - - - - - - - - - - - - - - - - - - संदेश पुनर्प्राप्त करत आहे… @@ -1507,8 +1479,6 @@ Signal डेटाबेस स्थलांतर करत आहे नवीन लॉक केलेला संदेश प्रलंबित संदेश बघण्यासाठी अनलॉक करा - - बॅकअप पासफ्रेझ बॅकअप बाह्य संचयन मध्ये जतन केले जातील आणि खालील पासफ्रेझ सोबत एन्क्रिप्ट केले जातील. बॅकअप पुनर्स्थापना करण्यासाठी आपल्याकडे हे पासफ्रेझ असायलाच हवे. मी हे पासफ्रेझ लिहून ठेवले आहे. याविना, मी बॅकअपची पुनर्स्थापना करण्यास अक्षम असेल. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index cf9ff803c..bff50add1 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -303,33 +303,6 @@   Kongsi dengan - - - - - - - - - - - - - - - Kini anda boleh melihat dan berkongsi apabila mesej sedang ditaip secara pilihan. -  - - - - - - - - - - - Mengambil satu mesej… @@ -1347,8 +1320,6 @@ Menerima mesej pertukaran kunci untuk versi protokol yang tidak sah. Memindahkan pangkalan data Signal Mesej dikunci yang baru Buka untuk melihat mesej belum selesai - - Frasa laluan sandaran Sandaran akan disimpan ke storan luaran dan disulitkan dengan frasa laluan di bawah. Anda mesti mempunyai frasa laluan tersebut untuk memulihkan sandaran. Saya telah menulis frasa laluan tersebut. Saya tidak memulihkan sandaran tanpanya. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 0f88ef83b..f6900fc87 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -343,33 +343,6 @@ Del med Flere vedlegg støttes bare for bilder og videoer - - - - - - - - - - - - - - - - - - - - - - - - - - - Mottar en melding… @@ -1557,8 +1530,6 @@ Mottok nøkkelutvekslingsmelding for ugyldig protokollversion. Flytter Signal-database Ny låst melding Lås opp for å vise ventende meldinger - - Passordfrase for sikkerhetskopi Sikkerhetskopier blir lagret på eksterne enheter og kryptert med passordfrasen nedenfor. Du må skrive inn denne passordfrasen for å kunne gjennopprette fra en sikkerhetskopi. Jeg har notert passordfrasen. Uten denne blir det umulig å bruke sikkerhetskopien til gjenoppretting. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8d6e7a35e..0958acd1c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -347,35 +347,6 @@ Doorsturen naar Het verzenden van meerdere bijlagen is alleen mogelijk voor afbeeldingen en video\'s - - - - - - - - - - - - - - - - - - - - - Nieuw: voorbeeld- -weergaven. - - - - - - - Bericht aan het ophalen… @@ -403,7 +374,7 @@ weergaven. Groepsdetails aan het laden… Je bent al lid van deze groep. - Tik hier om je profielnaam en -foto voor deze groep zichtbaar te maken + Je profielnaam en -foto voor deze groep zichtbaar maken? Wil je je profielnaam en -foto zichtbaar maken voor alle huidige en toekomstige leden van deze groep? Zichtbaar maken @@ -541,7 +512,7 @@ weergaven. %s heeft je gebeld Je hebt %s gebeld Gemiste oproep van %s - %s is vanaf nu ook beschikbaar via Signal. + %s is vanaf nu bereikbaar via Signal. Je hebt zelf-wissende berichten uitgeschakeld. %1$s heeft zelf-wissende berichten uitgeschakeld. Je hebt de timer voor zelf-wissende berichten op %1$s ingesteld. @@ -567,6 +538,10 @@ weergaven. %1$d lid %1$d leden + + %1$d lid (+%2$d uitgenodigden) + %1$d leden (+%2$d uitgenodigden) + %d ander %d andere @@ -785,7 +760,7 @@ Tot slot moet Signal de telefoonstatus kunnen lezen om te voorkomen dat Signal-o Eenmaligeweergave-afbeelding Eenmaligeweergave-video Eenmaligeweergave-media - %s is beschikbaar voor Signal-berichten. + %s is bereikbaar via Signal. Zelf-wissende berichten uitgeschakeld Berichten zullen zichzelf wissen %s nadat ze gelezen zijn Veiligheidsnummer veranderd @@ -1140,7 +1115,7 @@ Tot slot moet Signal de telefoonstatus kunnen lezen om te voorkomen dat Signal-o De groepsnaam is vanaf nu ‘%1$s’. - Je profielfoto en naam zichtbaar maken voor deze groep? + Tik hier om je profielnaam en -foto voor deze groep zichtbaar te maken Ontgrendelen @@ -1388,7 +1363,7 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Uitnodigingsvoorstel weergeven Toon boven sms-gesprekken met gesprekspartners die niet op Signal zitten een suggestie om je gesprekspartner uit te nodigen op Signal Tekstgrootte voor berichten - Contactpersoon beschikbaar via Signal + Nieuw persoon bereikbaar via Signal Belangrijkheid Verzegelde afzender ‘Verzegelde afzender’-pictorgram @@ -1643,15 +1618,13 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Signal-databank wordt gemigreerd Nieuw vergrendeld bericht Ontgrendel om je berichten te lezen - - Back-up-wachtwoord Back-up-bestanden worden opgeslagen op het externe opslaggeheugen en versleuteld met het wachtwoord hieronder. Je hebt dit wachtwoord nodig om de back-up-gegevens terug te zetten. Ik heb dit wachtwoord opgeschreven. Zonder dit wachtwoord kan ik deze back-up-bestanden niet gebruiken om gegevens terug te zetten. Back-up-gegevens terugzetten Overslaan Registreren - Laat Signal back-up-bestanden maken + Back-up-bestanden maken Sta Signal toe om versleutelde back-up-bestanden van gesprekken en media te maken naar /Signal/Backups op je interne opslag. Als dit is ingeschakeld maakt Signal automatisch elke dag een back-up-bestand. Alleen de twee meest recente geslaagde back-up-bestanden worden bewaard. Nu een back-up-bestand maken Back-up-wachtwoord verifiëren diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 1334da7ea..e711c102f 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -347,34 +347,6 @@ Del med Fleire vedlegg er berre støtta for bilde og film - - - - - - - - - - - - - - - - - - - - - - - - - - - - Hentar ei melding … @@ -566,6 +538,10 @@ %1$d medlem %1$d medlem + + %1$d medlem (+ %2$d invitert) + %1$d medlem (+ %2$d inviterte) + %d anna %d andre @@ -1637,8 +1613,6 @@ Du er à jour! Migrerer Signal-databasen Ny låst melding Lås opp for å sjå ulesne meldingar - - Passordfrase til reservekopi Reservekopiar blir lagra til ekstern lagring og krypterte med passordfrasen nedanfor. Du må ha denne passordfrasen for å gjenoppretta frå kopien. Eg har skrive ned denne passordfrasen. Utan den vil eg ikkje få gjenoppretta frå reservekopien. diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index eb1c925d3..6b8fa299a 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -325,34 +325,6 @@ ایہہ آلہ پلے سروسز دی معاونت نئیں کر دا اے۔ سسٹم بیٹری دی احسن کاری نوں غیر فعال کرن لئی بٹن دباؤ جیہڑی Signal نوں غیر فعال ہون دے دوران سنیہے بازیافت کرن توں روکدی اے۔ ایہدے نال سانجھا کرو - - - - - - - - - - - - - - - - - - - - - - - - - - - - اک سنیہا بازیافت کر رئیا اے… @@ -1555,8 +1527,6 @@ Signal ڈیٹا بیس منتقل کرنا نواں مقفل سنیہا التوا اچ پئۓ سنیہے ویکھن لئی غیر مقفل کرو - - بیک اپ پاس فریز بیک اپ نوں بیرونی اسٹوریج اچ محفوظ کیتا جاوے گا تے تھلے دتے ہوئے پاس فریز دے نال ایہدی خفیہ کاری ہووے گی۔ بیک اپ بحال کرن لئی تہاڈے کول ایہہ پاس فریز ہونا لازمی اے۔ میں اے پاس فریز لکھ لیا اے۔ ایہدے توں بغیر، میں بیک اپ بحال نئیں کر سکاں گا۔ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 6df258bab..281803085 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -313,30 +313,6 @@ ਇਹ ਡਿਵਾਈਸ ਪਲੇ ਸੇਵਾਵਾਂ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ. ਸਿਸਟਮ ਬੈਟਰੀ ਅਨੁਕੂਲਤਾ ਨੂੰ ਅਸਮਰੱਥ ਬਣਾਉਣ ਲਈ ਟੈਪ ਕਰੋ ਜੋ ਕਿ Signal ਨੂੰ ਨਾ-ਸਰਗਰਮ ਹੋਣ ਵੇਲੇ ਸੁਨੇਹਿਆਂ ਨੂੰ ਮੁੜ ਪ੍ਰਾਪਤ ਕਰਨ ਤੋਂ ਰੋਕਦੇ ਹਨ. ਨਾਲ ਸ਼ੇਅਰ ਕਰੋ - - - - - - - - - - - - - - - - - - - - - - - - ਸੁਨੇਹਾ ਪ੍ਰਾਪਤ ਕਰ ਰਿਹਾ ਹੈ … @@ -1364,8 +1340,6 @@ Signal ਡਾਟਾਬੇਸ ਮਾਈਗ੍ਰੇਟ ਹੋ ਰਿਹਾ ਹੈ ਨਵਾਂ ਲੌਕ ਕੀਤਾ ਸੁਨੇਹਾ ਬਕਾਇਆ ਸੁਨੇਹੇ ਵੇਖਣ ਲਈ ਅਨਲੌਕ ਕਰੋ - - ਬੈਕਅਪ ਪਾਸਫਰੇਜ ਬੈਕਅਪ ਬਾਹਰੀ ਸਟੋਰੇਜ ਵਿੱਚ ਸੁਰੱਖਿਅਤ ਕੀਤੇ ਜਾਣਗੇ ਅਤੇ ਹੇਠਾਂ ਪਾਸਫਰੇਜ ਨਾਲ ਏਨਕ੍ਰਿਪਟ ਕੀਤੇ ਜਾਣਗੇ. ਬੈਕਅੱਪ ਨੂੰ ਰੀਸਟੋਰ ਕਰਨ ਲਈ ਤੁਹਾਡੇ ਕੋਲ ਇਹ ਪਾਸਫਰੇਜ ਹੋਣਾ ਲਾਜ਼ਮੀ ਹੈ. ਮੈਂ ਇਹ ਪਾਸਫਰੇਜ ਲਿੱਖ ਲਿਆ ਹੈ. ਇਸ ਤੋਂ ਬਿਨਾਂ, ਮੈਂ ਬੈਕਅੱਪ ਨੂੰ ਰੀਸਟੋਰ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ ਹੋਵਾਂਗਾ. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index fc2a072f0..9f623d488 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -375,34 +375,6 @@ Podziel się z Wysyłanie kilku załączników na raz jest obsługiwane tylko dla zdjęć i filmów. - - - - - - - - - - - - - - - - - - - - - - - - - - - - Pobieranie wiadomości… @@ -610,6 +582,12 @@ %1$d użytkowników %1$d użytkowników + + %1$d członek (+%2$d zaproszony/ch) + %1$d członków (+%2$d zaproszony/ch) + %1$d członków (+%2$d zaproszony/ch) + %1$d członków (+%2$d zaproszony/ch) + %d inna %d inne @@ -1713,8 +1691,6 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Przenoszenie bazy danych Signal Nowa zablokowana wiadomość Odblokuj, aby zobaczyć oczekujące wiadomości - - Hasło kopii zapasowej Kopia zapasowa zostanie zapisana w pamięci urządzenia i zaszyfrowana za pomocą poniższego hasła. Musisz mieć to hasło, aby przywrócić kopię zapasową. Zapisałem(am) to hasło. Bez tego nie będę mógł(a) przywrócić kopii zapasowej. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index c742967f7..1a2a97c42 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -347,34 +347,6 @@ Encaminhar para Muitos anexos de uma só vez são suportados apenas para imagens e vídeos. - - - - - - - - - - - - - - - - - - - - - - - - - - - - Recuperando uma mensagem… @@ -1637,8 +1609,6 @@ Migrando a base de dados do Signal Nova mensagem bloqueada Destranque para ver as mensagens pendentes - - Frase-chave de backup Os backups serão salvos no armazenamento externo e criptografados com a frase-chave abaixo. Você precisa desta frase-chave para restaurar um backup. Eu anotei esta frase-chave. Sem ela eu não conseguirei restaurar um backup. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 859e9ebf3..56c71c7ba 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -347,34 +347,6 @@ Partilhar com Apenas são suportados anexos múltiplos para imagens e vídeos. - - - - - - - - - - - - - - - - - - - - - - - - - - - - A obter uma mensagem… @@ -1633,8 +1605,6 @@ Mensagem de troca de chaves inválida para esta versão do protocolo. A migrar a base de dados Signal Nova mensagem bloqueada Desbloqueie para ver mensagens pendentes - - Frase-passe da cópia de segurança As cópias de segurança vão ser gravadas na memória externa e cifradas com a frase-passe abaixo. Tem de ter esta frase-passe para poder restaurar as cópias de segurança. Eu anotei esta frase-passe. Sem ela, serei incapaz de restaurar uma cópia de segurança. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d73bc758d..b2d82a8ec 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -361,34 +361,6 @@ Distribuie cu Atașamentele multiple sunt suportate doar pentru imagini și videouri - - - - - - - - - - - - - - - - - - - - - - - - - - - - Se obține un mesaj… @@ -588,6 +560,11 @@ %1$d membrii %1$d membrii + + %1$d membru (+%2$d invitați) + %1$d membrii (+%2$d invitați) + %1$d membrii (+%2$d invitați) + %d altul %d alții @@ -1676,8 +1653,6 @@ Am primit mesajul conform căruia schimbul de chei a avut loc pentru o versiune Se migrează baza de date Signal Mesaj nou blocat Deblocați pentru a vedea mesajele în curs - - Parolă backup Backup-urile se vor salva pe stocarea externă și vor fi criptate cu parola de mai jos. Trebuie să aveți această parolă pentru a putea restaura un backup. Am scris parola. Fără ea nu voi putea restaura un backup. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 61bd507ed..8e7ed0670 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -375,34 +375,6 @@ Поделиться с Несколько вложений поддерживаются только для изображений и видео - - - - - - - - - - - - - - - - - - - - - - - - - - - - Получаем сообщение… @@ -610,6 +582,12 @@ %1$d участников %1$d участников + + %1$d участник (+%2$d приглашен(-о)) + %1$d участника (+%2$d приглашен(-о)) + %1$d участников (+%2$d приглашен(-о)) + %1$d участников (+%2$d приглашен(-о)) + %d другой %d других @@ -1714,8 +1692,6 @@ Миграция базы данных Signal Новое заблокированное сообщение Разблокируйте для просмотра ожидающих сообщений - - Парольная фраза резервной копии Резервные копии будут сохранены во внешнем хранилище и зашифрованы с помощью приведённой ниже парольной фразы. У вас должна быть эта фраза, чтобы восстановить резервную копию. Я записал(-а) эту парольную фразу. Без неё я не смогу восстановить резервную копию. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index bfc446239..a7dc7f62d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -375,34 +375,6 @@ Zdieľať s Viaceré prílohy sú podporované iba pre obrázky a videá - - - - - - - - - - - - - - - - - - - - - - - - - - - - Preberanie správy… @@ -1720,8 +1692,6 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. Presúvam databázu Signalu Nová uzamknutá správa Odomknite pre zobrazenie nevybavených správ - - Heslo pre zálohy Zálohy budú uložené na úložisko zašifrované nasledujúcim heslom. K obnoveniu zálohy musíte mať toto heslo k dispozícii. Heslo som si zapísal/a, bez hesla nebudem schopný/á obnoviť zálohy. diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index d730a321f..bcd1401a0 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -375,34 +375,6 @@ Deli z Več priponk je možnih le pri slikah in videu - - - - - - - - - - - - - - - - - - - - - - - - - - - - Pridobivanje sporočila … @@ -610,6 +582,12 @@ %1$d člani %1$d članov + + %1$d član (+%2$d povabljenih) + %1$d članov (+%2$d povabljena) + %1$d članov (+%2$d povabljeni) + %1$d članov (+%2$d povabljenih) + %d ostali %d ostala @@ -1712,8 +1690,6 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola.Prenašam podatkovno bazo Signal Novo zaklenjeno sporočilo Za ogled čakajočih sporočil odklenite - - Geslo varnostne kopije Varnostne kopije bodo shranjene na zunanji pomnilnik in šifrirane s spodnjim geslom. Za obnovitev varnostne kopije morate poznati to geslo. Zapisal sem si geslo. Brez njega ne bom mogel obnoviti varnostne kopije. diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index d3380217d..a57b3c79e 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -7,7 +7,7 @@ Ruaje Shënim për Veten - Lajm i ri + Mesazh i ri Signal-i po përditësohet… @@ -216,7 +216,7 @@ Bllokoje Bllokoje dhe fshije Anuloje - Fshij bisedën? + Të fshihet biseda? Të fshihet dhe braktiset grupi? Kjo bisedë do të fshihet prej krejt pajisjeve tuaja. Do ta braktisni këtë grup dhe do të fshihet prej krejt pajisjeve tuaja. @@ -347,34 +347,6 @@ Ndaje me Disa bashkëngjitje njëherësh mbulohen vetëm për figura dhe video - - - - - - - - - - - - - - - - - - - - - - - - - - - - Po merret një mesazh… @@ -437,7 +409,7 @@ Po punohet në prapaskenë… Dështoi dërgimi - Numër sigurie i ri + Numër i ri sigurie S\’arrihet të gjendet mesazh Mesazh prej %1$s @@ -566,6 +538,10 @@ %1$d anëtar %1$d anëtarë + + %1$d anëtar (+%2$d të ftuar) + %1$d anëtarë (+%2$d të ftuar) + %d tjetër %d të tjerë @@ -932,7 +908,7 @@ Numri i sigurisë për bisedën tuaj me %1$s ka ndryshuar. Kjo mund të jetë shenjë se ose sepse dikush po rreket të përgjojë komunikimin tuaj, ose se %2$s thjesht ka riinstaluar Signal-in. Mund të donit të verifikonit numrin tuaj të sigurisë me këtë kontakt. - Numër sigurie i ri + Numër i ri sigurie Pranoje Përfundoje thirrjen @@ -1633,8 +1609,6 @@ spastrohet dhe krejt lënda do të fshihet. Po migrohet baza e të dhënave Signal Mesazhe të rinj të kyçur Shkyçeni që të shihni mesazhet pezull - - Frazëkalim kopjeruajtjesh Kopjeruajtjet do të ruhen te depozitë e jashtme dhe fshehtëzohen me frazëkalimin më poshtë. Duhet të keni këtë frazëkalim, pa të mundeni të riktheni një kopjeruajtje E kam shkruar diku këtë frazëkalim. Pa të, s\’do të jem në gjendje të rikthej një kopjeruajtje @@ -1664,7 +1638,7 @@ spastrohet dhe krejt lënda do të fshihet. Fshiji kopjeruajtjet U kopjua në të papastër Jepni frazëkalimin tuaj të kopjeruajtjes që duhet verifikuar - Verifiko + Verifikoje E dhatë me sukses frazëkalimin tuaj të kopjeruajtjes Frazëkalimi s’qe i saktë Që të mund të krijohen kopjeruajtje, Signal-i lyp leje mbi depozitë të jashtme, por kjo i është mohuar. Ju lutemi, kaloni te rregullime aplikacioni, përzgjidhni \"Leje\" dhe aktivizoni \"Depozitim\". diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index ecbe6b314..3bf1eeeab 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -347,34 +347,6 @@ Dela med Flera bilagor stöds endast för bilder och videoklipp - - - - - - - - - - - - - - - - - - - - - - - - - - - - Hämtar ett meddelande… @@ -566,6 +538,10 @@ %1$d medlem %1$d medlemmar + + %1$d medlem (+%2$d inbjudna) + %1$d medlemmar (+%2$d inbjudna) + %d annan %d andra @@ -1631,8 +1607,6 @@ Tog emot meddelande för nyckelutbyte för ogiltig protokollversion. Migrerar Signal-databas Nytt låst meddelande Lås upp för att se väntande meddelanden - - Lösenord för säkerhetskopering Säkerhetskopior sparas till extern lagring och krypteras med lösenordet nedanför. Du måste ange lösenordet för att återställa en säkerhetskopia. Jag har skrivit ner lösenordet. Utan det kommer jag inte kunna återställa en säkerhetskopia. diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 436269574..7da558c7e 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -326,35 +326,6 @@ Kifaa hiki hakiwezeshi Huduma za Play. Gonga ili kulemaza na kuboresha mfumo wa betri unaozuia Signal kupokea ujumbe wakati haupo kwenye matumizi. Shiriki na - - - - - - - - - - - - - - - - - - - - - - - - - - - Mbona utumie maneno wakati unaweza kutumia vibandiko? -Gusa ikoni hii kwenye kicharazio chako: - Inaleta ujumbe @@ -1554,8 +1525,6 @@ nambari yako ya simu Inahamisha hifadhidata ya Signal Ujumbe mpya uliofungwa Fungua ili uone jumbe zinazosubiri - - Hifadhi Nenosiri Nakili zitahifadhiwa kwenye hifadhi ya nje na kusimbwa na nenosiri lililopo hapo chini. Lazima uwe na Nenosiri hili ili kurejesha nakalahifadhi. Nimeandika Nenosiri hili. Bila yalo, nishindwa kurejesha nakalahifadhi. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 83d47b650..ee498a884 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -326,34 +326,6 @@ இந்த சாதனம் Play சேவைகளை ஆதரிக்காது. செயலற்ற நிலையில் செய்திகளை மீட்டெடுப்பதில் இருந்து Signal லைத் தடுக்கும் கணினி பேட்டரி மேம்படுத்தல்களை முடக்க தட்டவும். பகிர் - - - - - - - - - - - - - - - - - - - - - - - - - - - - ஒரு செய்தியை மீட்டெடுக்கிறது … @@ -1535,8 +1507,6 @@ Signal தரவுத்தளத்தை மாற்றுதல் பூட்டப்பட்ட புதிய செய்தி நிலுவையிலுள்ள செய்திகளைக் காண திறக்கவும் - - கடவுச்சொல்லை காப்புப்பிரதி செய்க காப்புப்பதிவு பயனர் தரவு வெளிப்புற சேமிப்பகத்தில் சேமிக்கப்பட்டு கீழே உள்ள கடவுச்சொற்றொடருடன் குறியாக்கம் செய்யப்படும். காப்புப்பிரதியை மீட்டமைக்க இந்த கடவுச்சொற்றொடரை நீங்கள் கொண்டிருக்க வேண்டும். இந்த கடவுச்சொற்றொடரை நான் எழுதியுள்ளேன். இது இல்லாமல், என்னால் ஒரு காப்புப்பதிவு பயனர் தரவு மீட்டெடுக்க முடியாது. diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index d04e1d5ea..a55144263 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -329,34 +329,6 @@ క్రియారహితంగా ఉన్నప్పుడు సందేశాలను పొందటం నుండి Signal నిరోధించే వ్యవస్థ బ్యాటరీ అనుకూలతలు డిసేబుల్ నొక్కండి. తో పంచు - - - - - - - - - - - - - - - - - - - - - - - - - - - - సందేశాన్ని తిరిగి పొందుతోంది… @@ -1511,8 +1483,6 @@ Signal దత్తాంశమూల తలరింపడుతోంది. కొత్త లాక్ చేయబడిన సందేశం పెండింగ్లో ఉన్న సందేశాలను వీక్షించడానికి అన్లాక్ చేయండి - - ప్రత్యామ్నాయ పాస్ఫ్రేజ్ ప్రత్యామ్నాయ బాహ్య నిల్వకి భద్రపరచ బడుతుంది మరియు పాస్ఫ్రేజ్తో గుప్తీకరించబడుతుంది. ప్రత్యామ్నాయను పునరుద్ధరించడానికి మీరు తప్పనిసరిగా ఈ పాస్ఫ్రేజ్ను కలిగి ఉండాలి. నేను ఈ పాస్ఫ్రేజ్ని వ్రాశాను. ఇది లేకుండా, నేను ఒక ప్రత్యామ్నాయ పునరుద్ధరించడానికి సాధ్యం కాదు. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index a39404cf8..4490ca81a 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -333,34 +333,6 @@ แบ่งปันกับ การแนบแฟ้มหลายชิ้นแนบได้เฉพาะรูปภาพและวิดีโอ - - - - - - - - - - - - - - - - - - - - - - - - - - - - กำลังเรียกข้อความกลับมา… @@ -1595,8 +1567,6 @@ ย้ายฐานข้อมูล Signal ข้อความที่ถูกล็อกใหม่ ปลดล็อกเพื่อดูข้อความที่รออยู่ - - วลีรหัสผ่านสำหรับข้อมูลสำรอง ข้อมูลสำรองจะถูกบันทึกไว้ในที่เก็บข้อมูลภายนอก และถูกเข้ารหัสลับด้วยวลีรหัสผ่านด้านล่างนี้ คุณจะต้องใช้วลีรหัสผ่านนี้เพื่อกู้คืนข้อมูลสำรอง ฉันได้จดบันทึกวลีรหัสผ่านนี้แล้ว หากจำไม่ได้ ฉันจะไม่สามารถกู้คืนจากข้อมูลสำรองได้ diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index dfc36712e..17694fc4f 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -347,34 +347,6 @@ Ibahagi kay Ang maramihang kalakip ay suportado lamang para sa mga imahe at videos - - - - - - - - - - - - - - - - - - - - - - - - - - - - May kinukuhang mensahe… @@ -1635,8 +1607,6 @@ Inililipat ang database ng Signal Bagong naka-lock na mensahe I-unlock upang makita ang mga nakabinbin na mensahe - - I-backup ang passphrase Ang mga backup ay ise-save sa panlabas na storage at gagawing encrypted gamit ang passphrase sa ibaba. Kailangang nasa iyo ang passphrase na ito upang mag-restore ng backup. Naisulat ko na ang passphrase na ito. Kung wala ito, hindi ako makakapag-restore ng backup. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b897747c2..791f75c3d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -346,34 +346,6 @@ Paylaşılacak kişi Çoklu ekler sadece resim ve dosyalarda destekleniyor - - - - - - - - - - - - - - - - - - - - - - - - - - - - Bir ileti alınıyor… @@ -1624,8 +1596,6 @@ Geçersiz protokol sürümünde anahtar değişim iletisi alındı. Signal veritabanı taşınıyor Yeni kilitli ileti Bekleyen iletileri görüntülemek için kilidi açın - - Yedek parolası Yedekler harici depolama alanına kaydedilecek ve aşağıdaki parola ile şifrelenecektir. Yedeği geri yüklemek için bu parolaya sahip olmanız gereklidir. Bu parolayı kaydettim. Parola olmadan yedeği geri yükleyemem. diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index b829c57b0..37f9010ad 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -324,34 +324,6 @@ یہ ڈیوائس پلے سروسز کا تعاون نہیں کرتی ہے۔ سسٹم کی بیٹری کی اصلاح کو غیر فعال کرنے کیلئے ٹیپ کریں جو Signal کو غیر فعال ہونے پر پیغامات کی بازیافت سے روکتی ہیں۔ کے ساتھ اشتراک کریں - - - - - - - - - - - - - - - - - - - - - - - - - - - - پیغام دوبارہ حاصل ہو رہا ہے۔۔۔ @@ -1502,8 +1474,6 @@ Signal ڈیٹا بیس منتقل ہو رہا ہے نیا پیغام لاک ہے زیر غور پیغامات دیکھنے کے لیے لاک کھولیں - - فاسفریز بیک اپ کریں بیک اپ کو بیرونی اسٹوریج میں محفوظ کیا جائے گا اور نیچے پاسفریز کے ساتھ خفیہ کاری ہوگی۔ بیک اپ بحال کرنے کے لئے آپ کے پاس یہ پاس فریز ہونا ضروری ہے۔ میں نے یہ پاسفریز نیچے لکھا ہے۔اس کے بغیر، یہ بیک اپ بحال کرنے کے قابل نہیں گا۔ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 22c0e5f44..4f23629dc 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -333,34 +333,6 @@ Chia sẻ với Gửi nhiều tệp đính kèm chỉ áp dụng cho ảnh và video - - - - - - - - - - - - - - - - - - - - - - - - - - - - Đang nhận tin nhắn… @@ -1589,8 +1561,6 @@ Nhận thông tin trao đổi mã khóa về phiên bản giao thức không h Đang di dời cơ sở dữ liệu Signal Tin nhắn đã khoá mới Mở khoá để xem tin nhắn đang chờ - - Mật khẩu sao lưu Sao lưu sẽ được lưu trong bộ nhớ và được mã hóa với mật khẩu bên dưới. Bạn phải có mật khẩu này để khôi phục bản sao lưu. Tôi đã ghi lại mật khẩu này. Không có nó, tôi sẽ không thể khôi phục bản sao lưu. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1377734f5..a23905277 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -312,34 +312,6 @@ 该设备不支持 Google Play 服务。点击禁用系统电池优化,防止 Signal 非活动时无法获取新消息。 分享至 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 检索消息中… @@ -1535,8 +1507,6 @@ 迁移 Signal 数据库 新的锁定消息 解锁查看待处理消息 - - 备份密码 备份将保存至外部存储,并使用以下密码加密。备份恢复时需使用该密码。 我也记下该密码。没有密码,将无法恢复备份。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a03daf4f2..1d5b542b9 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -179,7 +179,7 @@ 解除封鎖此聯絡人? 解除封鎖這個群組? 您將可以再次收到來自此聯絡人的訊息與通話。 - 現存的組員將可以再次將你加入群組。 + 現存的成員將可以再次將你加入群組。 解除封鎖 當前訊息的附件類型已經超越大小限制。 無法使用相機 @@ -207,7 +207,7 @@ 封鎖並離開%1$s嗎? 解除封鎖%1$s嗎? 你將能夠發訊息並互相打電話。 - 群組組員將能夠再次將你新增到該群組。 + 群組成員將能夠再次將你新增到該群組。 已被封鎖的人將無法撥電話或傳訊息給你。 你將離開此群組及無法再接收訊息或升級。 封鎖 @@ -333,34 +333,6 @@ 分享給 僅圖片和影片支援多個附檔 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 讀取一個訊息中… @@ -379,7 +351,7 @@ 您所選擇的聯絡人並不支援 Signal 群組,所以此組將以多媒體訊息形式存在。 您沒有註冊 Signal 訊息與通話,所以 Signal 群組已經被停用。請嘗試在設定 > 進階中重新註冊。 建立群組至少需要加入一個聯絡人! - 群組中某個聯絡人的號碼有誤,請更正或移除此聯絡人,並再試一次。 + 群組中某個成員的電話號碼無法讀取,請更正或移除此聯絡人,並再試一次。 群組的大頭照 應用 %1$s 建立中… @@ -544,6 +516,9 @@ %1$d個成員 + + %1$d成員(+%2$d已被邀請) + %d其他 @@ -611,7 +586,7 @@ 解除封鎖此聯絡人? 您將可以再次收到來自此聯絡人的訊息與通話。 解除封鎖這個群組? - 現存的組員將可以再次將你加入群組。 + 現有的成員將可以再次將你加入群組。 離開群組時發生錯誤 解除封鎖 啟用 @@ -1592,8 +1567,6 @@ 遷移 Signal 資料庫 新鎖定的訊息 解鎖以查看待處理的訊息 - - 備份密碼 備份將會儲存到外部儲存空間,並以下方的密碼加密。你必須擁有這個密碼來還原此備份。 我已經寫下密碼。沒有密碼,我將無法還原此備份。 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 3da6c28f4..6eee232cf 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -217,6 +217,7 @@ + diff --git a/app/src/main/res/values/core_colors.xml b/app/src/main/res/values/core_colors.xml index 75b8ddc62..65afdde4d 100644 --- a/app/src/main/res/values/core_colors.xml +++ b/app/src/main/res/values/core_colors.xml @@ -1,7 +1,7 @@ #2c6bed - #2c6bed + #6191f3 #1851b4 #3a76f0 #552c6bed diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 581110ed6..b4331eb68 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -53,7 +53,7 @@ dark @style/ThemeOverlay.AppCompat.Dark @color/core_grey_95 - @color/core_ultramarine + @color/core_ultramarine_light #d00000 #66eeeeee @@ -110,7 +110,7 @@ false true true - @color/core_ultramarine + @color/core_ultramarine_light @color/white @color/core_grey_25 @@ -401,6 +401,7 @@ @drawable/preference_divider_light @color/core_grey_60 + @color/core_grey_70 @drawable/ic_group_outline_24 @style/PreferenceThemeOverlay.Fix @@ -450,7 +451,7 @@ @style/TextSecure.ActionModeStyle @color/text_color_dark_theme @color/text_color_secondary_dark_theme - @color/core_ultramarine + @color/core_ultramarine_light @color/core_ultramarine_light @color/core_ultramarine_light @color/core_ultramarine_light @@ -673,6 +674,7 @@ @drawable/preference_divider_dark @color/core_grey_05 + @color/core_white @drawable/ic_group_solid_24 @style/PreferenceThemeOverlay.Fix @@ -697,7 +699,7 @@ diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java index 2b6bcc0ca..b835ddee0 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupIdTest.java @@ -1,6 +1,12 @@ package org.thoughtcrime.securesms.groups; import org.junit.Test; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupIdentifier; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.thoughtcrime.securesms.util.Hex; + +import java.io.IOException; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -8,42 +14,90 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.thoughtcrime.securesms.groups.ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS; +import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom; public final class GroupIdTest { @Test public void can_create_for_gv1() { - GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId.V1 groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertFalse(groupId.isMms()); } @Test public void can_parse_gv1() { - GroupId groupId = GroupId.parse("__textsecure_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertTrue(groupId.isV1()); + assertFalse(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_create_for_gv2_from_GroupIdentifier() throws IOException, InvalidInputException { + GroupId.V2 groupId = GroupId.v2(new GroupIdentifier(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))); + + assertEquals("__textsecure_group__!0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", groupId.toString()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_create_for_gv2_from_GroupMasterKey() throws IOException, InvalidInputException { + assumeZkGroupSupportedOnOS(); + + GroupId.V2 groupId = GroupId.v2(new GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))); + + assertEquals("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e", groupId.toString()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); + } + + @Test + public void can_parse_gv2() throws IOException { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + assertEquals("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e", groupId.toString()); + assertArrayEquals(Hex.fromStringCondensed("9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"), groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertFalse(groupId.isV1()); + assertTrue(groupId.isV2()); + assertTrue(groupId.isPush()); } @Test public void can_create_for_mms() { - GroupId groupId = GroupId.mms(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId.Mms groupId = GroupId.mms(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); - assertEquals("__signal_mms_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertTrue(groupId.isMmsGroup()); + assertEquals("__signal_mms_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertTrue(groupId.isMms()); + assertFalse(groupId.isV1()); + assertFalse(groupId.isV2()); + assertFalse(groupId.isPush()); } @Test public void can_parse_mms() { - GroupId groupId = GroupId.parse("__signal_mms_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__signal_mms_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertTrue(groupId.isMmsGroup()); + assertEquals("__signal_mms_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertTrue(groupId.isMms()); + assertFalse(groupId.isV1()); + assertFalse(groupId.isV2()); + assertFalse(groupId.isPush()); } @SuppressWarnings("ConstantConditions") @@ -56,16 +110,19 @@ public final class GroupIdTest { @Test public void can_parse_gv1_with_parseNullable() { - GroupId groupId = GroupId.parseNullable("__textsecure_group__!0001020305060708090b0c0d0e0f"); + GroupId groupId = GroupId.parseNullable("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); - assertEquals("__textsecure_group__!0001020305060708090b0c0d0e0f", groupId.toString()); - assertArrayEquals(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); - assertFalse(groupId.isMmsGroup()); + assertEquals("__textsecure_group__!000102030405060708090a0b0c0d0e0f", groupId.toString()); + assertArrayEquals(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, groupId.getDecodedId()); + assertFalse(groupId.isMms()); + assertTrue(groupId.isV1()); + assertFalse(groupId.isV2()); + assertTrue(groupId.isPush()); } @Test(expected = AssertionError.class) public void bad_encoding__bad_prefix__parseNullable() { - GroupId.parseNullable("__BAD_PREFIX__!0001020305060708090b0c0d0e0f"); + GroupId.parseNullable("__BAD_PREFIX__!000102030405060708090a0b0c0d0e0f"); } @Test(expected = AssertionError.class) @@ -80,7 +137,7 @@ public final class GroupIdTest { @Test(expected = AssertionError.class) public void bad_encoding__bad_prefix__parse() { - GroupId.parse("__BAD_PREFIX__!0001020305060708090b0c0d0e0f"); + GroupId.parse("__BAD_PREFIX__!000102030405060708090a0b0c0d0e0f"); } @Test(expected = AssertionError.class) @@ -90,7 +147,7 @@ public final class GroupIdTest { @Test public void get_bytes() { - byte[] bytes = { 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }; + byte[] bytes = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; GroupId groupId = GroupId.v1(bytes); assertArrayEquals(bytes, groupId.getDecodedId()); @@ -98,8 +155,8 @@ public final class GroupIdTest { @Test public void equality() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertEquals(groupId1, groupId2); @@ -108,8 +165,8 @@ public final class GroupIdTest { @Test public void inequality_by_bytes() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.v1(new byte[]{ 0, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertNotEquals(groupId1, groupId2); @@ -118,8 +175,8 @@ public final class GroupIdTest { @Test public void inequality_of_sms_and_mms() { - GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); - GroupId groupId2 = GroupId.mms(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId1 = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); + GroupId groupId2 = GroupId.mms(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotSame(groupId1, groupId2); assertNotEquals(groupId1, groupId2); @@ -128,8 +185,100 @@ public final class GroupIdTest { @Test public void inequality_with_null() { - GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15 }); + GroupId groupId = GroupId.v1(new byte[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }); assertNotEquals(groupId, null); } + + @Test + public void require_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + GroupId.Mms mms = groupId.requireMms(); + + assertSame(groupId, mms); + } + + @Test + public void require_v1_and_push() { + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); + + GroupId.V1 v1 = groupId.requireV1(); + GroupId.Push push = groupId.requirePush(); + + assertSame(groupId, v1); + assertSame(groupId, push); + } + + @Test + public void require_v2_and_push() { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + GroupId.V2 v2 = groupId.requireV2 (); + GroupId.Push push = groupId.requirePush(); + + assertSame(groupId, v2); + assertSame(groupId, push); + } + + @Test(expected = AssertionError.class) + public void cannot_require_push_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requirePush(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v1_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV1(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v2_of_mms() { + GroupId groupId = GroupId.parse("__signal_mms_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV2(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v1_of_v2() { + GroupId groupId = GroupId.parse("__textsecure_group__!9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e"); + + groupId.requireV1(); + } + + @Test(expected = AssertionError.class) + public void cannot_require_v2_of_v1() { + GroupId groupId = GroupId.parse("__textsecure_group__!000102030405060708090a0b0c0d0e0f"); + + groupId.requireV2(); + } + + @Test(expected = AssertionError.class) + public void cannot_create_v1_with_a_v2_length() throws IOException { + GroupId.v1(Hex.fromStringCondensed("9f475f59b2518bff6df22e820803f0e3585bd99e686fa7e7fbfc2f92fd5d953e")); + } + + @Test(expected = AssertionError.class) + public void cannot_create_v2_with_a_v1_length() throws IOException { + GroupId.v2(Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f")); + } + + @Test + public void create_mms() { + GroupId.Mms mms = GroupId.createMms(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 })); + + assertEquals("__signal_mms_group__!090a0b0c0d0e0f000102030405060708", mms.toString()); + assertTrue(mms.isMms()); + } + + @Test + public void create_v1() { + GroupId.V1 v1 = GroupId.createV1(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 })); + + assertEquals("__textsecure_group__!090a0b0c0d0e0f000102030405060708", v1.toString()); + assertTrue(v1.isV1()); + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java b/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java new file mode 100644 index 000000000..0914e9160 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/ZkGroupLibraryUtil.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.groups; + +import org.signal.zkgroup.internal.Native; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNoException; + +class ZkGroupLibraryUtil { + + /** + * Attempts to initialize the ZkGroup Native class, which will load the native binaries. + *

+ * If that fails to link, then on Unix, it will fail as we rely on that for CI. + *

+ * If that fails to link, and it's not Unix, it will skip the test via assumption violation. + */ + static void assumeZkGroupSupportedOnOS() { + try { + Class.forName(Native.class.getName()); + } catch (ClassNotFoundException e) { + fail(); + } catch (UnsatisfiedLinkError e) { + String osName = System.getProperty("os.name"); + + if (isUnix(osName)) { + fail("Not able to link native ZkGroup on a key OS: " + osName); + } else { + assumeNoException("Not able to link native ZkGroup on this operating system: " + osName, e); + } + } + } + + private static boolean isUnix(String osName) { + assertNotNull(osName); + osName = osName.toLowerCase(); + return osName.contains("nix") || osName.contains("nux") || osName.contains("aix"); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index d863b2aa2..74961c334 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -113,7 +113,7 @@ public class SignalServiceMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes) + public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes) throws IOException, InvalidMessageException { return retrieveAttachment(pointer, destination, maxSizeBytes, null); @@ -142,7 +142,7 @@ public class SignalServiceMessageReceiver { return socket.retrieveProfileByUsername(username, unidentifiedAccess); } - public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes) + public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes) throws IOException { socket.retrieveProfileAvatar(path, destination, maxSizeBytes); @@ -162,7 +162,7 @@ public class SignalServiceMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener) + public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, InvalidMessageException { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 9c28a3a95..80e8ce059 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -27,6 +27,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; @@ -64,6 +66,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMe import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.NullMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; @@ -451,8 +454,15 @@ public class SignalServiceMessageSender { builder.setBody(message.getBody().get()); } - if (message.getGroupInfo().isPresent()) { - builder.setGroup(createGroupContent(message.getGroupInfo().get())); + if (message.getGroupContext().isPresent()) { + SignalServiceGroupContext groupContext = message.getGroupContext().get(); + if (groupContext.getGroupV1().isPresent()) { + builder.setGroup(createGroupContent(groupContext.getGroupV1().get())); + } + + if (groupContext.getGroupV2().isPresent()) { + builder.setGroupV2(createGroupContent(groupContext.getGroupV2().get())); + } } if (message.isEndSession()) { @@ -966,6 +976,19 @@ public class SignalServiceMessageSender { return builder.build(); } + private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) { + GroupContextV2.Builder builder = GroupContextV2.newBuilder() + .setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize())) + .setRevision(group.getRevision()); + + byte[] signedGroupChange = group.getSignedGroupChange(); + if (signedGroupChange != null) { + builder.setGroupChange(ByteString.copyFrom(signedGroupChange)); + } + + return builder.build(); + } + private List createSharedContactContent(List contacts) throws IOException { List results = new LinkedList<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index e9d009124..2ac40641e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -11,6 +11,8 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.libsignal.metadata.ProtocolInvalidKeyException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; @@ -253,7 +255,17 @@ public final class SignalServiceContent { private static SignalServiceDataMessage createSignalServiceMessage(SignalServiceMetadata metadata, SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException, UnsupportedDataMessageException { - SignalServiceGroup groupInfo = createGroupInfo(content); + SignalServiceGroup groupInfoV1 = createGroupV1Info(content); + SignalServiceGroupV2 groupInfoV2 = createGroupV2Info(content); + Optional groupContext; + + try { + groupContext = SignalServiceGroupContext.createOptional(groupInfoV1, groupInfoV2); + } catch (InvalidMessageException e) { + throw new ProtocolInvalidMessageException(e, null, 0); + } + + List attachments = new LinkedList<>(); boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0); boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0); @@ -269,7 +281,7 @@ public final class SignalServiceContent { content.getRequiredProtocolVersion(), metadata.getSender().getIdentifier(), metadata.getSenderDevice(), - Optional.fromNullable(groupInfo)); + groupContext); } for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) { @@ -283,7 +295,7 @@ public final class SignalServiceContent { } return new SignalServiceDataMessage(metadata.getTimestamp(), - groupInfo, + groupInfoV1, groupInfoV2, attachments, content.getBody(), endSession, @@ -310,7 +322,7 @@ public final class SignalServiceContent { ? Optional.of(new SignalServiceAddress(UuidUtil.parseOrNull(sentContent.getDestinationUuid()), sentContent.getDestinationE164())) : Optional.absent(); - if (!address.isPresent() && !dataMessage.getGroupInfo().isPresent()) { + if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) { throw new ProtocolInvalidMessageException(new InvalidMessageException("SyncMessage missing both destination and group ID!"), null, 0); } @@ -739,7 +751,7 @@ public final class SignalServiceContent { } - private static SignalServiceGroup createGroupInfo(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { + private static SignalServiceGroup createGroupV1Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (!content.hasGroup()) return null; SignalServiceGroup.Type type; @@ -799,4 +811,30 @@ public final class SignalServiceContent { return new SignalServiceGroup(content.getGroup().getId().toByteArray()); } + + private static SignalServiceGroupV2 createGroupV2Info(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { + if (!content.hasGroupV2()) return null; + + SignalServiceProtos.GroupContextV2 groupV2 = content.getGroupV2(); + if (!groupV2.hasMasterKey()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 master key on message"), null, 0); + } + if (!groupV2.hasRevision()) { + throw new ProtocolInvalidMessageException(new InvalidMessageException("No GV2 revision on message"), null, 0); + } + + SignalServiceGroupV2.Builder builder; + try { + builder = SignalServiceGroupV2.newBuilder(new GroupMasterKey(groupV2.getMasterKey().toByteArray())) + .withRevision(groupV2.getRevision()); + } catch (InvalidInputException e) { + throw new ProtocolInvalidMessageException(new InvalidMessageException(e), null, 0); + } + + if (groupV2.hasGroupChange()) { + builder.withSignedGroupChange(groupV2.getGroupChange().toByteArray()); + } + + return builder.build(); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index 28b8a5a50..a3d4067f4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -6,6 +6,7 @@ package org.whispersystems.signalservice.api.messages; +import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -21,7 +22,7 @@ public class SignalServiceDataMessage { private final long timestamp; private final Optional> attachments; private final Optional body; - private final Optional group; + private final Optional group; private final Optional profileKey; private final boolean endSession; private final boolean expirationUpdate; @@ -34,101 +35,33 @@ public class SignalServiceDataMessage { private final boolean viewOnce; private final Optional reaction; - /** - * Construct a SignalServiceDataMessage with a body and no attachments. - * - * @param timestamp The sent timestamp. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, String body) { - this(timestamp, body, 0); - } - - /** - * Construct an expiring SignalServiceDataMessage with a body and no attachments. - * - * @param timestamp The sent timestamp. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which the message should expire after having been seen. - */ - public SignalServiceDataMessage(long timestamp, String body, int expiresInSeconds) { - this(timestamp, (List)null, body, expiresInSeconds); - } - - - public SignalServiceDataMessage(final long timestamp, final SignalServiceAttachment attachment, final String body) { - this(timestamp, new LinkedList() {{add(attachment);}}, body); - } - - /** - * Construct a SignalServiceDataMessage with a body and list of attachments. - * - * @param timestamp The sent timestamp. - * @param attachments The attachments. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, List attachments, String body) { - this(timestamp, attachments, body, 0); - } - - /** - * Construct an expiring SignalServiceDataMessage with a body and list of attachments. - * - * @param timestamp The sent timestamp. - * @param attachments The attachments. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which the message should expire after having been seen. - */ - public SignalServiceDataMessage(long timestamp, List attachments, String body, int expiresInSeconds) { - this(timestamp, null, attachments, body, expiresInSeconds); - } - - /** - * Construct a SignalServiceDataMessage group message with attachments and body. - * - * @param timestamp The sent timestamp. - * @param group The group information. - * @param attachments The attachments. - * @param body The message contents. - */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body) { - this(timestamp, group, attachments, body, 0); - } - - - /** - * Construct an expiring SignalServiceDataMessage group message with attachments and body. - * - * @param timestamp The sent timestamp. - * @param group The group information. - * @param attachments The attachments. - * @param body The message contents. - * @param expiresInSeconds The number of seconds in which a message should disappear after having been seen. - */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, List attachments, String body, int expiresInSeconds) { - this(timestamp, group, attachments, body, false, expiresInSeconds, false, null, false, null, null, null, null, false, null); - } - /** * Construct a SignalServiceDataMessage. * * @param timestamp The sent timestamp. * @param group The group information (or null if none). + * @param groupV2 The group information (or null if none). * @param attachments The attachments (or null if none). * @param body The message contents. * @param endSession Flag indicating whether this message should close a session. * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. */ - public SignalServiceDataMessage(long timestamp, SignalServiceGroup group, - List attachments, - String body, boolean endSession, int expiresInSeconds, - boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, - Quote quote, List sharedContacts, List previews, - Sticker sticker, boolean viewOnce, Reaction reaction) + SignalServiceDataMessage(long timestamp, + SignalServiceGroup group, SignalServiceGroupV2 groupV2, + List attachments, + String body, boolean endSession, int expiresInSeconds, + boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, + Quote quote, List sharedContacts, List previews, + Sticker sticker, boolean viewOnce, Reaction reaction) { + try { + this.group = SignalServiceGroupContext.createOptional(group, groupV2); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } + this.timestamp = timestamp; this.body = Optional.fromNullable(body); - this.group = Optional.fromNullable(group); this.endSession = endSession; this.expiresInSeconds = expiresInSeconds; this.expirationUpdate = expirationUpdate; @@ -184,9 +117,9 @@ public class SignalServiceDataMessage { } /** - * @return The message group info (if any). + * @return The message group context (if any). */ - public Optional getGroupInfo() { + public Optional getGroupContext() { return group; } @@ -202,8 +135,10 @@ public class SignalServiceDataMessage { return profileKeyUpdate; } - public boolean isGroupUpdate() { - return group.isPresent() && group.get().getType() != SignalServiceGroup.Type.DELIVER; + public boolean isGroupV1Update() { + return group.isPresent() && + group.get().getGroupV1().isPresent() && + group.get().getGroupV1().get().getType() != SignalServiceGroup.Type.DELIVER; } public int getExpiresInSeconds() { @@ -244,18 +179,19 @@ public class SignalServiceDataMessage { private List sharedContacts = new LinkedList<>(); private List previews = new LinkedList<>(); - private long timestamp; - private SignalServiceGroup group; - private String body; - private boolean endSession; - private int expiresInSeconds; - private boolean expirationUpdate; - private byte[] profileKey; - private boolean profileKeyUpdate; - private Quote quote; - private Sticker sticker; - private boolean viewOnce; - private Reaction reaction; + private long timestamp; + private SignalServiceGroup group; + private SignalServiceGroupV2 groupV2; + private String body; + private boolean endSession; + private int expiresInSeconds; + private boolean expirationUpdate; + private byte[] profileKey; + private boolean profileKeyUpdate; + private Quote quote; + private Sticker sticker; + private boolean viewOnce; + private Reaction reaction; private Builder() {} @@ -265,10 +201,21 @@ public class SignalServiceDataMessage { } public Builder asGroupMessage(SignalServiceGroup group) { + if (this.groupV2 != null) { + throw new AssertionError("Can not contain both V1 and V2 group contexts."); + } this.group = group; return this; } + public Builder asGroupMessage(SignalServiceGroupV2 group) { + if (this.group != null) { + throw new AssertionError("Can not contain both V1 and V2 group contexts."); + } + this.groupV2 = group; + return this; + } + public Builder withAttachment(SignalServiceAttachment attachment) { this.attachments.add(attachment); return this; @@ -354,7 +301,7 @@ public class SignalServiceDataMessage { public SignalServiceDataMessage build() { if (timestamp == 0) timestamp = System.currentTimeMillis(); - return new SignalServiceDataMessage(timestamp, group, attachments, body, endSession, + return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, viewOnce, reaction); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 218f111b8..321557aaf 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java new file mode 100644 index 000000000..6f1ca258e --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java @@ -0,0 +1,59 @@ +package org.whispersystems.signalservice.api.messages; + +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.util.guava.Optional; + +public final class SignalServiceGroupContext { + + private final Optional groupV1; + private final Optional groupV2; + + private SignalServiceGroupContext(SignalServiceGroup groupV1) { + this.groupV1 = Optional.of(groupV1); + this.groupV2 = Optional.absent(); + } + + private SignalServiceGroupContext(SignalServiceGroupV2 groupV2) { + this.groupV1 = Optional.absent(); + this.groupV2 = Optional.of(groupV2); + } + + public Optional getGroupV1() { + return groupV1; + } + + public Optional getGroupV2() { + return groupV2; + } + + static Optional createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) + throws InvalidMessageException + { + return Optional.fromNullable(create(groupV1, groupV2)); + } + + public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) + throws InvalidMessageException + { + if (groupV1 == null && groupV2 == null) { + return null; + } + + if (groupV1 != null && groupV2 != null) { + throw new InvalidMessageException("Message cannot have both V1 and V2 group contexts."); + } + + if (groupV1 != null) { + return new SignalServiceGroupContext(groupV1); + } else { + return new SignalServiceGroupContext(groupV2); + } + } + + public SignalServiceGroup.Type getGroupV1Type() { + if (groupV1.isPresent()) { + return groupV1.get().getType(); + } + return null; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java new file mode 100644 index 000000000..1362ad4e1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java @@ -0,0 +1,66 @@ +package org.whispersystems.signalservice.api.messages; + +import org.signal.zkgroup.groups.GroupMasterKey; + +/** + * Group information to include in SignalServiceMessages destined to v2 groups. + *

+ * This class represents a "context" that is included with Signal Service messages + * to make them group messages. + */ +public final class SignalServiceGroupV2 { + + private final GroupMasterKey masterKey; + private final int revision; + private final byte[] signedGroupChange; + + private SignalServiceGroupV2(Builder builder) { + this.masterKey = builder.masterKey; + this.revision = builder.revision; + this.signedGroupChange = builder.signedGroupChange != null ? builder.signedGroupChange.clone() : null; + } + + public GroupMasterKey getMasterKey() { + return masterKey; + } + + public int getRevision() { + return revision; + } + + public byte[] getSignedGroupChange() { + return signedGroupChange; + } + + public static Builder newBuilder(GroupMasterKey masterKey) { + return new Builder(masterKey); + } + + public static class Builder { + + private final GroupMasterKey masterKey; + private int revision; + private byte[] signedGroupChange; + + private Builder(GroupMasterKey masterKey) { + if (masterKey == null) { + throw new IllegalArgumentException(); + } + this.masterKey = masterKey; + } + + Builder withRevision(int revision) { + this.revision = revision; + return this; + } + + Builder withSignedGroupChange(byte[] signedGroupChange) { + this.signedGroupChange = signedGroupChange; + return this; + } + + public SignalServiceGroupV2 build() { + return new SignalServiceGroupV2(this); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java new file mode 100644 index 000000000..88cb3df05 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java @@ -0,0 +1,120 @@ +package org.whispersystems.signalservice.internal.groupsv2; + +import com.google.protobuf.ByteString; + +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; +import org.signal.zkgroup.util.UUIDUtil; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; + +public final class DecryptedGroupUtil { + + public static Set toUuidSet(Collection membersList) { + HashSet uuids = new HashSet<>(membersList.size()); + + for (DecryptedMember member : membersList) { + uuids.add(toUuid(member)); + } + + return uuids; + } + + public static ArrayList toUuidList(Collection membersList) { + ArrayList uuidList = new ArrayList<>(membersList.size()); + + for (DecryptedMember member : membersList) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + + public static ArrayList pendingToUuidList(Collection membersList) { + ArrayList uuidList = new ArrayList<>(membersList.size()); + + for (DecryptedPendingMember member : membersList) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + + public static UUID toUuid(DecryptedMember member) { + return UUIDUtil.deserialize(member.getUuid().toByteArray()); + } + + public static UUID toUuid(DecryptedPendingMember member) { + return UUIDUtil.deserialize(member.getUuid().toByteArray()); + } + + /** + * The UUID of the member that made the change. + */ + public static UUID editorUuid(DecryptedGroupChange change) { + return UuidUtil.fromByteString(change.getEditor()); + } + + public static Optional findMemberByUuid(Collection members, UUID uuid) { + ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + + for (DecryptedMember member : members) { + if (uuidBytes.equals(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.absent(); + } + + public static Optional findPendingByUuid(Collection members, UUID uuid) { + ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + + for (DecryptedPendingMember member : members) { + if (uuidBytes.equals(member.getUuid())) { + return Optional.of(member); + } + } + + return Optional.absent(); + } + + /** + * Removes the uuid from the full members of a group. + *

+ * Generally not expected to have to do this, just in the case of leaving a group where you cannot + * get the new group state as you are not in the group any longer. + */ + public static DecryptedGroup removeMember(DecryptedGroup group, UUID uuid, int revision) { + DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group); + ByteString uuidString = UuidUtil.toByteString(uuid); + boolean removed = false; + ArrayList decryptedMembers = new ArrayList<>(builder.getMembersList()); + Iterator membersList = decryptedMembers.iterator(); + + while (membersList.hasNext()) { + if (uuidString.equals(membersList.next().getUuid())) { + membersList.remove(); + removed = true; + } + } + + if (removed) { + return builder.clearMembers() + .addAllMembers(decryptedMembers) + .setVersion(revision) + .build(); + } else { + return group; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index eb74f0c72..eaa7e6ea5 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -498,7 +498,7 @@ public class PushServiceSocket { makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); } - public void retrieveAttachment(long attachmentId, File destination, int maxSizeBytes, ProgressListener listener) + public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener) throws NonSuccessfulResponseCodeException, PushNetworkException { downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener); @@ -590,7 +590,7 @@ public class PushServiceSocket { } } - public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes) + public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes) throws NonSuccessfulResponseCodeException, PushNetworkException { downloadFromCdn(destination, path, maxSizeBytes, null); @@ -874,7 +874,7 @@ public class PushServiceSocket { return new Pair<>(id, digest); } - private void downloadFromCdn(File destination, String path, int maxSizeBytes, ProgressListener listener) + private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { try (FileOutputStream outputStream = new FileOutputStream(destination, true)) { @@ -884,7 +884,7 @@ public class PushServiceSocket { } } - private void downloadFromCdn(OutputStream outputStream, long offset, String path, int maxSizeBytes, ProgressListener listener) + private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(cdnClients, random); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java index 3f89c8e6c..1891aa762 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/UnsupportedDataMessageException.java @@ -1,8 +1,7 @@ package org.whispersystems.signalservice.internal.push; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; /** * Exception that indicates that the data message has a higher required protocol version than the @@ -10,16 +9,16 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup; */ public class UnsupportedDataMessageException extends Exception { - private final int requiredVersion; - private final String sender; - private final int senderDevice; - private final Optional group; + private final int requiredVersion; + private final String sender; + private final int senderDevice; + private final Optional group; public UnsupportedDataMessageException(int currentVersion, int requiredVersion, String sender, int senderDevice, - Optional group) + Optional group) { super("Required version: " + requiredVersion + ", Our version: " + currentVersion); this.requiredVersion = requiredVersion; @@ -40,7 +39,7 @@ public class UnsupportedDataMessageException extends Exception { return senderDevice; } - public Optional getGroup() { + public Optional getGroup() { return group; } } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index e583f5e97..37121b2ba 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -198,6 +198,7 @@ message DataMessage { optional string body = 1; repeated AttachmentPointer attachments = 2; optional GroupContext group = 3; + optional GroupContextV2 groupV2 = 15; optional uint32 flags = 4; optional uint32 expireTimer = 5; optional bytes profileKey = 6; @@ -412,6 +413,12 @@ message GroupContext { optional AttachmentPointer avatar = 5; } +message GroupContextV2 { + optional bytes masterKey = 1; + optional uint32 revision = 2; + optional bytes groupChange = 3; +} + message ContactDetails { message Avatar { optional string contentType = 1; diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java new file mode 100644 index 000000000..b17fc4ec6 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java @@ -0,0 +1,41 @@ +package org.whispersystems.signalservice.internal.groupsv2; + +import com.google.protobuf.ByteString; + +import org.junit.Test; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.zkgroup.util.UUIDUtil; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +public final class DecryptedGroupUtilTest { + + @Test + public void can_extract_uuid_from_decrypted_member() { + UUID uuid = UUID.randomUUID(); + DecryptedMember decryptedMember = DecryptedMember.newBuilder() + .setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid))) + .build(); + + UUID parsed = DecryptedGroupUtil.toUuid(decryptedMember); + + assertEquals(uuid, parsed); + } + + @Test + public void can_extract_editor_uuid_from_decrypted_group_change() { + UUID uuid = UUID.randomUUID(); + ByteString editor = ByteString.copyFrom(UUIDUtil.serialize(uuid)); + DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder() + .setEditor(editor) + .build(); + + UUID parsed = DecryptedGroupUtil.editorUuid(groupChange); + + assertEquals(uuid, parsed); + } + +}