From b14eddefc95c620b2e258c51e5c2405a4f325ded Mon Sep 17 00:00:00 2001 From: Adam Mork Date: Fri, 10 May 2024 05:13:33 -0700 Subject: [PATCH 01/20] Add payment enclave measurements for v6.0.0 --- .../payments/MobileCoinMainNetConfig.java | 37 ++++++------------- .../payments/MobileCoinTestNetConfig.java | 21 ++++++----- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinMainNetConfig.java b/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinMainNetConfig.java index 3831acb4ad..6c801900d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinMainNetConfig.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinMainNetConfig.java @@ -71,38 +71,23 @@ public MobileCoinMainNetConfig(@NonNull SignalServiceAccountManager signalServic try { Set trustRoots = getTrustRoots(R.raw.signal_mobilecoin_authority); ClientConfig config = new ClientConfig(); - VerifierFactory verifierFactory = new VerifierFactory(// ~August 10th, 2022 - new ServiceConfig( - "d6e54e43c368f0fa2c5f13361afd303ee8f890424e99bd6c367f6164b5fff1b5", - "3e9bf61f3191add7b054f0e591b62f832854606f6594fd63faef1e2aedec4021", - "92fb35d0f603ceb5eaf2988b24a41d4a4a83f8fb9cd72e67c3bc37960d864ad6", - "3d6e528ee0574ae3299915ea608b71ddd17cbe855d4f5e1c46df9b0d22b04cdb", - new String[] { "INTEL-SA-00334", "INTEL-SA-00615" } - ), - // ~November 1, 2022 - new ServiceConfig( - "207c9705bf640fdb960034595433ee1ff914f9154fbe4bc7fc8a97e912961e5c", - "3370f131b41e5a49ed97c4188f7a976461ac6127f8d222a37929ac46b46d560e", - "dca7521ce4564cc2e54e1637e533ea9d1901c2adcbab0e7a41055e719fb0ff9d", - "fd4c1c82cca13fa007be15a4c90e2b506c093b21c2e7021a055cbb34aa232f3f", - new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } - ), - // ~December 15, 2022 - new ServiceConfig( - "e35bc15ee92775029a60a715dca05d310ad40993f56ad43bca7e649ccc9021b5", - "a8af815564569aae3558d8e4e4be14d1bcec896623166a10494b4eaea3e1c48c", - "8c80a2b95a549fa8d928dd0f0771be4f3d774408c0f98bf670b1a2c390706bf3", - "da209f4b24e8f4471bd6440c4e9f1b3100f1da09e2836d236e285b274901ed3b", - new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } - ), - // ~May 30, 2023 + VerifierFactory verifierFactory = new VerifierFactory(// ~May 30, 2023 new ServiceConfig( "cd86d300c78f74ec23558cdaf734f90dd3e1bcdf8ae43fc827c6b4734ccb8862", "7d10f5e72cacc87a6027b2be42ed4a74a6370a03c3476be754933eb18c404b0b", "1dee8e2e98b7dc684506991d62856b2e572a0c23f5a7d698086e62f08fb997cc", "e94f6e6557b3fb85b27d804e2d005ee14a564cc50fc477797f2e5f9984b0bd79", new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } - )); + ), + // ~May 9, 2024 + new ServiceConfig( + "82c14d06951a2168763c8ddb9c34174f7d2059564146650661da26ab62224b8a", + "34881106254a626842fa8557e27d07cdf863083e9e6f888d5a492a456720916f", + "2494f1542f30a6962707d0bf2aa6c8c08d7bed35668c9db1e5c61d863a0176d1", + "2f542dcd8f682b72e8921d87e06637c16f4aa4da27dce55b561335326731fa73", + new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } + ) + ); config.logAdapter = new MobileCoinLogAdapter(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java b/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java index 1075e6566e..d60dc43361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java @@ -59,22 +59,23 @@ public MobileCoinTestNetConfig(@NonNull SignalServiceAccountManager signalServic try { Set trustRoots = getTrustRoots(R.raw.signal_mobilecoin_authority); ClientConfig config = new ClientConfig(); - VerifierFactory verifierFactory = new VerifierFactory(// ~January 27, 2023 - new ServiceConfig( - "4f3879bfffb7b9f86a33086202b6120a32da0ca159615fbbd6fbac6aa37bbf02", - "16d73984c2d2712156135ab69987ca78aca67a2cf4f0f2287ea584556f9d223a", - "23ececb2482e3b1d9e284502e2beb65ae76492f2791f3bfef50852ee64b883c3", - "f52b3dc018195eae42f543e64e976c818c06672b5489746e2bf74438d488181b", - new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } - ), - // ~May 30, 2023 + VerifierFactory verifierFactory = new VerifierFactory(// ~May 30, 2023 new ServiceConfig( "5341c6702a3312243c0f049f87259352ff32aa80f0f6426351c3dd063d817d7a", "248356aa0d3431abc45da1773cfd6191a4f2989a4a99da31f450bd7c461e312b", "b61188a6c946557f32e612eff5615908abd1b72ec11d8b7070595a92d4abbbf1", "ac292a1ad27c0338a5159d5fab2bed3917ea144536cb13b5c1226d09a2fbc648", new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } - )); + ), + // ~May 9, 2024 + new ServiceConfig( + "ae7930646f37e026806087d2a3725d3f6d75a8e989fb320e6ecb258eb829057a", + "4a5daa23db5efa4b18071291cfa24a808f58fb0cedce7da5de804b011e87cfde", + "065b1e17e95f2c356d4d071d434cea7eb6b95bc797f94954146736efd47057a7", + "44de03c2ba34c303e6417480644f9796161eacbe5af4f2092e413b4ebf5ccf6a", + new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } + ) + ); config.logAdapter = new MobileCoinLogAdapter(); config.fogView = new ClientConfig.Service().withTrustRoots(trustRoots) From c3c743fbb8ce3b11f3c7d629ed15e4ba779609f7 Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Mon, 13 May 2024 10:45:00 -0700 Subject: [PATCH 02/20] Update camera permission UI in media. --- .../securesms/DeviceActivity.java | 5 +- .../avatar/picker/AvatarPickerFragment.kt | 29 +++--- .../ConversationSettingsFragment.kt | 9 +- .../v2/ConversationActivityResultContracts.kt | 28 +++--- .../ConversationListFragment.java | 21 +++-- .../securesms/mediasend/CameraXFragment.java | 90 +++++++++++++++++-- .../mediasend/CameraXVideoCaptureHelper.java | 14 +-- .../mediasend/v2/MediaSelectionNavigator.kt | 21 +++-- .../v2/gallery/MediaGalleryFragment.kt | 15 +++- .../transfer/PaymentsTransferFragment.java | 13 +-- .../stories/landing/StoriesLandingFragment.kt | 23 ++--- .../verify/VerifyIdentityFragment.kt | 5 +- .../main/res/drawable/permission_camera.xml | 23 +++++ app/src/main/res/layout/camerax_fragment.xml | 39 ++++++++ app/src/main/res/values/strings.xml | 32 +++++++ 15 files changed, 286 insertions(+), 81 deletions(-) create mode 100644 app/src/main/res/drawable/permission_camera.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index 4333ca36c2..575b665961 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -131,14 +131,15 @@ public void onClick(View v) { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getSupportFragmentManager()) .onAllGranted(() -> { getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, deviceAddFragment) .addToBackStack(null) .commitAllowingStateLoss(); }) - .onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show()) + .onAnyDenied(() -> Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show()) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt index e2b7b4fce7..7b95255e1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.groups.ParcelableGroupId import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.ViewUtil @@ -222,18 +223,22 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) { @Suppress("DEPRECATION") private fun openCameraCapture() { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .onAllGranted { - val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) - startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) - } - .onAnyDenied { - Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT) - .show() - } - .execute() + if (CameraXUtil.isSupported()) { + val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { + val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() } + .execute() + } } @Suppress("DEPRECATION") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 72e6123036..27419feb2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentD import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository import org.thoughtcrime.securesms.nicknames.NicknameActivity import org.thoughtcrime.securesms.permissions.Permissions @@ -420,14 +421,16 @@ class ConversationSettingsFragment : DSLSettingsFragment( .setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story) .setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() } .show() + } else if (CameraXUtil.isSupported()) { + addToGroupStoryDelegate.addToStory(state.recipient.id) } else { Permissions.with(this@ConversationSettingsFragment) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) .onAllGranted { addToGroupStoryDelegate.addToStory(state.recipient.id) } - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } .execute() } }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt index 7f27799662..36a8efecbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.maps.PlacePickerActivity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.RecipientId @@ -74,17 +75,22 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat } fun launchCamera(recipientId: RecipientId, isReply: Boolean) { - Permissions.with(fragment) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(fragment.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted { - cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) - fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) - } - .onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) + fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) + } else { + Permissions.with(fragment) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(fragment.getString(R.string.CameraXFragment_allow_access_camera), fragment.getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(fragment.getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, fragment.parentFragmentManager) + .onAllGranted { + cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) + fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) + } + .onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } fun launchMediaEditor(mediaList: List, recipientId: RecipientId, text: CharSequence?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 45b71dd741..67a1af9a09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -144,6 +144,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity; import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder; import org.thoughtcrime.securesms.main.SearchBinder; +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity; import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; @@ -392,14 +393,18 @@ public boolean canStartNestedScroll() { fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class))); cameraFab.setOnClickListener(v -> { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted(() -> startActivity(MediaSelectionActivity.camera(requireContext()))) - .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show()) - .execute(); + if (CameraXUtil.isSupported()) { + startActivity(MediaSelectionActivity.camera(requireContext())); + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAllGranted(() -> startActivity(MediaSelectionActivity.camera(requireContext()))) + .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show()) + .execute(); + } }); initializeViewModel(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 078a0f04da..5c67fad673 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.mediasend; +import android.Manifest; import android.animation.Animator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.os.Build; @@ -24,6 +24,8 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.RotateAnimation; import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,6 +39,7 @@ import androidx.core.content.ContextCompat; import com.bumptech.glide.Glide; +import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import org.signal.core.util.Stopwatch; @@ -57,6 +60,8 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.MediaConstraints; +import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -65,6 +70,7 @@ import java.io.FileDescriptor; import java.io.IOException; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -72,6 +78,8 @@ import io.reactivex.rxjava3.disposables.Disposable; import kotlin.Unit; +import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment; + /** * Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be * preferred whenever possible. @@ -98,6 +106,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { private CameraXModePolicy cameraXModePolicy; private CameraScreenBrightnessController cameraScreenBrightnessController; private boolean isMediaSelected; + private View missingPermissionsContainer; + private TextView missingPermissionsText; + private MaterialButton allowAccessButton; private final Executor qrAnalysisExecutor = Executors.newSingleThreadExecutor(); private final QrProcessor qrProcessor = new QrProcessor(); @@ -149,13 +160,18 @@ public void onAttach(@NonNull Context context) { @SuppressLint("MissingPermission") @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - this.cameraParent = view.findViewById(R.id.camerax_camera_parent); + this.cameraParent = view.findViewById(R.id.camerax_camera_parent); + + this.previewView = view.findViewById(R.id.camerax_camera); + this.controlsContainer = view.findViewById(R.id.camerax_controls_container); + this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(), + controller.getMediaConstraints(), + requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); + this.missingPermissionsContainer = view.findViewById(R.id.missing_permissions_container); + this.missingPermissionsText = view.findViewById(R.id.missing_permissions_text); + this.allowAccessButton = view.findViewById(R.id.allow_access_button); - this.previewView = view.findViewById(R.id.camerax_camera); - this.controlsContainer = view.findViewById(R.id.camerax_controls_container); - this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(), - controller.getMediaConstraints(), - requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); + checkPermissions(requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName()); @@ -218,6 +234,9 @@ public void onResume() { cameraController.bindToLifecycle(getViewLifecycleOwner(), () -> Log.d(TAG, "Camera init complete from onResume")); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + if (hasCameraPermission()) { + missingPermissionsContainer.setVisibility(View.GONE); + } } @Override @@ -259,6 +278,61 @@ public void onAnimationEnd(Animator animation) { }); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } + + private void checkPermissions(boolean includeAudio) { + if (hasCameraPermission()) { + missingPermissionsContainer.setVisibility(View.GONE); + } else { + boolean hasAudioPermission = Permissions.hasAll(requireContext(), Manifest.permission.RECORD_AUDIO); + missingPermissionsContainer.setVisibility(View.VISIBLE); + int textResId = (!includeAudio || hasAudioPermission) ? R.string.CameraXFragment_to_capture_photos_and_video_allow_camera : R.string.CameraXFragment_to_capture_photos_and_video_allow_camera_microphone; + missingPermissionsText.setText(textResId); + allowAccessButton.setOnClickListener(v -> requestPermissions(includeAudio)); + } + } + + private void requestPermissions(boolean includeAudio) { + if (includeAudio) { + Permissions.with(this) + .request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + .ifNecessary() + .onSomeGranted(permissions -> { + if (permissions.contains(Manifest.permission.CAMERA)) { + missingPermissionsContainer.setVisibility(View.GONE); + } + }) + .onSomePermanentlyDenied(deniedPermissions -> { + if (deniedPermissions.containsAll(List.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) { + showPermissionFragment(R.string.CameraXFragment_allow_access_camera_microphone, R.string.CameraXFragment_to_capture_photos_videos).show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } else if (deniedPermissions.contains(Manifest.permission.CAMERA)) { + showPermissionFragment(R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos).show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } + }) + .onSomeDenied(deniedPermissions -> { + if (deniedPermissions.contains(Manifest.permission.CAMERA)) { + Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show(); + } + }) + .execute(); + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted (() -> missingPermissionsContainer.setVisibility(View.GONE)) + .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show()) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager()) + .execute(); + } + } + + private boolean hasCameraPermission() { + return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA); + } + private void onOrientationChanged() { int layout = R.layout.camera_controls_portrait; @@ -356,7 +430,7 @@ private void initControls() { selfieFlash = requireView().findViewById(R.id.camera_selfie_flash); captureButton.setOnClickListener(v -> { - if (cameraController.isInitialized()) { + if (hasCameraPermission() && cameraController.isInitialized()) { captureButton.setEnabled(false); flipButton.setEnabled(false); flashButton.setEnabled(false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 93fce43023..e6ce4d06b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -118,13 +118,17 @@ public void accept(VideoRecordEvent videoRecordEvent) { public void onVideoCaptureStarted() { Log.d(TAG, "onVideoCaptureStarted"); - if (canRecordAudio()) { + if (canUseCamera() && canRecordAudio()) { beginCameraRecording(); - } else { + } else if (!canRecordAudio()) { displayAudioRecordingPermissionsDialog(); } } + private boolean canUseCamera() { + return Permissions.hasAll(fragment.requireContext(), Manifest.permission.CAMERA); + } + private boolean canRecordAudio() { return Permissions.hasAll(fragment.requireContext(), Manifest.permission.RECORD_AUDIO); } @@ -133,9 +137,9 @@ private void displayAudioRecordingPermissionsDialog() { Permissions.with(fragment) .request(Manifest.permission.RECORD_AUDIO) .ifNecessary() - .withRationaleDialog(fragment.getString(R.string.ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound), R.drawable.ic_mic_solid_24) - .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video)) - .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_recording_permissions_to_capture_video, Toast.LENGTH_LONG).show()) + .withRationaleDialog(fragment.getString(R.string.CameraXFragment_allow_access_microphone), fragment.getString(R.string.CameraXFragment_to_capture_videos_with_sound), R.drawable.ic_mic_24) + .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video), null, R.string.CameraXFragment_allow_access_microphone, R.string.CameraXFragment_to_capture_videos, fragment.getParentFragmentManager()) + .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.CameraXFragment_signal_needs_microphone_access_video, Toast.LENGTH_LONG).show()) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt index c55efd9858..bca57bf84e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt @@ -5,6 +5,7 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.navigation.NavController import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -33,14 +34,18 @@ class MediaSelectionNavigator( fun Fragment.requestPermissionsForCamera( onGranted: () -> Unit ) { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted(onGranted) - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + onGranted() + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAllGranted(onGranted) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } fun Fragment.requestPermissionsForGallery( diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt index 916966cb22..b1353dcf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.mediasend.v2.gallery +import android.Manifest import android.os.Bundle import android.view.View import android.widget.Toast @@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaRepository +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.Material3OnScrollHelper @@ -94,7 +96,18 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { if (callbacks.isCameraEnabled()) { binding.mediaGalleryToolbar.setOnMenuItemClickListener { item -> if (item.itemId == R.id.action_camera) { - callbacks.onNavigateToCamera() + if (CameraXUtil.isSupported()) { + callbacks.onNavigateToCamera() + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { callbacks.onNavigateToCamera() } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } true } else { false diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java index 3ead250b21..6cc5d179e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java @@ -96,22 +96,13 @@ private void scanQrCode() { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) - .onAnyPermanentlyDenied(this::onCameraPermissionPermanentlyDenied) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_paymentsTransfer_to_paymentsScanQr)) .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera, Toast.LENGTH_LONG).show()) .execute(); } - private void onCameraPermissionPermanentlyDenied() { - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.Permissions_permission_required) - .setMessage(R.string.PaymentsTransferFragment__signal_needs_the_camera_permission_to_capture_qr_code_go_to_settings) - .setPositiveButton(R.string.PaymentsTransferFragment__settings, (dialog, which) -> requireActivity().startActivity(Permissions.getApplicationSettingsIntent(requireContext()))) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - @Override @SuppressWarnings("deprecation") public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index b4ec71b525..10ec1b208e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.events.ReminderUpdateEvent import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder import org.thoughtcrime.securesms.main.SearchBinder +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity @@ -224,16 +225,18 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l }) cameraFab.setOnClickListener { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted { - startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) - } - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } viewModel.state.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt index d1e00399ee..824f173847 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -106,7 +106,8 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted { childFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom, R.anim.slide_from_bottom, R.anim.slide_to_top) @@ -114,7 +115,7 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen .addToBackStack(null) .commitAllowingStateLoss() } - .onAnyDenied { Toast.makeText(requireContext(), R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show() } + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/res/drawable/permission_camera.xml b/app/src/main/res/drawable/permission_camera.xml new file mode 100644 index 0000000000..f42854ff6a --- /dev/null +++ b/app/src/main/res/drawable/permission_camera.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/layout/camerax_fragment.xml b/app/src/main/res/layout/camerax_fragment.xml index 2073853d6e..a0b42eb5d5 100644 --- a/app/src/main/res/layout/camerax_fragment.xml +++ b/app/src/main/res/layout/camerax_fragment.xml @@ -39,4 +39,43 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="@id/camerax_camera_parent" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2addf51860..9280df226f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,6 +225,38 @@ Capture Change camera Open gallery + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Recent contacts From b4a8f01980705743565ae2927220f06a06283b76 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 13 May 2024 16:00:45 -0400 Subject: [PATCH 03/20] Include message timestamp in local send timings. --- .../securesms/database/LocalMetricsDatabase.kt | 2 +- .../securesms/database/model/LocalMetricsEvent.kt | 6 ++++-- .../securesms/jobs/IndividualSendJob.java | 2 +- .../securesms/jobs/PushGroupSendJob.java | 2 ++ .../thoughtcrime/securesms/util/LocalMetrics.kt | 13 +++++++++++-- .../securesms/util/SignalLocalMetrics.java | 14 +++++++++++++- 6 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt index 437771e594..6d464aea5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LocalMetricsDatabase.kt @@ -136,7 +136,7 @@ class LocalMetricsDatabase private constructor( put(EVENT_ID, event.eventId) put(EVENT_NAME, event.eventName) put(SPLIT_NAME, split.name) - put(DURATION, event.timeunit.convert(split.duration, TimeUnit.NANOSECONDS)) + put(DURATION, event.timeUnit.convert(split.duration, TimeUnit.NANOSECONDS)) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/LocalMetricsEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/LocalMetricsEvent.kt index e540cabff1..3a8552be99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/LocalMetricsEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/LocalMetricsEvent.kt @@ -10,10 +10,12 @@ data class LocalMetricsEvent( val eventId: String, val eventName: String, val splits: MutableList, - val timeunit: TimeUnit + val timeUnit: TimeUnit, + val extraLabel: String? = null ) { override fun toString(): String { - return "[$eventName] total: ${splits.sumOf { it.duration }.fractionalMillis(timeunit)} | ${splits.map { it.toString() }.joinToString(", ")}" + val extra = extraLabel?.let { "[$extraLabel]" } ?: "" + return "[$eventName]$extra total: ${splits.sumOf { it.duration }.fractionalMillis(timeUnit)} | ${splits.map { it.toString() }.joinToString(", ")}" } private fun Long.fractionalMillis(timeunit: TimeUnit): String { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java index 5cf445bdba..c45e9c2ae2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -323,7 +323,7 @@ private boolean deliver(OutgoingMessage message, MessageRecord originalEditedMes SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false); return syncAccess.isPresent(); } else { - SignalLocalMetrics.IndividualMessageSend.onDeliveryStarted(messageId); + SignalLocalMetrics.IndividualMessageSend.onDeliveryStarted(messageId, message.getSentTimeMillis()); SendMessageResult result = messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, new MetricEventListener(messageId), message.isUrgent(), messageRecipient.getNeedsPniSignature()); SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), message.isUrgent()); 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 577c2f1efe..c478c57489 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -186,6 +186,8 @@ public void onPushSend() Set existingNetworkFailures = new HashSet<>(message.getNetworkFailures()); Set existingIdentityMismatches = new HashSet<>(message.getIdentityKeyMismatches()); + SignalLocalMetrics.GroupMessageSend.setSentTimestamp(messageId, message.getSentTimeMillis()); + ApplicationDependencies.getJobManager().cancelAllInQueue(TypingSendJob.getQueue(threadId)); if (database.isSent(messageId)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt index e8aa12119e..0ac6550153 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt @@ -56,7 +56,7 @@ object LocalMetrics { eventId = id, eventName = name, splits = mutableListOf(), - timeunit = timeunit + timeUnit = timeunit ) lastSplitTimeById[id] = time } @@ -76,12 +76,21 @@ object LocalMetrics { val splitDoesNotExist: Boolean = eventsById[id]?.splits?.none { it.name == split } ?: true if (lastTime != null && splitDoesNotExist) { val event = eventsById[id] - event?.splits?.add(LocalMetricsSplit(split, time - lastTime, event.timeunit)) + event?.splits?.add(LocalMetricsSplit(split, time - lastTime, event.timeUnit)) lastSplitTimeById[id] = time } } } + fun setLabel(id: String, label: String) { + executor.execute { + val event = eventsById[id] + if (event != null) { + eventsById[id] = event.copy(extraLabel = label) + } + } + } + /** * Marks a split for an event. Updates the last time, so future splits will have duration relative to this event. * diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java index dcb88431e7..0f68b11ab7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java @@ -154,8 +154,13 @@ public static void onJobStarted(long messageId) { split(messageId, SPLIT_JOB_ENQUEUE); } - public static void onDeliveryStarted(long messageId) { + public static void onDeliveryStarted(long messageId, long sentTimestamp) { split(messageId, SPLIT_JOB_PRE_NETWORK); + + String splitId = ID_MAP.get(messageId); + if (splitId != null) { + LocalMetrics.getInstance().setLabel(splitId, String.valueOf(sentTimestamp)); + } } public static void onMessageEncrypted(long messageId) { @@ -337,6 +342,13 @@ public static void onJobStarted(long messageId) { split(messageId, SPLIT_JOB_ENQUEUE); } + public static void setSentTimestamp(long messageId, long sentTimestamp) { + String splitId = ID_MAP.get(messageId); + if (splitId != null) { + LocalMetrics.getInstance().setLabel(splitId, String.valueOf(sentTimestamp)); + } + } + public static void onSenderKeyStarted(long messageId) { split(messageId, SPLIT_JOB_PRE_NETWORK); } From 68ced18ea1e605e59508ebce195876b212d651ee Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Mon, 13 May 2024 17:33:42 -0400 Subject: [PATCH 04/20] Fleshed out session management in registration v2. --- .../v2/data/RegistrationRepository.kt | 72 +++++++++++---- .../data/network/RegistrationSessionResult.kt | 89 +++++++++++++++++++ .../v2/ui/RegistrationV2ViewModel.kt | 59 +++++++++++- .../api/registration/RegistrationApi.kt | 9 ++ 4 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index f016e5c3a0..80279e706c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -42,6 +42,9 @@ import org.thoughtcrime.securesms.registration.PushChallengeRequest import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.VerifyAccountRepository import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener @@ -254,13 +257,19 @@ object RegistrationRepository { } /** - * Asks the service to send a verification code through one of our supported channels (SMS, phone call). - * This requires two or more network calls: - * 1. Create (or reuse) a session. - * 2. (Optional) If the session has any proof requirements ("challenges"), the user must solve them and submit the proof. - * 3. Once the service responds we are allowed to, we request the verification code. + * Validates a session ID. + */ + suspend fun validateSession(context: Context, sessionId: String, e164: String, password: String): RegistrationSessionCheckResult = + withContext(Dispatchers.IO) { + val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + val registrationSessionResult = api.getRegistrationSessionStatus(sessionId) + return@withContext RegistrationSessionCheckResult.from(registrationSessionResult) + } + + /** + * Initiates a new registration session on the service. */ - suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): VerificationCodeRequestResult = + suspend fun createSession(context: Context, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionCreationResult = withContext(Dispatchers.IO) { val fcmToken: String? = FcmUtil.getToken(context).orElse(null) val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi @@ -270,17 +279,46 @@ object RegistrationRepository { } else { createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc) } - val session = registrationSessionResult.successOrThrow() - val sessionId = session.body.id - SignalStore.registrationValues().sessionId = sessionId - SignalStore.registrationValues().sessionE164 = e164 - if (!session.body.allowedToRequestCode) { - val challenges = session.body.requestedInformation.joinToString() - Log.w(TAG, "Not allowed to request code! Remaining challenges: $challenges") - return@withContext VerificationCodeRequestResult.from(registrationSessionResult) + val result = RegistrationSessionCreationResult.from(registrationSessionResult) + if (result is RegistrationSessionCreationResult.Success) { + SignalStore.registrationValues().sessionId = result.getMetadata().body.id + SignalStore.registrationValues().sessionE164 = e164 } + + return@withContext result + } + + /** + * Validates an existing session, if its ID is provided. If the session is expired/invalid, or none is provided, it will attempt to initiate a new session. + */ + suspend fun createOrValidateSession(context: Context, sessionId: String?, e164: String, password: String, mcc: String?, mnc: String?): RegistrationSessionResult { + if (sessionId != null) { + val sessionValidationResult = validateSession(context, sessionId, e164, password) + when (sessionValidationResult) { + is RegistrationSessionCheckResult.Success -> return sessionValidationResult + is RegistrationSessionCheckResult.UnknownError -> { + Log.w(TAG, "Encountered error when validating existing session.", sessionValidationResult.getCause()) + return sessionValidationResult + } + + is RegistrationSessionCheckResult.SessionNotFound -> { + Log.i(TAG, "Current session is invalid or has expired. Must create new one.") + // fall through to creation + } + } + } + return createSession(context, e164, password, mcc, mnc) + } + + /** + * Asks the service to send a verification code through one of our supported channels (SMS, phone call). + */ + suspend fun requestSmsCode(context: Context, sessionId: String, e164: String, password: String, mode: Mode = Mode.SMS_WITHOUT_LISTENER): VerificationCodeRequestResult = + withContext(Dispatchers.IO) { + val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + // TODO [regv2]: support other verification code [Mode] options - if (mode == Mode.PHONE_CALL) { + val codeRequestResult = if (mode == Mode.PHONE_CALL) { // TODO [regv2] val notImplementedError = NotImplementedError() Log.w(TAG, "Not yet implemented!", notImplementedError) @@ -289,7 +327,7 @@ object RegistrationRepository { api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported) } - return@withContext VerificationCodeRequestResult.from(registrationSessionResult) + return@withContext VerificationCodeRequestResult.from(codeRequestResult) } /** @@ -362,7 +400,7 @@ object RegistrationRepository { } } - private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult = + suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult = withContext(Dispatchers.IO) { // TODO [regv2]: do not use event bus nor latch val subscriber = PushTokenChallengeSubscriber() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt new file mode 100644 index 0000000000..64025ed0ee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegistrationSessionResult.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.data.network + +import org.signal.core.util.logging.Log +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException +import org.whispersystems.signalservice.api.push.exceptions.NotFoundException +import org.whispersystems.signalservice.api.push.exceptions.RateLimitException +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse + +sealed class RegistrationSessionResult(cause: Throwable?) : RegistrationResult(cause) + +interface SessionMetadataHolder { + fun getMetadata(): RegistrationSessionMetadataResponse +} + +sealed class RegistrationSessionCreationResult(cause: Throwable?) : RegistrationSessionResult(cause) { + companion object { + + private val TAG = Log.tag(RegistrationSessionResult::class.java) + + @JvmStatic + fun from(networkResult: NetworkResult): RegistrationSessionCreationResult { + return when (networkResult) { + is NetworkResult.Success -> { + Success(networkResult.result) + } + is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) + is NetworkResult.NetworkError -> UnknownError(networkResult.exception) + is NetworkResult.StatusCodeError -> { + when (val cause = networkResult.exception) { + is RateLimitException -> RateLimited(cause) + is MalformedRequestException -> MalformedRequest(cause) + else -> if (networkResult.code == 422) { + ServerUnableToParse(cause) + } else { + UnknownError(cause) + } + } + } + } + } + } + + class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCreationResult(null), SessionMetadataHolder { + override fun getMetadata(): RegistrationSessionMetadataResponse { + return metadata + } + } + + class RateLimited(cause: Throwable) : RegistrationSessionCreationResult(cause) + class ServerUnableToParse(cause: Throwable) : RegistrationSessionCreationResult(cause) + class MalformedRequest(cause: Throwable) : RegistrationSessionCreationResult(cause) + class UnknownError(cause: Throwable) : RegistrationSessionCreationResult(cause) +} + +sealed class RegistrationSessionCheckResult(cause: Throwable?) : RegistrationSessionResult(cause) { + companion object { + fun from(networkResult: NetworkResult): RegistrationSessionCheckResult { + return when (networkResult) { + is NetworkResult.Success -> { + Success(networkResult.result) + } + + is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable) + is NetworkResult.NetworkError -> UnknownError(networkResult.exception) + is NetworkResult.StatusCodeError -> { + when (val cause = networkResult.exception) { + is NotFoundException -> SessionNotFound(cause) + else -> UnknownError(cause) + } + } + } + } + } + + class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCheckResult(null), SessionMetadataHolder { + override fun getMetadata(): RegistrationSessionMetadataResponse { + return metadata + } + } + + class SessionNotFound(cause: Throwable) : RegistrationSessionCheckResult(cause) + class UnknownError(cause: Throwable) : RegistrationSessionCheckResult(cause) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index 1cdd13c29b..af9924725d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob @@ -29,6 +30,9 @@ import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository import org.thoughtcrime.securesms.registration.v2.data.network.BackupAuthCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.v2.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.AttemptsExhausted import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.ChallengeRequired @@ -49,6 +53,7 @@ import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.internal.push.LockedException +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import java.io.IOException /** @@ -173,12 +178,63 @@ class RegistrationV2ViewModel : ViewModel() { is BackupAuthCheckResult.SuccessWithoutCredentials -> Log.d(TAG, "No local SVR auth credentials could be found and/or validated.") } - val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc) + val validSession = getOrCreateValidSession(context) ?: return@launch + + if (!validSession.body.allowedToRequestCode) { + val challenges = validSession.body.requestedInformation.joinToString() + Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") + handleSessionStateResult(context, ChallengeRequired(validSession.body.requestedInformation)) + return@launch + } + + val codeRequestResponse = RegistrationRepository.requestSmsCode(context, validSession.body.id, e164, password) handleSessionStateResult(context, codeRequestResponse) } } + private suspend fun getOrCreateValidSession(context: Context): RegistrationSessionMetadataResponse? { + val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!") + val mccMncProducer = MccMncProducer(context) + + val existingSessionId = store.value.sessionId + val sessionResult: RegistrationSessionResult = RegistrationRepository.createOrValidateSession(context, existingSessionId, e164, password, mccMncProducer.mcc, mccMncProducer.mnc) + when (sessionResult) { + is RegistrationSessionCheckResult.Success -> { + val metadata = sessionResult.getMetadata() + val newSessionId = metadata.body.id + if (newSessionId.isNotNullOrBlank() && newSessionId != existingSessionId) { + store.update { + it.copy( + sessionId = newSessionId + ) + } + } + return metadata + } + is RegistrationSessionCreationResult.Success -> { + val metadata = sessionResult.getMetadata() + val newSessionId = metadata.body.id + if (newSessionId.isNotNullOrBlank() && newSessionId != existingSessionId) { + store.update { + it.copy( + sessionId = newSessionId + ) + } + } + return metadata + } + is RegistrationSessionCheckResult.SessionNotFound -> Log.w(TAG, "This should be impossible to reach at this stage; it should have been handled in RegistrationRepository.", sessionResult.getCause()) + is RegistrationSessionCheckResult.UnknownError -> Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) + is RegistrationSessionCreationResult.MalformedRequest -> Log.i(TAG, "Malformed request error occurred while creating registration session.", sessionResult.getCause()) + is RegistrationSessionCreationResult.RateLimited -> Log.i(TAG, "Rate limit occurred while creating registration session.", sessionResult.getCause()) + is RegistrationSessionCreationResult.ServerUnableToParse -> Log.i(TAG, "Server unable to parse request for creating registration session.", sessionResult.getCause()) + is RegistrationSessionCreationResult.UnknownError -> Log.i(TAG, "Unknown error occurred while checking registration session.", sessionResult.getCause()) + } + setInProgress(false) + return null + } + fun submitCaptchaToken(context: Context) { val e164 = getCurrentE164() ?: throw IllegalStateException("TODO") val sessionId = store.value.sessionId ?: throw IllegalStateException("TODO") @@ -216,6 +272,7 @@ class RegistrationV2ViewModel : ViewModel() { is AttemptsExhausted -> Log.w(TAG, "TODO") is ChallengeRequired -> store.update { + // TODO [regv2] handle push challenge required it.copy( registrationCheckpoint = RegistrationCheckpoint.CHALLENGE_RECEIVED ) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 544c2ff4ae..a3fa4a317a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -30,6 +30,15 @@ class RegistrationApi( } } + /** + * Retrieve current status of a registration session. + */ + fun getRegistrationSessionStatus(sessionId: String): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getSessionStatus(sessionId) + } + } + /** * Submit an FCM token to the service as proof that this is an honest user attempting to register. */ From f570f1f2c4e57c7b45e055ecd975410b3048e56f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 14 May 2024 09:47:21 -0400 Subject: [PATCH 05/20] Initial test implementation of SVR3. --- .../svr/InternalSvrPlaygroundFragment.kt | 2 +- .../svr/InternalSvrPlaygroundState.kt | 3 +- .../svr/InternalSvrPlaygroundViewModel.kt | 19 ++- .../api/SignalServiceAccountManager.java | 5 + .../signalservice/api/kbs/PinHashUtil.kt | 12 +- .../api/svr/SecureValueRecoveryV2.kt | 2 +- .../api/svr/SecureValueRecoveryV3.kt | 155 ++++++++++++++++++ .../internal/push/PushServiceSocket.java | 6 +- .../internal/websocket/LibSignalNetwork.kt | 10 +- 9 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt index 6b195fa557..569beda01b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt @@ -138,7 +138,7 @@ fun SvrPlaygroundScreenDarkTheme() { Surface { SvrPlaygroundScreen( state = InternalSvrPlaygroundState( - options = persistentListOf(SvrImplementation.SVR2) + options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3) ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt index 0f49cd8de1..943f4c7ca5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundState.kt @@ -13,5 +13,6 @@ data class InternalSvrPlaygroundState( enum class SvrImplementation( val title: String ) { - SVR2("SVR2") + SVR2("SVR2"), + SVR3("SVR3") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt index 191ad7deff..b9bbb77957 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt @@ -19,12 +19,13 @@ import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.whispersystems.signalservice.api.svr.SecureValueRecovery +import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3 class InternalSvrPlaygroundViewModel : ViewModel() { private val _state: MutableState = mutableStateOf( InternalSvrPlaygroundState( - options = persistentListOf(SvrImplementation.SVR2) + options = persistentListOf(SvrImplementation.SVR2, SvrImplementation.SVR3) ) ) val state: State = _state @@ -104,6 +105,22 @@ class InternalSvrPlaygroundViewModel : ViewModel() { private fun SvrImplementation.toImplementation(): SecureValueRecovery { return when (this) { SvrImplementation.SVR2 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE) + SvrImplementation.SVR3 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV3(ApplicationDependencies.getLibsignalNetwork().network, TestShareSetStorage()) + } + } + + /** + * Temporary implementation of share set storage. Only useful for testing. + */ + private class TestShareSetStorage : SecureValueRecoveryV3.ShareSetStorage { + private var shareSet: ByteArray? = null + + override fun write(data: ByteArray) { + shareSet = data + } + + override fun read(): ByteArray? { + return shareSet } } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 7c271cbc89..2106095746 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -56,6 +56,7 @@ import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageManifestKey; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; +import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -183,6 +184,10 @@ public SecureValueRecoveryV2 getSecureValueRecoveryV2(String mrEnclave) { return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket); } + public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network, SecureValueRecoveryV3.ShareSetStorage storage) { + return new SecureValueRecoveryV3(network, pushServiceSocket, storage); + } + public WhoAmIResponse getWhoAmI() throws IOException { return this.pushServiceSocket.getWhoAmI(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt index 3e58ca7314..93f188a1c4 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/PinHashUtil.kt @@ -61,15 +61,21 @@ object PinHashUtil { * Takes a user-input PIN string and normalizes it to a standard character set. */ @JvmStatic - fun normalize(pin: String): ByteArray { + fun normalizeToString(pin: String): String { var normalizedPin = pin.trim() if (PinString.allNumeric(normalizedPin)) { normalizedPin = PinString.toArabic(normalizedPin) } - normalizedPin = Normalizer.normalize(normalizedPin, Normalizer.Form.NFKD) + return Normalizer.normalize(normalizedPin, Normalizer.Form.NFKD) + } - return normalizedPin.toByteArray(StandardCharsets.UTF_8) + /** + * Takes a user-input PIN string and normalizes it to a standard character set. + */ + @JvmStatic + fun normalize(pin: String): ByteArray { + return normalizeToString(pin).toByteArray(StandardCharsets.UTF_8) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt index 7f1371dcfa..011dfd47a0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt @@ -90,7 +90,7 @@ class SecureValueRecoveryV2( @Throws(IOException::class) override fun authorization(): AuthCredentials { - return pushServiceSocket.svr2Authorization + return pushServiceSocket.svrAuthorization } override fun toString(): String { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt new file mode 100644 index 0000000000..78b12d4ed9 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.svr + +import org.signal.core.util.logging.Log +import org.signal.libsignal.attest.AttestationFailedException +import org.signal.libsignal.net.EnclaveAuth +import org.signal.libsignal.net.Network +import org.signal.libsignal.net.NetworkException +import org.signal.libsignal.sgxsession.SgxCommunicationFailureException +import org.signal.libsignal.svr.DataMissingException +import org.signal.libsignal.svr.RestoreFailedException +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.kbs.PinHashUtil +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse +import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import java.io.IOException +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException + +/** + * An interface for working with V3 of the Secure Value Recovery service. + */ +class SecureValueRecoveryV3( + private val network: Network, + private val pushServiceSocket: PushServiceSocket, + private val shareSetStorage: ShareSetStorage +) : SecureValueRecovery { + + companion object { + val TAG = Log.tag(SecureValueRecoveryV3::class) + } + + override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession { + return Svr3PinChangeSession(userPin, masterKey) + } + + /** + * Note: Unlike SVR2, there is no concept of "resuming", so this is equivalent to starting a new session. + */ + override fun resumePinChangeSession(userPin: String, masterKey: MasterKey, serializedChangeSession: String): PinChangeSession { + return Svr3PinChangeSession(userPin, masterKey) + } + + override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse { + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) + val shareSet = shareSetStorage.read() ?: return RestoreResponse.ApplicationError(IllegalStateException("No share set found!")) + val enclaveAuth = EnclaveAuth(authorization.username(), authorization.password()) + + return try { + val result = network.svr3().restore(normalizedPin, shareSet, enclaveAuth).get() + val masterKey = MasterKey(result) + RestoreResponse.Success(masterKey, authorization) + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> RestoreResponse.NetworkError(IOException(cause)) // TODO [svr3] Update when we get to IOException + is DataMissingException -> RestoreResponse.Missing + is RestoreFailedException -> RestoreResponse.PinMismatch(1) // TODO [svr3] Get proper API for this + is AttestationFailedException -> RestoreResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> RestoreResponse.ApplicationError(cause) + is IOException -> RestoreResponse.NetworkError(cause) + else -> RestoreResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + return RestoreResponse.ApplicationError(e) + } catch (e: CancellationException) { + return RestoreResponse.ApplicationError(e) + } + } + + override fun restoreDataPostRegistration(userPin: String): RestoreResponse { + val authorization: AuthCredentials = try { + pushServiceSocket.svrAuthorization + } catch (e: NonSuccessfulResponseCodeException) { + return RestoreResponse.ApplicationError(e) + } catch (e: IOException) { + return RestoreResponse.NetworkError(e) + } catch (e: Exception) { + return RestoreResponse.ApplicationError(e) + } + + return restoreDataPreRegistration(authorization, userPin) + } + + /** + * There's no concept of "deleting" data with SVR3. + */ + override fun deleteData(): DeleteResponse { + return DeleteResponse.Success + } + + @Throws(IOException::class) + override fun authorization(): AuthCredentials { + return pushServiceSocket.svrAuthorization + } + + inner class Svr3PinChangeSession( + private val userPin: String, + private val masterKey: MasterKey + ) : PinChangeSession { + override fun execute(): BackupResponse { + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) + val rawAuth = try { + pushServiceSocket.svrAuthorization + } catch (e: NonSuccessfulResponseCodeException) { + return BackupResponse.ApplicationError(e) + } catch (e: IOException) { + return BackupResponse.NetworkError(e) + } catch (e: Exception) { + return BackupResponse.ApplicationError(e) + } + + val enclaveAuth = EnclaveAuth(rawAuth.username(), rawAuth.password()) + + return try { + val result = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get() + shareSetStorage.write(result) + BackupResponse.Success(masterKey, rawAuth) + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> BackupResponse.NetworkError(IOException(cause)) // TODO [svr] Update when we move to IOException + is AttestationFailedException -> BackupResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause) + is IOException -> BackupResponse.NetworkError(cause) + else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + BackupResponse.ApplicationError(e) + } catch (e: CancellationException) { + BackupResponse.ApplicationError(e) + } + } + + override fun serialize(): String { + // There is no "resuming" SVR3, so we don't need to serialize anything + return "" + } + } + + /** + * An interface to allow reading and writing the "share set" to persistent storage. + */ + interface ShareSetStorage { + fun write(data: ByteArray) + fun read(): ByteArray? + } +} 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 af6602b5d3..598402df91 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 @@ -304,7 +304,7 @@ public class PushServiceSocket { private static final String REGISTRATION_PATH = "/v1/registration"; private static final String CDSI_AUTH = "/v2/directory/auth"; - private static final String SVR2_AUTH = "/v2/backup/auth"; + private static final String SVR_AUTH = "/v2/backup/auth"; private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; @@ -485,8 +485,8 @@ public CdsiAuthResponse getCdsiAuth() throws IOException { return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); } - public AuthCredentials getSvr2Authorization() throws IOException { - String body = makeServiceRequest(SVR2_AUTH, "GET", null); + public AuthCredentials getSvrAuthorization() throws IOException { + String body = makeServiceRequest(SVR_AUTH, "GET", null); AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class); return credentials; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt index 6d2134ea3c..f36be5c9be 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetwork.kt @@ -21,7 +21,7 @@ import java.util.function.Consumer /** * Makes Network API more ergonomic to use with Android client types */ -class LibSignalNetwork(private val inner: Network, config: SignalServiceConfiguration) { +class LibSignalNetwork(val network: Network, config: SignalServiceConfiguration) { init { resetSettings(config) } @@ -31,7 +31,7 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur ): ChatService { val username = credentialsProvider?.username ?: "" val password = credentialsProvider?.password ?: "" - return inner.createChatService(username, password) + return network.createChatService(username, password) } fun resetSettings(config: SignalServiceConfiguration) { @@ -40,9 +40,9 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur private fun resetProxy(proxy: SignalProxy?) { if (proxy == null) { - inner.clearProxy() + network.clearProxy() } else { - inner.setProxy(proxy.host, proxy.port) + network.setProxy(proxy.host, proxy.port) } } @@ -54,6 +54,6 @@ class LibSignalNetwork(private val inner: Network, config: SignalServiceConfigur request: CdsiLookupRequest?, tokenConsumer: Consumer ): CompletableFuture? { - return inner.cdsiLookup(username, password, request, tokenConsumer) + return network.cdsiLookup(username, password, request, tokenConsumer) } } From 13bd4a9c7473653aa8039b3c0dab54134c1394fa Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Tue, 14 May 2024 11:12:35 -0400 Subject: [PATCH 06/20] Update regv2 result field name. --- .../v2/data/network/VerificationCodeRequestResult.kt | 6 +++--- .../securesms/registration/v2/ui/RegistrationV2ViewModel.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt index 703648bcad..778d10fe29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt @@ -42,8 +42,8 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu Success( sessionId = networkResult.result.body.id, allowedToRequestCode = networkResult.result.body.allowedToRequestCode, - nextSms = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), - nextCall = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall) + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall) ) } } @@ -92,7 +92,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } } - class Success(val sessionId: String, val allowedToRequestCode: Boolean, val nextSms: Long, val nextCall: Long) : VerificationCodeRequestResult(null) + class Success(val sessionId: String, val allowedToRequestCode: Boolean, val nextSmsTimestamp: Long, val nextCallTimestamp: Long) : VerificationCodeRequestResult(null) class ChallengeRequired(val challenges: List) : VerificationCodeRequestResult(null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index af9924725d..c7d4efc566 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -262,8 +262,8 @@ class RegistrationV2ViewModel : ViewModel() { store.update { it.copy( sessionId = sessionResult.sessionId, - nextSms = sessionResult.nextSms, - nextCall = sessionResult.nextCall, + nextSms = sessionResult.nextSmsTimestamp, + nextCall = sessionResult.nextCallTimestamp, registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED ) } From 0465fdea62f3b3738d25c8805002a7aa90b1e56b Mon Sep 17 00:00:00 2001 From: mtang-signal Date: Tue, 14 May 2024 09:22:24 -0700 Subject: [PATCH 07/20] Update contacts permission UI. --- .../securesms/ContactSelectionListAdapter.kt | 36 +++++ .../ContactSelectionListFragment.java | 144 +++++++++--------- .../securesms/keyvalue/UiHints.java | 9 ++ .../res/drawable/permissions_contact_book.xml | 31 ++++ ...ct_selection_find_contacts_banner_item.xml | 73 +++++++++ .../contact_selection_find_contacts_item.xml | 57 +++++++ .../contact_selection_list_fragment.xml | 65 -------- app/src/main/res/values/strings.xml | 12 ++ 8 files changed, 292 insertions(+), 135 deletions(-) create mode 100644 app/src/main/res/drawable/permissions_contact_book.xml create mode 100644 app/src/main/res/layout/contact_selection_find_contacts_banner_item.xml create mode 100644 app/src/main/res/layout/contact_selection_find_contacts_item.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListAdapter.kt index ab63ad61d1..54c76eaf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListAdapter.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms import android.content.Context import android.view.View import android.widget.TextView +import com.google.android.material.button.MaterialButton import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration import org.thoughtcrime.securesms.contacts.paged.ContactSearchData @@ -24,6 +25,8 @@ class ContactSelectionListAdapter( init { registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item)) registerFactory(InviteToSignalModel::class.java, LayoutFactory({ InviteToSignalViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item)) + registerFactory(FindContactsModel::class.java, LayoutFactory({ FindContactsViewHolder(it, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_item)) + registerFactory(FindContactsBannerModel::class.java, LayoutFactory({ FindContactsBannerViewHolder(it, onClickCallbacks::onDismissFindContactsBannerClicked, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_banner_item)) registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item)) registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header)) registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state)) @@ -46,6 +49,16 @@ class ContactSelectionListAdapter( override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true } + class FindContactsModel : MappingModel { + override fun areItemsTheSame(newItem: FindContactsModel): Boolean = true + override fun areContentsTheSame(newItem: FindContactsModel): Boolean = true + } + + class FindContactsBannerModel : MappingModel { + override fun areItemsTheSame(newItem: FindContactsBannerModel): Boolean = true + override fun areContentsTheSame(newItem: FindContactsBannerModel): Boolean = true + } + class FindByUsernameModel : MappingModel { override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true @@ -86,6 +99,23 @@ class ContactSelectionListAdapter( override fun bind(model: RefreshContactsModel) = Unit } + private class FindContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder(itemView) { + init { + itemView.setOnClickListener { onClickListener() } + } + + override fun bind(model: FindContactsModel) = Unit + } + + private class FindContactsBannerViewHolder(itemView: View, onDismissListener: () -> Unit, onClickListener: () -> Unit) : MappingViewHolder(itemView) { + init { + itemView.findViewById(R.id.no_thanks_button).setOnClickListener { onDismissListener() } + itemView.findViewById(R.id.allow_contacts_button).setOnClickListener { onClickListener() } + } + + override fun bind(model: FindContactsBannerModel) = Unit + } + private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder(itemView) { private val headerTextView: TextView = itemView.findViewById(R.id.section_header) @@ -129,6 +159,8 @@ class ContactSelectionListAdapter( INVITE_TO_SIGNAL("invite-to-signal"), MORE_HEADING("more-heading"), REFRESH_CONTACTS("refresh-contacts"), + FIND_CONTACTS("find-contacts"), + FIND_CONTACTS_BANNER("find-contacts-banner"), FIND_BY_USERNAME("find-by-username"), FIND_BY_PHONE_NUMBER("find-by-phone-number"); @@ -152,6 +184,8 @@ class ContactSelectionListAdapter( ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel() ArbitraryRow.MORE_HEADING -> MoreHeaderModel() ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel() + ArbitraryRow.FIND_CONTACTS -> FindContactsModel() + ArbitraryRow.FIND_CONTACTS_BANNER -> FindContactsBannerModel() ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel() ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel() } @@ -162,6 +196,8 @@ class ContactSelectionListAdapter( fun onNewGroupClicked() fun onInviteToSignalClicked() fun onRefreshContactsClicked() + fun onFindContactsClicked() + fun onDismissFindContactsBannerClicked() fun onFindByPhoneNumberClicked() fun onFindByUsernameClicked() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 4a15618ab8..924c144454 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -70,13 +70,13 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.manage.UsernameRepository; import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.ViewUtil; @@ -125,10 +125,6 @@ public final class ContactSelectionListFragment extends LoggingFragment { private TextView emptyText; private OnContactSelectedListener onContactSelectedListener; private SwipeRefreshLayout swipeRefresh; - private View showContactsLayout; - private Button showContactsButton; - private TextView showContactsDescription; - private ProgressWheel showContactsProgress; private String cursorFilter; private RecyclerView recyclerView; private RecyclerViewFastScroller fastScroller; @@ -223,43 +219,25 @@ public void onActivityCreated(Bundle icicle) { public void onStart() { super.onStart(); - Permissions.with(this) - .request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS) - .ifNecessary() - .onAllGranted(() -> { - if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) { - handleContactPermissionGranted(); - } else { - contactSearchMediator.refresh(); - } - }) - .onAnyDenied(() -> { - requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - - if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) { - contactSearchMediator.refresh(); - } else { - initializeNoContactsPermission(); - } - }) - .execute(); + if (hasContactsPermissions(requireContext()) && !TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) { + handleContactPermissionGranted(); + } else { + requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + contactSearchMediator.refresh(); + } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false); - emptyText = view.findViewById(android.R.id.empty); - recyclerView = view.findViewById(R.id.recycler_view); - swipeRefresh = view.findViewById(R.id.swipe_refresh); - fastScroller = view.findViewById(R.id.fast_scroller); - showContactsLayout = view.findViewById(R.id.show_contacts_container); - showContactsButton = view.findViewById(R.id.show_contacts_button); - showContactsDescription = view.findViewById(R.id.show_contacts_description); - showContactsProgress = view.findViewById(R.id.progress); - chipRecycler = view.findViewById(R.id.chipRecycler); - constraintLayout = view.findViewById(R.id.container); - headerActionView = view.findViewById(R.id.header_action); + emptyText = view.findViewById(android.R.id.empty); + recyclerView = view.findViewById(R.id.recycler_view); + swipeRefresh = view.findViewById(R.id.swipe_refresh); + fastScroller = view.findViewById(R.id.fast_scroller); + chipRecycler = view.findViewById(R.id.chipRecycler); + constraintLayout = view.findViewById(R.id.container); + headerActionView = view.findViewById(R.id.header_action); final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext()); @@ -269,6 +247,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { return true; } + + @Override + public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) { + recyclerView.setAlpha(1f); + } }); contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class); @@ -372,6 +355,19 @@ public void onAdapterListCommitted(int size) { fixedContacts, displayOptions, new ContactSelectionListAdapter.OnContactSelectionClick() { + @Override + public void onDismissFindContactsBannerClicked() { + SignalStore.uiHints().markDismissedContactsPermissionBanner(); + if (onRefreshListener != null) { + onRefreshListener.onRefresh(); + } + } + + @Override + public void onFindContactsClicked() { + requestContactPermissions(); + } + @Override public void onRefreshContactsClicked() { if (onRefreshListener != null) { @@ -498,6 +494,27 @@ public boolean isMulti() { return isMulti; } + private void requestContactPermissions() { + Permissions.with(this) + .request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS) + .ifNecessary() + .onAllGranted(() -> { + recyclerView.setAlpha(0.5f); + if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) { + handleContactPermissionGranted(); + } else { + contactSearchMediator.refresh(); + if (onRefreshListener != null) { + swipeRefresh.setRefreshing(true); + onRefreshListener.onRefresh(); + } + } + }) + .onAnyDenied(() -> contactSearchMediator.refresh()) + .withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts), null, R.string.ContactSelectionListFragment_allow_access_contacts, R.string.ContactSelectionListFragment_to_find_people, getParentFragmentManager()) + .execute(); + } + private void initializeCursor() { recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders)); recyclerView.setAdapter(contactSearchMediator.getAdapter()); @@ -521,28 +538,6 @@ private boolean hideLetterHeaders() { return hasQueryFilter() || shouldDisplayRecents(); } - private void initializeNoContactsPermission() { - swipeRefresh.setVisibility(View.GONE); - - showContactsLayout.setVisibility(View.VISIBLE); - showContactsProgress.setVisibility(View.INVISIBLE); - showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them); - showContactsButton.setVisibility(View.VISIBLE); - - showContactsButton.setOnClickListener(v -> { - Permissions.with(this) - .request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS) - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts)) - .onSomeGranted(permissions -> { - if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) { - handleContactPermissionGranted(); - } - }) - .execute(); - }); - } - public void setQueryFilter(String filter) { if (Objects.equals(filter, this.cursorFilter)) { return; @@ -583,7 +578,6 @@ private void onLoadFinished(int count) { } swipeRefresh.setVisibility(View.VISIBLE); - showContactsLayout.setVisibility(View.GONE); emptyText.setText(R.string.contact_selection_group_activity__no_contacts); boolean useFastScroller = count > 20; @@ -614,12 +608,10 @@ private void handleContactPermissionGranted() { new AsyncTask() { @Override protected void onPreExecute() { - swipeRefresh.setVisibility(View.GONE); - showContactsLayout.setVisibility(View.VISIBLE); - showContactsButton.setVisibility(View.INVISIBLE); - showContactsDescription.setText(R.string.ConversationListFragment_loading); - showContactsProgress.setVisibility(View.VISIBLE); - showContactsProgress.spin(); + if (onRefreshListener != null) { + setRefreshing(true); + onRefreshListener.onRefresh(); + } } @Override @@ -636,14 +628,11 @@ protected Boolean doInBackground(Void... voids) { @Override protected void onPostExecute(Boolean result) { if (result) { - showContactsLayout.setVisibility(View.GONE); - swipeRefresh.setVisibility(View.VISIBLE); reset(); } else { Context context = getContext(); if (context != null) { Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show(); - initializeNoContactsPermission(); } } } @@ -890,6 +879,13 @@ private void smoothScrollChipsToEnd() { return ContactSearchConfiguration.build(builder -> { builder.setQuery(contactSearchState.getQuery()); + if (newConversationCallback != null && + !hasContactsPermissions(requireContext()) && + !SignalStore.uiHints().getDismissedContactsPermissionBanner() && + !hasQuery) { + builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS_BANNER.getCode()); + } + if (newConversationCallback != null && !hasQuery) { builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode()); } @@ -946,7 +942,7 @@ private void smoothScrollChipsToEnd() { builder.username(newRowMode); } - if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) { + if ((newCallCallback != null || newConversationCallback != null)) { addMoreSection(builder); builder.withEmptyState(emptyBuilder -> { emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE); @@ -959,9 +955,17 @@ private void smoothScrollChipsToEnd() { }); } + private boolean hasContactsPermissions(@NonNull Context context) { + return Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS); + } + private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) { builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode()); - builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode()); + if (hasContactsPermissions(requireContext())) { + builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode()); + } else if (SignalStore.uiHints().getDismissedContactsPermissionBanner()) { + builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS.getCode()); + } builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java index de3acba570..cbcc878c97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -24,6 +24,7 @@ public class UiHints extends SignalStoreValues { private static final String LAST_CRASH_PROMPT = "uihints.last_crash_prompt"; private static final String HAS_COMPLETED_USERNAME_ONBOARDING = "uihints.has_completed_username_onboarding"; private static final String HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET = "uihints.has_seen_double_tap_edit_education_sheet"; + private static final String DISMISSED_CONTACTS_PERMISSION_BANNER = "uihints.dismissed_contacts_permission_banner"; UiHints(@NonNull KeyValueStore store) { super(store); @@ -167,4 +168,12 @@ public void setHasSeenDoubleTapEditEducationSheet(boolean seen) { public boolean getHasSeenDoubleTapEditEducationSheet() { return getBoolean(HAS_SEEN_DOUBLE_TAP_EDIT_EDUCATION_SHEET, false); } + + public void markDismissedContactsPermissionBanner() { + putBoolean(DISMISSED_CONTACTS_PERMISSION_BANNER, true); + } + + public boolean getDismissedContactsPermissionBanner() { + return getBoolean(DISMISSED_CONTACTS_PERMISSION_BANNER, false); + } } diff --git a/app/src/main/res/drawable/permissions_contact_book.xml b/app/src/main/res/drawable/permissions_contact_book.xml new file mode 100644 index 0000000000..974cd2beae --- /dev/null +++ b/app/src/main/res/drawable/permissions_contact_book.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/contact_selection_find_contacts_banner_item.xml b/app/src/main/res/layout/contact_selection_find_contacts_banner_item.xml new file mode 100644 index 0000000000..04e99d3af4 --- /dev/null +++ b/app/src/main/res/layout/contact_selection_find_contacts_banner_item.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/contact_selection_find_contacts_item.xml b/app/src/main/res/layout/contact_selection_find_contacts_item.xml new file mode 100644 index 0000000000..2ddcf305d0 --- /dev/null +++ b/app/src/main/res/layout/contact_selection_find_contacts_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/contact_selection_list_fragment.xml b/app/src/main/res/layout/contact_selection_list_fragment.xml index 5673b11e73..86529c36fc 100644 --- a/app/src/main/res/layout/contact_selection_list_fragment.xml +++ b/app/src/main/res/layout/contact_selection_list_fragment.xml @@ -48,71 +48,6 @@ app:layout_constraintTop_toBottomOf="@+id/chipRecycler" tools:visibility="visible" /> - - - - - - - - - - - - - - - - Refresh contacts Missing someone? Try refreshing + + Find people you know on Signal + + Allow access to your contacts More @@ -2747,6 +2751,14 @@ Find by phone number Find by username + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal needs access to your contacts in order to display them. From 227a279131939faeb7a4593eda3195c727a057e5 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 14 May 2024 15:44:44 -0400 Subject: [PATCH 08/20] Make sure note to self is included in backupsV2. --- .../v2/ui/subscription/MessageBackupsFlowViewModel.kt | 1 - .../backup/InternalBackupPlaygroundFragment.kt | 4 ++-- .../securesms/jobs/ArchiveAttachmentJob.kt | 4 ++-- .../securesms/jobs/ArchiveThumbnailUploadJob.kt | 2 +- .../securesms/jobs/AttachmentDownloadJob.kt | 6 +++--- .../thoughtcrime/securesms/jobs/BackupMessagesJob.kt | 2 +- .../securesms/jobs/RestoreAttachmentJob.kt | 4 ++-- .../thoughtcrime/securesms/keyvalue/BackupValues.kt | 8 ++++++-- .../org/thoughtcrime/securesms/sms/MessageSender.java | 10 +++++----- 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 98103b3f85..16928e683b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -72,7 +72,6 @@ class MessageBackupsFlowViewModel : ViewModel() { } private fun validateTypeAndUpdateState(): MessageBackupsScreen { - SignalStore.backup().canReadWriteToArchiveCdn = state.value.selectedMessageBackupTier == MessageBackupTier.PAID SignalStore.backup().areBackupsEnabled = true return MessageBackupsScreen.COMPLETED // return MessageBackupsScreen.CHECKOUT_SHEET TODO [message-backups] Switch back to payment flow diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index 78563ed818..cf433df35a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -170,7 +170,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() { }, mediaContent = { snackbarHostState -> MediaList( - enabled = SignalStore.backup().canReadWriteToArchiveCdn, + enabled = SignalStore.backup().backsUpMedia, state = mediaState, snackbarHostState = snackbarHostState, archiveAttachmentMedia = { viewModel.archiveAttachmentMedia(it) }, @@ -215,7 +215,7 @@ fun Tabs( } }, actions = { - if (tabIndex == 1 && SignalStore.backup().canReadWriteToArchiveCdn) { + if (tabIndex == 1 && SignalStore.backup().backsUpMedia) { TextButton(onClick = onDeleteAllArchivedMedia) { Text(text = "Delete All") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentJob.kt index d814178408..8304a91e81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentJob.kt @@ -26,7 +26,7 @@ class ArchiveAttachmentJob private constructor(private val attachmentId: Attachm const val KEY = "ArchiveAttachmentJob" fun enqueueIfPossible(attachmentId: AttachmentId) { - if (!SignalStore.backup().canReadWriteToArchiveCdn) { + if (!SignalStore.backup().backsUpMedia) { return } @@ -48,7 +48,7 @@ class ArchiveAttachmentJob private constructor(private val attachmentId: Attachm override fun getFactoryKey(): String = KEY override fun onRun() { - if (!SignalStore.backup().canReadWriteToArchiveCdn) { + if (!SignalStore.backup().backsUpMedia) { Log.w(TAG, "Do not have permission to read/write to archive cdn") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt index a41ab66895..56c11a1808 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt @@ -45,7 +45,7 @@ class ArchiveThumbnailUploadJob private constructor( private val TAG = Log.tag(ArchiveThumbnailUploadJob::class.java) fun enqueueIfNecessary(attachmentId: AttachmentId) { - if (SignalStore.backup().canReadWriteToArchiveCdn) { + if (SignalStore.backup().backsUpMedia) { ApplicationDependencies.getJobManager().add(ArchiveThumbnailUploadJob(attachmentId)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt index a95ed497fe..4698ecac23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt @@ -178,7 +178,7 @@ class AttachmentDownloadJob private constructor( if ((attachment.cdn == Cdn.CDN_2 || attachment.cdn == Cdn.CDN_3) && attachment.archiveMediaId == null && - SignalStore.backup().canReadWriteToArchiveCdn + SignalStore.backup().backsUpMedia ) { ApplicationDependencies.getJobManager().add(ArchiveAttachmentJob(attachmentId)) } @@ -212,7 +212,7 @@ class AttachmentDownloadJob private constructor( throw MmsException("Attachment too large, failing download") } - useArchiveCdn = if (SignalStore.backup().canReadWriteToArchiveCdn && (forceArchiveDownload || attachment.remoteLocation == null)) { + useArchiveCdn = if (SignalStore.backup().backsUpMedia && (forceArchiveDownload || attachment.remoteLocation == null)) { if (attachment.archiveMediaName.isNullOrEmpty()) { throw InvalidPartException("Invalid attachment configuration") } @@ -272,7 +272,7 @@ class AttachmentDownloadJob private constructor( Log.w(TAG, "Experienced exception while trying to download an attachment.", e) markFailed(messageId, attachmentId) } catch (e: NonSuccessfulResponseCodeException) { - if (SignalStore.backup().canReadWriteToArchiveCdn) { + if (SignalStore.backup().backsUpMedia) { if (e.code == 404 && !useArchiveCdn && attachment.archiveMediaName?.isNotEmpty() == true) { Log.i(TAG, "Retrying download from archive CDN") forceArchiveDownload = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index 21b31ab0bb..102cb7b29b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -58,7 +58,7 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa override fun onFailure() = Unit private fun archiveAttachments(): Boolean { - if (!SignalStore.backup().canReadWriteToArchiveCdn) return false + if (!SignalStore.backup().backsUpMedia) return false val batchSize = 100 var needToBackfill = 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index 88eb206e92..21a88b7934 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -202,7 +202,7 @@ class RestoreAttachmentJob private constructor( throw MmsException("Attachment too large, failing download") } - useArchiveCdn = if (SignalStore.backup().canReadWriteToArchiveCdn && (forceArchiveDownload || attachment.remoteLocation == null)) { + useArchiveCdn = if (SignalStore.backup().backsUpMedia && (forceArchiveDownload || attachment.remoteLocation == null)) { if (attachment.archiveMediaName.isNullOrEmpty()) { throw InvalidPartException("Invalid attachment configuration") } @@ -262,7 +262,7 @@ class RestoreAttachmentJob private constructor( Log.w(TAG, "Experienced exception while trying to download an attachment.", e) markFailed(messageId, attachmentId) } catch (e: NonSuccessfulResponseCodeException) { - if (SignalStore.backup().canReadWriteToArchiveCdn) { + if (SignalStore.backup().backsUpMedia) { if (e.code == 404 && !useArchiveCdn && attachment.archiveMediaName?.isNotEmpty() == true) { Log.i(TAG, "Retrying download from archive CDN") forceArchiveDownload = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 020934c781..f0b275f4ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -53,7 +53,6 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { override fun onFirstEverAppLaunch() = Unit override fun getKeysToIncludeInBackup(): List = emptyList() - var canReadWriteToArchiveCdn: Boolean by booleanValue(KEY_CDN_CAN_READ_WRITE, false) var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer) var optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false) var backupWithCellular: Boolean by booleanValue(KEY_BACKUP_OVER_CELLULAR, false) @@ -64,6 +63,11 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { val totalBackupSize: Long get() = lastBackupProtoSize + usedBackupMediaSpace + /** True if the user backs up media, otherwise false. */ + val backsUpMedia: Boolean + @JvmName("backsUpMedia") + get() = backupTier == MessageBackupTier.PAID + var areBackupsEnabled: Boolean get() { return getBoolean(KEY_BACKUPS_ENABLED, false) @@ -78,7 +82,7 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { } val backupTier: MessageBackupTier? = if (areBackupsEnabled) { - if (canReadWriteToArchiveCdn) { + if (backsUpMedia) { MessageBackupTier.PAID } else { MessageBackupTier.FREE diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index a9450ae19e..779d38110e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -394,7 +394,7 @@ public static void sendMediaBroadcast(@NonNull Context context, Recipient recipient = messages.get(i).getThreadRecipient(); if (isLocalSelfSend(context, recipient, SendType.SIGNAL)) { - sendLocalMediaSelf(context, messageId); + sendLocalMediaSelf(messageId); } else if (recipient.isPushGroup()) { jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), Collections.emptySet(), true, false), messageDependsOnIds, recipient.getId().toQueueKey()); } else if (recipient.isDistributionList()) { @@ -526,8 +526,8 @@ private static void sendMessageInternal(Context context, @NonNull Collection uploadJobIds, boolean isScheduledSend) { - if (isLocalSelfSend(context, recipient, sendType) && !isScheduledSend) { - sendLocalMediaSelf(context, messageId); + if (isLocalSelfSend(context, recipient, sendType) && !isScheduledSend && !SignalStore.backup().backsUpMedia()) { + sendLocalMediaSelf(messageId); } else if (recipient.isPushGroup()) { sendGroupPush(context, recipient, messageId, Collections.emptySet(), uploadJobIds); } else if (recipient.isDistributionList()) { @@ -608,13 +608,13 @@ public static boolean isLocalSelfSend(@NonNull Context context, @Nullable Recipi !TextSecurePreferences.isMultiDevice(context); } - private static void sendLocalMediaSelf(Context context, long messageId) { + private static void sendLocalMediaSelf(long messageId) { try { ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager(); MessageTable mmsDatabase = SignalDatabase.messages(); OutgoingMessage message = mmsDatabase.getOutgoingMessage(messageId); SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getSentTimeMillis()); - List attachments = new LinkedList<>(); + List attachments = new LinkedList<>(); attachments.addAll(message.getAttachments()); From d0340d39dbbd14ecd47a6487735608ae35c7a9fd Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 14 May 2024 16:12:40 -0400 Subject: [PATCH 09/20] Reset backupV2 credentials on 403. --- .../securesms/backup/v2/BackupRepository.kt | 12 +++++++++--- .../thoughtcrime/securesms/keyvalue/BackupValues.kt | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 3fa583a761..bab4be3edd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -65,9 +65,15 @@ object BackupRepository { private const val VERSION = 1L private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error -> - if (error.code == 401) { - Log.i(TAG, "Resetting initialized state due to 401.") - SignalStore.backup().backupsInitialized = false + when (error.code) { + 401 -> { + Log.i(TAG, "Resetting initialized state due to 401.") + SignalStore.backup().backupsInitialized = false + } + 403 -> { + Log.i(TAG, "Bad auth credential. Clearing stored credentials.") + SignalStore.backup().clearAllCredentials() + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index f0b275f4ca..c934ec7b4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -152,6 +152,10 @@ internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(updated))) } + fun clearAllCredentials() { + putString(KEY_CREDENTIALS, null) + } + class SerializedCredentials( @JsonProperty val credentialsByDay: Map From f83275e246c4e8fbfa2025634359cd0b3a8f146d Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Tue, 14 May 2024 16:22:26 -0400 Subject: [PATCH 10/20] Add customize button to in-call reaction picker. --- .../any/ReactWithAnyEmojiBottomSheetDialogFragment.java | 9 ++++++++- .../securesms/reactions/edit/EditReactionsActivity.kt | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java index 16ae57fcc1..80fc91342b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java @@ -57,6 +57,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound private static final String ARG_SHADOWS = "arg_shadows"; private static final String ARG_RECENT_KEY = "arg_recent_key"; private static final String ARG_EDIT = "arg_edit"; + private static final String ARG_DARK = "arg_dark"; private ReactWithAnyEmojiViewModel viewModel; private Callback callback = null; @@ -132,6 +133,8 @@ public static ReactWithAnyEmojiBottomSheetDialogFragment createForCallingReactio args.putInt(ARG_START_PAGE, -1); args.putBoolean(ARG_SHADOWS, false); args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY); + args.putBoolean(ARG_EDIT, true); + args.putBoolean(ARG_DARK, true); fragment.setArguments(args); return fragment; @@ -201,7 +204,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat if (requireArguments().getBoolean(ARG_EDIT, false)) { View customizeReactions = tabBar.findViewById(R.id.customize_reactions_frame); customizeReactions.setVisibility(View.VISIBLE); - customizeReactions.setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class))); + customizeReactions.setOnClickListener(v -> { + final Intent intent = new Intent(requireContext(), EditReactionsActivity.class); + intent.putExtra(EditReactionsActivity.ARG_FORCE_DARK_MODE, requireArguments().getBoolean(ARG_DARK, false)); + startActivity(intent); + }); } container.addView(tabBar); diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsActivity.kt index 3cccf25f6b..b35f35cb44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.reactions.edit import android.os.Bundle import android.view.View +import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R @@ -15,6 +16,9 @@ class EditReactionsActivity : PassphraseRequiredActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) + if (intent.extras?.getBoolean(ARG_FORCE_DARK_MODE) == true) { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + } theme.onCreate(this) @Suppress("DEPRECATION") @@ -32,4 +36,8 @@ class EditReactionsActivity : PassphraseRequiredActivity() { super.onResume() theme.onResume(this) } + + companion object { + const val ARG_FORCE_DARK_MODE = "arg_dark" + } } From c4e4eaf1108ebe1ce8eb655a6919b3f3e0e22321 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Tue, 14 May 2024 16:47:39 -0400 Subject: [PATCH 11/20] Remove lower hand confirmation dialog. --- .../components/webrtc/CallOverflowPopupWindow.kt | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt index 0f5038bd1c..c5d3aad54f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt @@ -16,7 +16,6 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.widget.PopupWindowCompat import androidx.fragment.app.FragmentActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.util.FeatureFlags @@ -44,19 +43,8 @@ class CallOverflowPopupWindow(private val activity: FragmentActivity, parentView val raiseHand = root.findViewById(R.id.raise_hand_layout_parent) raiseHand.visible = true raiseHand.setOnClickListener { - if (raisedHandDelegate.isSelfHandRaised()) { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.CallOverflowPopupWindow__lower_your_hand) - .setPositiveButton(R.string.CallOverflowPopupWindow__lower_hand) { _, _ -> - ApplicationDependencies.getSignalCallManager().raiseHand(false) - this@CallOverflowPopupWindow.dismiss() - } - .setNegativeButton(R.string.CallOverflowPopupWindow__cancel, null) - .show() - } else { - ApplicationDependencies.getSignalCallManager().raiseHand(true) - dismiss() - } + ApplicationDependencies.getSignalCallManager().raiseHand(!raisedHandDelegate.isSelfHandRaised()) + dismiss() } } } From 757c0fd2ea3aaa967fc8e40daac493c06f00811a Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Wed, 15 May 2024 09:21:59 -0500 Subject: [PATCH 12/20] create video call mimetype for raw contacts links --- app/src/main/AndroidManifest.xml | 8 ++++- .../jobs/SyncSystemContactLinksJob.kt | 29 ++++++++++--------- .../securesms/webrtc/VoiceCallShare.java | 8 ++++- app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/contactsformat.xml | 5 ++++ .../contacts/ContactLinkConfiguration.kt | 2 ++ .../contacts/SystemContactsRepository.kt | 10 +++++++ 7 files changed, 49 insertions(+), 16 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a74e94c4d5..745ce113b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -947,12 +947,18 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> - + + + + + + + context.getString(R.string.ContactsDatabase_message_s, e164) }, - callPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_call_s, e164) }, - e164Formatter = { number -> PhoneNumberFormatter.get(context).format(number) }, - messageMimetype = MESSAGE_MIMETYPE, - callMimetype = CALL_MIMETYPE, - syncTag = CONTACT_TAG - ) - } - class Factory : Job.Factory { override fun create(parameters: Parameters, serializedData: ByteArray?) = SyncSystemContactLinksJob(parameters) } @@ -110,6 +97,22 @@ class SyncSystemContactLinksJob private constructor(parameters: Parameters) : Ba private const val MESSAGE_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" private const val CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" + private const val VIDEO_CALL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" private const val CONTACT_TAG = "__TS" + + fun buildContactLinkConfiguration(context: Context, account: Account): ContactLinkConfiguration { + return ContactLinkConfiguration( + account = account, + appName = context.getString(R.string.app_name), + messagePrompt = { e164 -> context.getString(R.string.ContactsDatabase_message_s, e164) }, + callPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_call_s, e164) }, + videoCallPrompt = { e164 -> context.getString(R.string.ContactsDatabase_signal_video_call_s, e164) }, + e164Formatter = { number -> PhoneNumberFormatter.get(context).format(number) }, + messageMimetype = MESSAGE_MIMETYPE, + callMimetype = CALL_MIMETYPE, + videoCallMimetype = VIDEO_CALL_MIMETYPE, + syncTag = CONTACT_TAG + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java index e54e8c1b84..50adc4f5c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VoiceCallShare.java @@ -16,6 +16,8 @@ public class VoiceCallShare extends Activity { private static final String TAG = Log.tag(VoiceCallShare.class); + + private static final String VIDEO_CALL_MIME_TYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall"; @Override public void onCreate(Bundle icicle) { @@ -32,7 +34,11 @@ public void onCreate(Bundle icicle) { SimpleTask.run(() -> Recipient.external(this, destination), recipient -> { if (!TextUtils.isEmpty(destination)) { - ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(recipient); + if (VIDEO_CALL_MIME_TYPE.equals(getIntent().getType())) { + ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(recipient); + } else { + ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(recipient); + } Intent activityIntent = new Intent(this, WebRtcCallActivity.class); activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c63f4687e2..96d1187385 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -327,7 +327,8 @@ Message %s - Signal Call %s + Signal Voice Call %s + Signal Video Call %s diff --git a/app/src/main/res/xml/contactsformat.xml b/app/src/main/res/xml/contactsformat.xml index f22102f76b..f0eaefa58c 100644 --- a/app/src/main/res/xml/contactsformat.xml +++ b/app/src/main/res/xml/contactsformat.xml @@ -11,4 +11,9 @@ android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" android:summaryColumn="data2" android:detailColumn="data3"/> + diff --git a/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt b/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt index 3d32e63e7d..18d1752efc 100644 --- a/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt +++ b/contacts/lib/src/main/java/org/signal/contacts/ContactLinkConfiguration.kt @@ -17,8 +17,10 @@ class ContactLinkConfiguration( val appName: String, val messagePrompt: (String) -> String, val callPrompt: (String) -> String, + val videoCallPrompt: (String) -> String, val e164Formatter: (String) -> String, val messageMimetype: String, val callMimetype: String, + val videoCallMimetype: String, val syncTag: String ) diff --git a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt index 645d54ba33..43a1092367 100644 --- a/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt +++ b/contacts/lib/src/main/java/org/signal/contacts/SystemContactsRepository.kt @@ -526,6 +526,16 @@ object SystemContactsRepository { .withYieldAllowed(true) .build(), + // Data entry for making a video call + ContentProviderOperation.newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, operationIndex) + .withValue(ContactsContract.Data.MIMETYPE, linkConfig.videoCallMimetype) + .withValue(ContactsContract.Data.DATA1, systemContactInfo.displayPhone) + .withValue(ContactsContract.Data.DATA2, linkConfig.appName) + .withValue(ContactsContract.Data.DATA3, linkConfig.videoCallPrompt(systemContactInfo.displayPhone)) + .withYieldAllowed(true) + .build(), + // Ensures that this RawContact entry is shown next to another RawContact entry we found for this contact ContentProviderOperation.newUpdate(ContactsContract.AggregationExceptions.CONTENT_URI) .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, systemContactInfo.siblingRawContactId) From b72d586748045e897d6114879ec1198423716042 Mon Sep 17 00:00:00 2001 From: Clark Date: Wed, 15 May 2024 14:58:29 -0400 Subject: [PATCH 13/20] Add initial thumbnail restore for message backup. --- .../attachments/ArchivedAttachment.kt | 8 + .../securesms/attachments/Attachment.kt | 3 +- .../attachments/DatabaseAttachment.kt | 19 +++ .../attachments/PointerAttachment.kt | 1 + .../attachments/TombstoneAttachment.kt | 1 + .../securesms/attachments/UriAttachment.kt | 1 + .../securesms/backup/v2/BackupRepository.kt | 14 +- .../v2/database/ChatItemImportInserter.kt | 1 + .../securesms/database/AttachmentTable.kt | 130 ++++++++++++-- .../securesms/database/MessageTable.kt | 4 +- .../helpers/SignalDatabaseMigrations.kt | 6 +- .../migration/V231_ArchiveThumbnailColumns.kt | 18 ++ .../jobs/ArchiveThumbnailUploadJob.kt | 10 +- .../securesms/jobs/BackupMessagesJob.kt | 8 +- .../securesms/jobs/BackupRestoreMediaJob.kt | 8 +- .../securesms/jobs/RestoreAttachmentJob.kt | 161 ++++++++++++++++-- .../securesms/mms/PartAuthority.java | 7 +- .../org/thoughtcrime/securesms/mms/Slide.java | 11 +- app/src/main/protowire/JobData.proto | 2 +- .../sms/UploadDependencyGraphTest.kt | 4 +- .../securesms/database/FakeMessageRecords.kt | 7 +- .../signalservice/api/archive/ArchiveApi.kt | 8 +- .../signalservice/api/backup/BackupKey.kt | 4 + .../signalservice/api/backup/MediaName.kt | 1 + .../internal/push/PushServiceSocket.java | 10 ++ 25 files changed, 397 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V231_ArchiveThumbnailColumns.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt index bda906d2a8..f005e3642d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt @@ -22,6 +22,9 @@ class ArchivedAttachment : Attachment { @JvmField val archiveMediaId: String + @JvmField + val archiveThumbnailMediaId: String + constructor( contentType: String?, size: Long, @@ -31,6 +34,7 @@ class ArchivedAttachment : Attachment { archiveCdn: Int?, archiveMediaName: String, archiveMediaId: String, + archiveThumbnailMediaId: String, digest: ByteArray, incrementalMac: ByteArray?, incrementalMacChunkSize: Int?, @@ -70,12 +74,14 @@ class ArchivedAttachment : Attachment { this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber this.archiveMediaName = archiveMediaName this.archiveMediaId = archiveMediaId + this.archiveThumbnailMediaId = archiveThumbnailMediaId } constructor(parcel: Parcel) : super(parcel) { archiveCdn = parcel.readInt() archiveMediaName = parcel.readString()!! archiveMediaId = parcel.readString()!! + archiveThumbnailMediaId = parcel.readString()!! } override fun writeToParcel(dest: Parcel, flags: Int) { @@ -83,8 +89,10 @@ class ArchivedAttachment : Attachment { dest.writeInt(archiveCdn) dest.writeString(archiveMediaName) dest.writeString(archiveMediaId) + dest.writeString(archiveThumbnailMediaId) } override val uri: Uri? = null override val publicUri: Uri? = null + override val thumbnailUri: Uri? = null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt index 8d88953e76..784871a778 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt @@ -70,6 +70,7 @@ abstract class Attachment( abstract val uri: Uri? abstract val publicUri: Uri? + abstract val thumbnailUri: Uri? protected constructor(parcel: Parcel) : this( contentType = parcel.readString()!!, @@ -129,7 +130,7 @@ abstract class Attachment( } val isInProgress: Boolean - get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE + get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED val isPermanentlyFailed: Boolean get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt index 1c0f4bfc30..0f62be8c04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt @@ -28,12 +28,16 @@ class DatabaseAttachment : Attachment { @JvmField val archiveCdn: Int + @JvmField + val archiveThumbnailCdn: Int + @JvmField val archiveMediaName: String? @JvmField val archiveMediaId: String? + private val hasArchiveThumbnail: Boolean private val hasThumbnail: Boolean val displayOrder: Int @@ -42,6 +46,7 @@ class DatabaseAttachment : Attachment { mmsId: Long, hasData: Boolean, hasThumbnail: Boolean, + hasArchiveThumbnail: Boolean, contentType: String?, transferProgress: Int, size: Long, @@ -68,6 +73,7 @@ class DatabaseAttachment : Attachment { uploadTimestamp: Long, dataHash: String?, archiveCdn: Int, + archiveThumbnailCdn: Int, archiveMediaName: String?, archiveMediaId: String? ) : super( @@ -99,8 +105,10 @@ class DatabaseAttachment : Attachment { this.hasData = hasData this.dataHash = dataHash this.hasThumbnail = hasThumbnail + this.hasArchiveThumbnail = hasArchiveThumbnail this.displayOrder = displayOrder this.archiveCdn = archiveCdn + this.archiveThumbnailCdn = archiveThumbnailCdn this.archiveMediaName = archiveMediaName this.archiveMediaId = archiveMediaId } @@ -113,8 +121,10 @@ class DatabaseAttachment : Attachment { mmsId = parcel.readLong() displayOrder = parcel.readInt() archiveCdn = parcel.readInt() + archiveThumbnailCdn = parcel.readInt() archiveMediaName = parcel.readString() archiveMediaId = parcel.readString() + hasArchiveThumbnail = ParcelUtil.readBoolean(parcel) } override fun writeToParcel(dest: Parcel, flags: Int) { @@ -126,8 +136,10 @@ class DatabaseAttachment : Attachment { dest.writeLong(mmsId) dest.writeInt(displayOrder) dest.writeInt(archiveCdn) + dest.writeInt(archiveThumbnailCdn) dest.writeString(archiveMediaName) dest.writeString(archiveMediaId) + ParcelUtil.writeBoolean(dest, hasArchiveThumbnail) } override val uri: Uri? @@ -144,6 +156,13 @@ class DatabaseAttachment : Attachment { null } + override val thumbnailUri: Uri? + get() = if (hasArchiveThumbnail) { + PartAuthority.getAttachmentThumbnailUri(attachmentId) + } else { + null + } + override fun equals(other: Any?): Boolean { return other != null && other is DatabaseAttachment && other.attachmentId == attachmentId diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt index 6b988cf75f..e26b32d95f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt @@ -66,6 +66,7 @@ class PointerAttachment : Attachment { override val uri: Uri? = null override val publicUri: Uri? = null + override val thumbnailUri: Uri? = null companion object { @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt index efbf88b404..dd2d0364b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt @@ -80,4 +80,5 @@ class TombstoneAttachment : Attachment { override val uri: Uri? = null override val publicUri: Uri? = null + override val thumbnailUri: Uri? = null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt index 39e3f02a18..2de2f818e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt @@ -98,6 +98,7 @@ class UriAttachment : Attachment { override val uri: Uri override val publicUri: Uri? = null + override val thumbnailUri: Uri? = null override fun writeToParcel(dest: Parcel, flags: Int) { super.writeToParcel(dest, flags) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index bab4be3edd..856956270b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor @@ -346,7 +347,7 @@ object BackupRepository { /** * Retrieves an upload spec that can be used to upload attachment media. */ - fun getMediaUploadSpec(): NetworkResult { + fun getMediaUploadSpec(secretKey: ByteArray? = null): NetworkResult { val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() @@ -355,20 +356,21 @@ object BackupRepository { api.getMediaUploadForm(backupKey, credential) } .then { form -> - api.getResumableUploadSpec(form) + api.getResumableUploadSpec(form, secretKey) } } fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult { val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() + val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey) return initBackupAndFetchAuth(backupKey) .then { credential -> api.archiveAttachmentMedia( backupKey = backupKey, serviceCredential = credential, - item = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey) + item = request ) } } @@ -390,7 +392,8 @@ object BackupRepository { .map { Triple(mediaName, request.mediaId, it) } } .map { (mediaName, mediaId, response) -> - SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId) + val thumbnailId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode() + SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId, archiveThumbnailMediaId = thumbnailId) } .also { Log.i(TAG, "archiveMediaResult: $it") } } @@ -427,7 +430,8 @@ object BackupRepository { .forEach { val attachmentId = result.mediaIdToAttachmentId(it.mediaId) val mediaName = result.attachmentIdToMediaName(attachmentId) - SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId) + val thumbnailId = backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode() + SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId, thumbnailId) } result } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt index c8f3ee2dea..b1116f8dc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt @@ -658,6 +658,7 @@ class ChatItemImportInserter( archiveCdn = pointer.backupLocator.cdnNumber, archiveMediaName = pointer.backupLocator.mediaName, archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(), + archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(), digest = pointer.backupLocator.digest.toByteArray(), incrementalMac = pointer.incrementalMac?.toByteArray(), incrementalMacChunkSize = pointer.incrementalMacChunkSize, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index f0401b83c0..49f8bf370c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -125,6 +125,8 @@ class AttachmentTable( const val DATA_RANDOM = "data_random" const val DATA_HASH_START = "data_hash_start" const val DATA_HASH_END = "data_hash_end" + const val THUMBNAIL_FILE = "thumbnail_file" + const val THUMBNAIL_RANDOM = "thumbnail_random" const val FILE_NAME = "file_name" const val FAST_PREFLIGHT_ID = "fast_preflight_id" const val VOICE_NOTE = "voice_note" @@ -145,6 +147,8 @@ class AttachmentTable( const val ARCHIVE_CDN = "archive_cdn" const val ARCHIVE_MEDIA_NAME = "archive_media_name" const val ARCHIVE_MEDIA_ID = "archive_media_id" + const val ARCHIVE_THUMBNAIL_MEDIA_ID = "archive_thumbnail_media_id" + const val ARCHIVE_THUMBNAIL_CDN = "archive_thumbnail_cdn" const val ARCHIVE_TRANSFER_FILE = "archive_transfer_file" const val ARCHIVE_TRANSFER_STATE = "archive_transfer_state" @@ -159,6 +163,7 @@ class AttachmentTable( const val TRANSFER_PROGRESS_PERMANENT_FAILURE = 4 const val TRANSFER_NEEDS_RESTORE = 5 const val TRANSFER_RESTORE_IN_PROGRESS = 6 + const val TRANSFER_RESTORE_OFFLOADED = 7 const val PREUPLOAD_MESSAGE_ID: Long = -8675309 private val PROJECTION = arrayOf( @@ -196,9 +201,11 @@ class AttachmentTable( DATA_HASH_START, DATA_HASH_END, ARCHIVE_CDN, + ARCHIVE_THUMBNAIL_CDN, ARCHIVE_MEDIA_NAME, ARCHIVE_MEDIA_ID, - ARCHIVE_TRANSFER_FILE + ARCHIVE_TRANSFER_FILE, + THUMBNAIL_FILE ) @JvmField @@ -240,8 +247,12 @@ class AttachmentTable( $ARCHIVE_CDN INTEGER DEFAULT 0, $ARCHIVE_MEDIA_NAME TEXT DEFAULT NULL, $ARCHIVE_MEDIA_ID TEXT DEFAULT NULL, + $ARCHIVE_THUMBNAIL_MEDIA_ID TEXT DEFAULT NULL, + $ARCHIVE_THUMBNAIL_CDN INTEGER DEFAULT 0, $ARCHIVE_TRANSFER_FILE TEXT DEFAULT NULL, - $ARCHIVE_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value} + $ARCHIVE_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value}, + $THUMBNAIL_FILE TEXT DEFAULT NULL, + $THUMBNAIL_RANDOM BLOB DEFAULT NULL ) """ @@ -273,6 +284,15 @@ class AttachmentTable( } ?: throw IOException("No stream for: $attachmentId") } + @Throws(IOException::class) + fun getAttachmentThumbnailStream(attachmentId: AttachmentId, offset: Long): InputStream { + return try { + getThumbnailStream(attachmentId, offset) + } catch (e: FileNotFoundException) { + throw IOException("No stream for: $attachmentId", e) + } ?: throw IOException("No stream for: $attachmentId") + } + /** * Returns a [File] for an attachment that has no [DATA_HASH_END] and is in the [TRANSFER_PROGRESS_DONE] state, if present. */ @@ -826,6 +846,36 @@ class AttachmentTable( } } + @Throws(IOException::class) + fun finalizeAttachmentThumbnailAfterDownload(attachmentId: AttachmentId, archiveMediaId: String, inputStream: InputStream, transferFile: File) { + Log.i(TAG, "[finalizeAttachmentThumbnailAfterDownload] Finalizing downloaded data for $attachmentId.") + val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), inputStream, TransformProperties.empty()) + + writableDatabase.withinTransaction { db -> + val values = contentValuesOf( + THUMBNAIL_FILE to fileWriteResult.file.absolutePath, + THUMBNAIL_RANDOM to fileWriteResult.random + ) + + db.update(TABLE_NAME) + .values(values) + .where("$ARCHIVE_MEDIA_ID = ?", archiveMediaId) + .run() + + db.update(TABLE_NAME) + .values(TRANSFER_STATE to TRANSFER_RESTORE_OFFLOADED) + .where("$ID = ?", attachmentId.id) + .run() + } + + notifyConversationListListeners() + notifyAttachmentListeners() + + if (!transferFile.delete()) { + Log.w(TAG, "Unable to delete transfer file.") + } + } + /** * Needs to be called after an attachment is successfully uploaded. Writes metadata around it's final remote location, as well as calculates * it's ending hash, which is critical for backups. @@ -1158,6 +1208,10 @@ class AttachmentTable( return transferFile } + fun createArchiveThumbnailTransferFile(): File { + return newTransferFile() + } + fun getDataFileInfo(attachmentId: AttachmentId): DataFileInfo? { return readableDatabase .select(ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP) @@ -1173,6 +1227,21 @@ class AttachmentTable( } } + fun getThumbnailFileInfo(attachmentId: AttachmentId): ThumbnailFileInfo? { + return readableDatabase + .select(ID, THUMBNAIL_FILE, THUMBNAIL_RANDOM) + .from(TABLE_NAME) + .where("$ID = ?", attachmentId.id) + .run() + .readToSingleObject { cursor -> + if (cursor.isNull(THUMBNAIL_FILE)) { + null + } else { + cursor.readThumbnailFileInfo() + } + } + } + fun getDataFilePath(attachmentId: AttachmentId): String? { return readableDatabase .select(DATA_FILE) @@ -1320,8 +1389,10 @@ class AttachmentTable( uploadTimestamp = jsonObject.getLong(UPLOAD_TIMESTAMP), dataHash = jsonObject.getString(DATA_HASH_END), archiveCdn = jsonObject.getInt(ARCHIVE_CDN), + archiveThumbnailCdn = jsonObject.getInt(ARCHIVE_THUMBNAIL_CDN), archiveMediaName = jsonObject.getString(ARCHIVE_MEDIA_NAME), - archiveMediaId = jsonObject.getString(ARCHIVE_MEDIA_ID) + archiveMediaId = jsonObject.getString(ARCHIVE_MEDIA_ID), + hasArchiveThumbnail = !TextUtils.isEmpty(jsonObject.getString(THUMBNAIL_FILE)) ) } } @@ -1361,13 +1432,14 @@ class AttachmentTable( return readableDatabase.rawQuery(query, null) } - fun setArchiveData(attachmentId: AttachmentId, archiveCdn: Int, archiveMediaName: String, archiveMediaId: String) { + fun setArchiveData(attachmentId: AttachmentId, archiveCdn: Int, archiveMediaName: String, archiveMediaId: String, archiveThumbnailMediaId: String) { writableDatabase .update(TABLE_NAME) .values( ARCHIVE_CDN to archiveCdn, ARCHIVE_MEDIA_ID to archiveMediaId, ARCHIVE_MEDIA_NAME to archiveMediaName, + ARCHIVE_THUMBNAIL_MEDIA_ID to archiveThumbnailMediaId, ARCHIVE_TRANSFER_STATE to ArchiveTransferState.FINISHED.value ) .where("$ID = ?", attachmentId.id) @@ -1375,13 +1447,14 @@ class AttachmentTable( } fun updateArchiveCdnByMediaId(archiveMediaId: String, archiveCdn: Int): Int { - return writableDatabase - .update(TABLE_NAME) - .values( - ARCHIVE_CDN to archiveCdn - ) - .where("$ARCHIVE_MEDIA_ID = ?", archiveMediaId) - .run() + return writableDatabase.rawQuery( + "UPDATE $TABLE_NAME SET " + + "$ARCHIVE_THUMBNAIL_CDN = CASE WHEN $ARCHIVE_THUMBNAIL_MEDIA_ID = ? THEN ? ELSE $ARCHIVE_THUMBNAIL_CDN END," + + "$ARCHIVE_CDN = CASE WHEN $ARCHIVE_MEDIA_ID = ? THEN ? ELSE $ARCHIVE_CDN END " + + "WHERE $ARCHIVE_MEDIA_ID = ? OR $ARCHIVE_THUMBNAIL_MEDIA_ID = ? " + + "RETURNING $ARCHIVE_CDN, $ARCHIVE_THUMBNAIL_CDN", + SqlUtil.buildArgs(archiveMediaId, archiveCdn, archiveMediaId, archiveCdn, archiveMediaId, archiveMediaId) + ).count } fun clearArchiveData(attachmentIds: List) { @@ -1485,6 +1558,21 @@ class AttachmentTable( } } + @Throws(FileNotFoundException::class) + private fun getThumbnailStream(attachmentId: AttachmentId, offset: Long): InputStream? { + val thumbnailInfo = getThumbnailFileInfo(attachmentId) ?: return null + + return try { + ModernDecryptingPartInputStream.createFor(attachmentSecret, thumbnailInfo.random, thumbnailInfo.file, offset) + } catch (e: FileNotFoundException) { + Log.w(TAG, e) + throw e + } catch (e: IOException) { + Log.w(TAG, e) + null + } + } + @Throws(IOException::class) private fun newTransferFile(): File { val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) @@ -1664,6 +1752,7 @@ class AttachmentTable( put(ARCHIVE_CDN, attachment.archiveCdn) put(ARCHIVE_MEDIA_NAME, attachment.archiveMediaName) put(ARCHIVE_MEDIA_ID, attachment.archiveMediaId) + put(ARCHIVE_THUMBNAIL_MEDIA_ID, attachment.archiveThumbnailMediaId) attachment.stickerLocator?.let { sticker -> put(STICKER_PACK_ID, sticker.packId) @@ -1874,8 +1963,10 @@ class AttachmentTable( uploadTimestamp = cursor.requireLong(UPLOAD_TIMESTAMP), dataHash = cursor.requireString(DATA_HASH_END), archiveCdn = cursor.requireInt(ARCHIVE_CDN), + archiveThumbnailCdn = cursor.requireInt(ARCHIVE_THUMBNAIL_CDN), archiveMediaName = cursor.requireString(ARCHIVE_MEDIA_NAME), - archiveMediaId = cursor.requireString(ARCHIVE_MEDIA_ID) + archiveMediaId = cursor.requireString(ARCHIVE_MEDIA_ID), + hasArchiveThumbnail = !cursor.isNull(THUMBNAIL_FILE) ) } @@ -1900,6 +1991,14 @@ class AttachmentTable( ) } + private fun Cursor.readThumbnailFileInfo(): ThumbnailFileInfo { + return ThumbnailFileInfo( + id = AttachmentId(this.requireLong(ID)), + file = File(this.requireNonNullString(THUMBNAIL_FILE)), + random = this.requireNonNullBlob(THUMBNAIL_RANDOM) + ) + } + private fun Cursor.readStickerLocator(): StickerLocator? { return if (this.requireInt(STICKER_ID) >= 0) { StickerLocator( @@ -1954,6 +2053,13 @@ class AttachmentTable( val uploadTimestamp: Long ) + @VisibleForTesting + class ThumbnailFileInfo( + val id: AttachmentId, + val file: File, + val random: ByteArray + ) + @Parcelize data class TransformProperties( @JsonProperty("skipTransform") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 591158676b..f45fd79fd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -357,7 +357,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat '${AttachmentTable.MESSAGE_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}, '${AttachmentTable.DATA_SIZE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE}, '${AttachmentTable.FILE_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME}, - '${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE}, + '${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE}, + '${AttachmentTable.THUMBNAIL_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE}, '${AttachmentTable.CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE}, '${AttachmentTable.CDN_NUMBER}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER}, '${AttachmentTable.REMOTE_LOCATION}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION}, @@ -381,6 +382,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat '${AttachmentTable.UPLOAD_TIMESTAMP}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP}, '${AttachmentTable.DATA_HASH_END}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END}, '${AttachmentTable.ARCHIVE_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN}, + '${AttachmentTable.ARCHIVE_THUMBNAIL_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_THUMBNAIL_CDN}, '${AttachmentTable.ARCHIVE_MEDIA_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_NAME}, '${AttachmentTable.ARCHIVE_MEDIA_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_ID} ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 8dd4929fac..d5f4dbdc1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -88,6 +88,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V227_AddAttachmentA import org.thoughtcrime.securesms.database.helpers.migration.V228_AddNameCollisionTables import org.thoughtcrime.securesms.database.helpers.migration.V229_MarkMissedCallEventsNotified import org.thoughtcrime.securesms.database.helpers.migration.V230_UnreadCountIndices +import org.thoughtcrime.securesms.database.helpers.migration.V231_ArchiveThumbnailColumns /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -178,10 +179,11 @@ object SignalDatabaseMigrations { 227 to V227_AddAttachmentArchiveTransferState, 228 to V228_AddNameCollisionTables, 229 to V229_MarkMissedCallEventsNotified, - 230 to V230_UnreadCountIndices + 230 to V230_UnreadCountIndices, + 231 to V231_ArchiveThumbnailColumns ) - const val DATABASE_VERSION = 230 + const val DATABASE_VERSION = 231 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V231_ArchiveThumbnailColumns.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V231_ArchiveThumbnailColumns.kt new file mode 100644 index 0000000000..dda3d63074 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V231_ArchiveThumbnailColumns.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +object V231_ArchiveThumbnailColumns : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE attachment ADD COLUMN thumbnail_file TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE attachment ADD COLUMN thumbnail_random BLOB DEFAULT NULL") + db.execSQL("ALTER TABLE attachment ADD COLUMN archive_thumbnail_cdn INTEGER DEFAULT 0") + db.execSQL("ALTER TABLE attachment ADD COLUMN archive_thumbnail_media_id TEXT DEFAULT NULL") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt index 56c11a1808..91b57e91f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.PointerAttachment import org.thoughtcrime.securesms.backup.v2.BackupRepository +import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobmanager.Job @@ -86,8 +87,9 @@ class ArchiveThumbnailUploadJob private constructor( Log.w(TAG, "Unable to generate a thumbnail result for $attachmentId") return Result.success() } + val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() - val resumableUpload = when (val result = BackupRepository.getMediaUploadSpec()) { + val resumableUpload = when (val result = BackupRepository.getMediaUploadSpec(secretKey = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()))) { is NetworkResult.Success -> { Log.d(TAG, "Got an upload spec!") result.result.toProto() @@ -116,9 +118,13 @@ class ArchiveThumbnailUploadJob private constructor( return Result.retry(defaultBackoff()) } + val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow() + val mediaSecrets = backupKey.deriveMediaSecrets(attachment.getThumbnailMediaName()) + return when (val result = BackupRepository.archiveThumbnail(attachmentPointer, attachment)) { is NetworkResult.Success -> { - Log.d(TAG, "Successfully archived thumbnail for $attachmentId") + Log.i(RestoreAttachmentJob.TAG, "Restore: Thumbnail mediaId=${mediaSecrets.id.encode()} backupDir=${backupDirectories.backupDir} mediaDir=${backupDirectories.mediaDir}") + Log.d(TAG, "Successfully archived thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}") Result.success() } is NetworkResult.NetworkError -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index 102cb7b29b..d2d25c2e2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -72,11 +72,15 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa when (val archiveResult = BackupRepository.archiveMedia(attachments)) { is NetworkResult.Success -> { Log.i(TAG, "Archive call successful") - for (success in archiveResult.result.sourceNotFoundResponses) { - val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId) + for (notFound in archiveResult.result.sourceNotFoundResponses) { + val attachmentId = archiveResult.result.mediaIdToAttachmentId(notFound.mediaId) Log.i(TAG, "Attachment $attachmentId not found on cdn, will need to re-upload") needToBackfill++ } + for (success in archiveResult.result.successfulResponses) { + val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId) + ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId) + } progress += attachments.size } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt index 039a5ef531..37ea9f3420 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt @@ -62,7 +62,11 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo attachmentId = attachment.attachmentId, manual = false, forceArchiveDownload = true, - fullSize = shouldRestoreFullSize(message, restoreTime, optimizeStorage = SignalStore.backup().optimizeStorage) + restoreMode = if (shouldRestoreFullSize(message, restoreTime, optimizeStorage = SignalStore.backup().optimizeStorage)) { + RestoreAttachmentJob.RestoreMode.ORIGINAL + } else { + RestoreAttachmentJob.RestoreMode.THUMBNAIL + } ) } jobManager.addAll(restoreJobBatch) @@ -70,7 +74,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo } private fun shouldRestoreFullSize(message: MmsMessageRecord, restoreTime: Long, optimizeStorage: Boolean): Boolean { - return ((restoreTime - message.dateSent) < 30.days.inWholeMilliseconds) || !optimizeStorage + return !optimizeStorage || ((restoreTime - message.dateSent) < 30.days.inWholeMilliseconds) } override fun onShouldRetry(e: Exception): Boolean = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index 21a88b7934..162fbf0197 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -15,6 +15,7 @@ import org.signal.libsignal.protocol.InvalidMessageException import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.backup.v2.BackupRepository +import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -52,18 +53,18 @@ class RestoreAttachmentJob private constructor( attachmentId: AttachmentId, private val manual: Boolean, private var forceArchiveDownload: Boolean, - private val fullSize: Boolean + private val restoreMode: RestoreMode ) : BaseJob(parameters) { companion object { const val KEY = "RestoreAttachmentJob" - private val TAG = Log.tag(AttachmentDownloadJob::class.java) + val TAG = Log.tag(AttachmentDownloadJob::class.java) private const val KEY_MESSAGE_ID = "message_id" private const val KEY_ATTACHMENT_ID = "part_row_id" private const val KEY_MANUAL = "part_manual" private const val KEY_FORCE_ARCHIVE = "force_archive" - private const val KEY_FULL_SIZE = "full_size" + private const val KEY_RESTORE_MODE = "restore_mode" @JvmStatic fun constructQueueString(attachmentId: AttachmentId): String { @@ -71,13 +72,19 @@ class RestoreAttachmentJob private constructor( return "RestoreAttachmentJob" } - fun jobSpecMatchesAnyAttachmentId(jobSpec: JobSpec, ids: Set): Boolean { + private fun getJsonJobData(jobSpec: JobSpec): JsonJobData? { if (KEY != jobSpec.factoryKey) { - return false + return null } - val serializedData = jobSpec.serializedData ?: return false - val data = JsonJobData.deserialize(serializedData) + val serializedData = jobSpec.serializedData ?: return null + return JsonJobData.deserialize(serializedData) + } + + fun jobSpecMatchesAnyAttachmentId(data: JsonJobData?, ids: Set): Boolean { + if (data == null) { + return false + } val parsed = AttachmentId(data.getLong(KEY_ATTACHMENT_ID)) return ids.contains(parsed) } @@ -85,8 +92,15 @@ class RestoreAttachmentJob private constructor( fun modifyPriorities(ids: Set, priority: Int) { val jobManager = ApplicationDependencies.getJobManager() jobManager.update { spec -> - if (jobSpecMatchesAnyAttachmentId(spec, ids) && spec.priority != priority) { - spec.copy(priority = priority) + val jobData = getJsonJobData(spec) + if (jobSpecMatchesAnyAttachmentId(jobData, ids) && spec.priority != priority) { + val restoreMode = RestoreMode.deserialize(jobData!!.getIntOrDefault(KEY_RESTORE_MODE, RestoreMode.ORIGINAL.value)) + val modifiedJobData = if (restoreMode == RestoreMode.ORIGINAL) { + jobData.buildUpon().putInt(KEY_RESTORE_MODE, RestoreMode.BOTH.value).build() + } else { + jobData + } + spec.copy(priority = priority, serializedData = modifiedJobData.serialize()) } else { spec } @@ -96,7 +110,7 @@ class RestoreAttachmentJob private constructor( private val attachmentId: Long - constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, forceArchiveDownload: Boolean = false, fullSize: Boolean = true) : this( + constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, forceArchiveDownload: Boolean = false, restoreMode: RestoreMode = RestoreMode.ORIGINAL) : this( Parameters.Builder() .setQueue(constructQueueString(attachmentId)) .addConstraint(NetworkConstraint.KEY) @@ -107,7 +121,7 @@ class RestoreAttachmentJob private constructor( attachmentId, manual, forceArchiveDownload, - fullSize + restoreMode ) init { @@ -120,7 +134,7 @@ class RestoreAttachmentJob private constructor( .putLong(KEY_ATTACHMENT_ID, attachmentId) .putBoolean(KEY_MANUAL, manual) .putBoolean(KEY_FORCE_ARCHIVE, forceArchiveDownload) - .putBoolean(KEY_FULL_SIZE, fullSize) + .putInt(KEY_RESTORE_MODE, restoreMode.value) .serialize() } @@ -166,12 +180,19 @@ class RestoreAttachmentJob private constructor( return } - if (attachment.transferState != AttachmentTable.TRANSFER_NEEDS_RESTORE && attachment.transferState != AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS) { + if (attachment.transferState != AttachmentTable.TRANSFER_NEEDS_RESTORE && + attachment.transferState != AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && + (attachment.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED || restoreMode == RestoreMode.THUMBNAIL) + ) { Log.w(TAG, "Attachment does not need to be restored.") return } - - retrieveAttachment(messageId, attachmentId, attachment) + if (attachment.thumbnailUri == null && (restoreMode == RestoreMode.THUMBNAIL || restoreMode == RestoreMode.BOTH)) { + downloadThumbnail(attachmentId, attachment) + } + if (restoreMode == RestoreMode.ORIGINAL || restoreMode == RestoreMode.BOTH) { + retrieveAttachment(messageId, attachmentId, attachment) + } } override fun onFailure() { @@ -360,6 +381,102 @@ class RestoreAttachmentJob private constructor( } } + @Throws(InvalidPartException::class) + private fun createThumbnailPointer(attachment: DatabaseAttachment): SignalServiceAttachmentPointer { + if (TextUtils.isEmpty(attachment.remoteKey)) { + throw InvalidPartException("empty encrypted key") + } + + val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() + val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow() + return try { + val key = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()) + + if (attachment.remoteDigest != null) { + Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.remoteDigest)) + } else { + Log.i(TAG, "Downloading attachment with no digest...") + } + + val mediaId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode() + Log.i(TAG, "Restore: Thumbnail mediaId=$mediaId backupDir=${backupDirectories.backupDir} mediaDir=${backupDirectories.mediaDir}") + + SignalServiceAttachmentPointer( + attachment.archiveThumbnailCdn, + SignalServiceAttachmentRemoteId.Backup( + backupDir = backupDirectories.backupDir, + mediaDir = backupDirectories.mediaDir, + mediaId = mediaId + ), + null, + key, + Optional.empty(), + Optional.empty(), + 0, + 0, + Optional.ofNullable(attachment.remoteDigest), + Optional.empty(), + attachment.incrementalMacChunkSize, + Optional.empty(), + attachment.voiceNote, + attachment.borderless, + attachment.videoGif, + Optional.empty(), + Optional.ofNullable(attachment.blurHash).map { it.hash }, + attachment.uploadTimestamp + ) + } catch (e: IOException) { + Log.w(TAG, e) + throw InvalidPartException(e) + } catch (e: ArithmeticException) { + Log.w(TAG, e) + throw InvalidPartException(e) + } + } + + private fun downloadThumbnail(attachmentId: AttachmentId, attachment: DatabaseAttachment) { + if (attachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) { + Log.w(TAG, "$attachmentId already has thumbnail downloaded") + return + } + if (attachment.archiveMediaName == null) { + Log.w(TAG, "$attachmentId was never archived! Cannot proceed.") + return + } + + val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes() + val thumbnailTransferFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() + val thumbnailFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() + + val progressListener = object : SignalServiceAttachment.ProgressListener { + override fun onAttachmentProgress(total: Long, progress: Long) { + EventBus.getDefault().postSticky(PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)) + } + + override fun shouldCancel(): Boolean { + return this@RestoreAttachmentJob.isCanceled + } + } + + val cdnCredentials = BackupRepository.getCdnReadCredentials(attachment.archiveCdn).successOrThrow().headers + val messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver() + val pointer = createThumbnailPointer(attachment) + + Log.w(TAG, "Downloading thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}") + val stream = messageReceiver + .retrieveArchivedAttachment( + SignalStore.svr().getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()), + cdnCredentials, + thumbnailTransferFile, + pointer, + thumbnailFile, + maxThumbnailSize, + progressListener + ) + + SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, stream, thumbnailTransferFile) + } + private fun markFailed(messageId: Long, attachmentId: AttachmentId) { try { SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId) @@ -382,6 +499,18 @@ class RestoreAttachmentJob private constructor( constructor(e: Exception?) : super(e) } + enum class RestoreMode(val value: Int) { + THUMBNAIL(0), + ORIGINAL(1), + BOTH(2); + + companion object { + fun deserialize(value: Int): RestoreMode { + return values().firstOrNull { it.value == value } ?: ORIGINAL + } + } + } + private data class RemoteData(val remoteId: SignalServiceAttachmentRemoteId, val cdnNumber: Int) class Factory : Job.Factory { @@ -393,7 +522,7 @@ class RestoreAttachmentJob private constructor( attachmentId = AttachmentId(data.getLong(KEY_ATTACHMENT_ID)), manual = data.getBoolean(KEY_MANUAL), forceArchiveDownload = data.getBooleanOrDefault(KEY_FORCE_ARCHIVE, false), - fullSize = data.getBooleanOrDefault(KEY_FULL_SIZE, true) + restoreMode = RestoreMode.deserialize(data.getIntOrDefault(KEY_RESTORE_MODE, RestoreMode.ORIGINAL.value)) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index 74b399786e..db14123ecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -32,11 +32,13 @@ public class PartAuthority { private static final String AUTHORITY = BuildConfig.APPLICATION_ID; private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part"; + private static final String PART_THUMBNAIL_STRING = "content://" + AUTHORITY + "/thumbnail"; private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker"; private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper"; private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji"; private static final String AVATAR_PICKER_URI_STRING = "content://" + AUTHORITY + "/avatar_picker"; private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); + private static final Uri PART_THUMBNAIL_URI = Uri.parse(PART_THUMBNAIL_STRING); private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING); private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING); private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING); @@ -49,12 +51,14 @@ public class PartAuthority { private static final int WALLPAPER_ROW = 5; private static final int EMOJI_ROW = 6; private static final int AVATAR_PICKER_ROW = 7; + private static final int THUMBNAIL_ROW = 8; private static final UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "part/#", PART_ROW); + uriMatcher.addURI(AUTHORITY, "thumbnail/#", THUMBNAIL_ROW); uriMatcher.addURI(AUTHORITY, "sticker/#", STICKER_ROW); uriMatcher.addURI(AUTHORITY, "wallpaper/*", WALLPAPER_ROW); uriMatcher.addURI(AUTHORITY, "emoji/*", EMOJI_ROW); @@ -83,6 +87,7 @@ public static InputStream getAttachmentStream(@NonNull Context context, @NonNull case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri)); case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri)); case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri)); + case THUMBNAIL_ROW: return SignalDatabase.attachments().getAttachmentThumbnailStream(new PartUriParser(uri).getPartId(), 0); default: return openExternalFileStream(context, uri); } } catch (SecurityException se) { @@ -178,7 +183,7 @@ public static Uri getAttachmentDataUri(AttachmentId attachmentId) { } public static Uri getAttachmentThumbnailUri(AttachmentId attachmentId) { - return getAttachmentDataUri(attachmentId); + return ContentUris.withAppendedId(PART_THUMBNAIL_URI, attachmentId.id); } public static Uri getStickerUri(long id) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index 771b0ebcea..3ae762881c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -49,9 +49,18 @@ public String getContentType() { return attachment.contentType; } + @Nullable + public Uri getThumbnailUri() { + return attachment.getThumbnailUri(); + } + @Nullable public Uri getUri() { - return attachment.getUri(); + Uri attachmentUri = attachment.getUri(); + if (attachmentUri != null) { + return attachmentUri; + } + return attachment.getThumbnailUri(); } public @Nullable Uri getPublicUri() { diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index 2284d9fa5b..889a9799b8 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -61,4 +61,4 @@ message ArchiveAttachmentBackfillJobData { message ArchiveThumbnailUploadJobData { uint64 attachmentId = 1; -} \ No newline at end of file +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt index 2f78a5272f..a7b6f3df29 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt @@ -232,6 +232,7 @@ class UploadDependencyGraphTest { mmsId = AttachmentTable.PREUPLOAD_MESSAGE_ID, hasData = false, hasThumbnail = false, + hasArchiveThumbnail = false, contentType = attachment.contentType, transferProgress = AttachmentTable.TRANSFER_PROGRESS_PENDING, size = attachment.size, @@ -259,7 +260,8 @@ class UploadDependencyGraphTest { dataHash = null, archiveMediaId = null, archiveMediaName = null, - archiveCdn = 0 + archiveCdn = 0, + archiveThumbnailCdn = 0 ) } diff --git a/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt b/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt index a3597070c8..91305387bd 100644 --- a/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt +++ b/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt @@ -32,6 +32,7 @@ object FakeMessageRecords { mmsId: Long = 1, hasData: Boolean = true, hasThumbnail: Boolean = true, + hasArchiveThumbnail: Boolean = false, contentType: String = MediaUtil.IMAGE_JPEG, transferProgress: Int = AttachmentTable.TRANSFER_PROGRESS_DONE, size: Long = 0L, @@ -59,14 +60,17 @@ object FakeMessageRecords { uploadTimestamp: Long = 200, dataHash: String? = null, archiveCdn: Int = 0, + archiveThumbnailCdn: Int = 0, archiveMediaName: String? = null, - archiveMediaId: String? = null + archiveMediaId: String? = null, + archiveThumbnailId: String? = null ): DatabaseAttachment { return DatabaseAttachment( attachmentId, mmsId, hasData, hasThumbnail, + hasArchiveThumbnail, contentType, transferProgress, size, @@ -93,6 +97,7 @@ object FakeMessageRecords { uploadTimestamp, dataHash, archiveCdn, + archiveThumbnailCdn, archiveMediaId, archiveMediaName ) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt index 670f169981..5ca7baa7b1 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt @@ -156,9 +156,13 @@ class ArchiveApi( } } - fun getResumableUploadSpec(uploadForm: AttachmentUploadForm): NetworkResult { + fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult { return NetworkResult.fromFetch { - pushServiceSocket.getResumableUploadSpec(uploadForm) + if (secretKey == null) { + pushServiceSocket.getResumableUploadSpec(uploadForm) + } else { + pushServiceSocket.getResumableUploadSpecWithKey(uploadForm, secretKey) + } } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt index 8c05a55eea..17f8b876db 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt @@ -42,6 +42,10 @@ class BackupKey(val value: ByteArray) { return deriveMediaSecrets(deriveMediaId(mediaName)) } + fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray { + return HKDF.deriveSecrets(value, deriveMediaId(thumbnailMediaName).value, "20240513_Signal_Backups_EncryptThumbnail".toByteArray(), 64) + } + private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial { val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaName.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaName.kt index e0bc242aaf..3c039634d6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaName.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaName.kt @@ -16,6 +16,7 @@ value class MediaName(val name: String) { companion object { fun fromDigest(digest: ByteArray) = MediaName(Base64.encodeWithoutPadding(digest)) fun fromDigestForThumbnail(digest: ByteArray) = MediaName("${Base64.encodeWithoutPadding(digest)}_thumbnail") + fun forThumbnailFromMediaName(mediaName: String) = MediaName("${mediaName}_thumbnail") } fun toByteArray(): ByteArray { 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 598402df91..cc38b0a654 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 @@ -1586,6 +1586,16 @@ public ResumableUploadSpec getResumableUploadSpec(AttachmentUploadForm uploadFor uploadForm.headers); } + public ResumableUploadSpec getResumableUploadSpecWithKey(AttachmentUploadForm uploadForm, byte[] secretKey) throws IOException { + return new ResumableUploadSpec(secretKey, + Util.getSecretBytes(16), + uploadForm.key, + uploadForm.cdn, + getResumableUploadUrl(uploadForm), + System.currentTimeMillis() + CDN2_RESUMABLE_LINK_LIFETIME_MILLIS, + uploadForm.headers); + } + public AttachmentDigest uploadAttachment(PushAttachmentData attachment) throws IOException { if (attachment.getResumableUploadSpec() == null || attachment.getResumableUploadSpec().getExpirationTimestamp() < System.currentTimeMillis()) { From 5e6d9434defacc418915b258cca58706c4aa2496 Mon Sep 17 00:00:00 2001 From: Rashad Sookram Date: Thu, 16 May 2024 09:32:18 -0400 Subject: [PATCH 14/20] Update to RingRTC v2.42.0 --- dependencies.gradle.kts | 2 +- gradle/verification-metadata.xml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 207106862e..5aaef221c7 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -120,7 +120,7 @@ dependencyResolutionManagement { library("libsignal-client", "org.signal", "libsignal-client").versionRef("libsignal-client") library("libsignal-android", "org.signal", "libsignal-android").versionRef("libsignal-client") library("signal-aesgcmprovider", "org.signal:aesgcmprovider:0.0.3") - library("signal-ringrtc", "org.signal:ringrtc-android:2.41.0") + library("signal-ringrtc", "org.signal:ringrtc-android:2.42.0") library("signal-android-database-sqlcipher", "org.signal:sqlcipher-android:4.5.4-S2") // Third Party diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f9421f637e..8904302400 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5719,12 +5719,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + From ec430da7724937517801c733d11a7ab986fd2017 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 16 May 2024 10:20:08 -0400 Subject: [PATCH 15/20] Update translations and other static files. --- app/src/main/res/values-af/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ar/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-az/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-bg/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-bn/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-bs/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ca/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-cs/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-da/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-de/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-el/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-es/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-et/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-eu/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-fa/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-fi/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-fr/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ga/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-gl/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-gu/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-hi/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-hr/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-hu/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-in/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-it/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-iw/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ja/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ka/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-kk/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-km/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-kn/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ko/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ky/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-lt/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-lv/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-mk/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ml/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-mr/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ms/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-my/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-nb/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-nl/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-pa/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-pl/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-pt-rBR/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-pt/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ro/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ru/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sk/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sl/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sq/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sr/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sv/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-sw/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ta/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-te/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-th/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-tl/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-tr/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-ug/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-uk/strings.xml | 57 +++++++++++++++++++--- app/src/main/res/values-ur/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-vi/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-yue/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-zh-rCN/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-zh-rHK/strings.xml | 47 +++++++++++++++++- app/src/main/res/values-zh-rTW/strings.xml | 47 +++++++++++++++++- app/src/main/res/values/strings.xml | 2 +- app/static-ips.gradle.kts | 2 +- 69 files changed, 3089 insertions(+), 74 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 472da4de54..05b0e3dea9 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -225,6 +225,38 @@ Vang Verander kamera Maak galery oop + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Onlangse kontakte @@ -295,7 +327,8 @@ Boodskap %1$s - Signal-oproep %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Verfris kontakte Sien jy iemand nie? Probeer verfris + + Find people you know on Signal + + Allow access to your contacts Meer @@ -2712,6 +2749,14 @@ Vind volgens telefoonnommer Vind volgens gebruikersnaam + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal het toegang tot jou kontakte nodig om dit te kan vertoon. diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 85b2281e7a..cac1a8651f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -225,6 +225,38 @@ التقاط تغيير الكاميرا فتح المعرض + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: جهات الاتصال مؤخّراً @@ -299,7 +331,8 @@ رسالة %1$s - مكالمة سيجنال %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -3015,6 +3048,10 @@ تحديث قائمة الاتصال هل تفتقد إلى جهة اتصال؟ حاول التحديث من جديد + + Find people you know on Signal + + Allow access to your contacts المزيد @@ -3052,6 +3089,14 @@ العثور عبر رقم الهاتف العثور عبر اسم المُستخدم + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. سيجنال بحاجة إلى الوصول إلى جهات الاتصال لديك وذلك بغرض عرضها. diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 7af1d8d25a..eaf6500989 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -225,6 +225,38 @@ Çək Kameranı dəyişdir Qalereyanı aç + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Son əlaqələr @@ -295,7 +327,8 @@ Mesaj %1$s - Signal Zəngi %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Kontaktları yenilə Kimsə nəzərdən qaçıb? Yeniləməyə çalışın + + Find people you know on Signal + + Allow access to your contacts Daha çox @@ -2712,6 +2749,14 @@ Telefon nömrəsinə görə tap İstifadəçi adına görə tap + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal-ın, əlaqələrinizi görüntüləmək üçün müraciətə ehtiyacı var. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 05ae0b7ffa..9eda6b9bc8 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -225,6 +225,38 @@ Запечатване Смяна на камерата Отвори галерията + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Последни контакти @@ -295,7 +327,8 @@ Съобщение %1$s - Signal обаждане %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Опресняване на контактите Някой липсва? Пробвайте да опресните + + Find people you know on Signal + + Allow access to your contacts Още @@ -2712,6 +2749,14 @@ Намиране по телефонен номер Намиране по потребителско име + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal се нуждае от достъп до контактите Ви, за да може да ги покаже. diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index a307ab1c2f..2a2f615176 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -225,6 +225,38 @@ ক্যাপচার ক্যামেরা পরিবর্তন গ্যালারী খুলুন + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: সাম্প্রতিক যোগাযোগ @@ -295,7 +327,8 @@ %1$sটি বার্তা - %1$sটি Signal কল + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ কন্টাক্ট রিফ্রেশ করুন কাউকে মিস করছেন? রিফ্রেশ করার চেষ্টা করুন + + Find people you know on Signal + + Allow access to your contacts আরো @@ -2712,6 +2749,14 @@ ফোন নম্বর দিয়ে খুঁজুন ব্যবহারকারীর নাম দিয়ে খুঁজুন + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal আপনার পরিচিতিসমূহ প্রদর্শন করতে তাদের প্রবেশাধিকার প্রয়োজন। diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 1772950b90..a662e34bb9 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -225,6 +225,38 @@ Slikaj Promijeni kameru Otvori galeriju + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nedavni kontakti @@ -297,7 +329,8 @@ Poruka %1$s - Signal poziv %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Osvježite kontakte Neko nedostaje? Pokušajte osvježiti + + Find people you know on Signal + + Allow access to your contacts Više @@ -2882,6 +2919,14 @@ Pronađite po broju telefona Pronađite po korisničkom imenu + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signalu je potreban pristup Vašim kontaktima kako bi ih mogao prikazati. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 5e79f50dad..348c0c1040 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -225,6 +225,38 @@ Captura Canvia la càmera Obre la galeria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contactes recents @@ -295,7 +327,8 @@ Missatge %1$s - Trucada del Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Actualitzar contactes Falta algú? Prova d\'actualitzar + + Find people you know on Signal + + Allow access to your contacts Més @@ -2712,6 +2749,14 @@ Cerca per número de telèfon Cerca per àlies + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. El Signal necessita accés als contactes per tal de mostrar-los. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 13b4aa93e4..ccacf23753 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -225,6 +225,38 @@ Pořídit Změnit kameru Otevřít galerii + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nedávné kontakty @@ -297,7 +329,8 @@ Zpráva %1$s - Signal volání %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Obnovit kontakty Chybí někdo? Zkuste provést obnovení + + Find people you know on Signal + + Allow access to your contacts Více @@ -2882,6 +2919,14 @@ Najít podle telefonního čísla Najít podle uživatelského jména + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal potřebuje přístup k vašim kontaktům, aby je mohl zobrazit. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 71033ae11c..15d63d5691 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -225,6 +225,38 @@ Tag billede Skift kamera Åbn galleri + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Seneste kontakter @@ -295,7 +327,8 @@ Besked %1$s - Signal-opkald %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Opdater kontakter Mangler der nogen? Prøv at opdatere + + Find people you know on Signal + + Allow access to your contacts Mere @@ -2712,6 +2749,14 @@ Find ud fra telefonnummer Find ud fra brugernavn + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal kræver tilladelse til at tilgå dine kontakter for at kunne vise dem. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2d8c6a5a22..9551681db8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -225,6 +225,38 @@ Aufnehmen Kamera wechseln Galerie öffnen + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Letzte Kontakte @@ -295,7 +327,8 @@ Nachricht %1$s - Signal-Anruf %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Kontakte aktualisieren Fehlt jemand? Versuche es mit Aktualisieren + + Find people you know on Signal + + Allow access to your contacts Mehr @@ -2712,6 +2749,14 @@ Nach Telefonnummer suchen Nach Nutzernamen suchen + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal benötigt Zugriff auf deine Kontakte, um sie anzeigen zu können. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 444dfabde1..6330b67977 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -225,6 +225,38 @@ Λήψη Αλλαγή κάμερας Άνοιγμα συλλογής + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Πρόσφατες επαφές @@ -295,7 +327,8 @@ Μήνυμα %1$s - Κλήση Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Ανανέωση επαφών Λείπει κάποια επαφή; Κάνε ανανέωση + + Find people you know on Signal + + Allow access to your contacts Περισσότερα @@ -2712,6 +2749,14 @@ Αναζήτηση ως αριθμό τηλεφώνου Αναζήτηση ως όνομα χρήστη + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Το Signal χρειάζεται πρόσβαση στις επαφές σου για να μπορέσει να τις εμφανίσει. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0b12c77fd0..518d9f413a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -225,6 +225,38 @@ Capturar Cambiar cámara Abrir galería de fotos + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contactos recientes @@ -295,7 +327,8 @@ Mensaje %1$s - Llamar a %1$s por Signal + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Actualizar contactos ¿Falta alguien? Prueba a actualizar + + Find people you know on Signal + + Allow access to your contacts Más @@ -2712,6 +2749,14 @@ Buscar por núm. de teléfono Buscar por alias + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal necesita acceso a tus contactos para poder mostrarlos. diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 42d61d132a..12ef2a678f 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -225,6 +225,38 @@ Pildista Vaheta kaamerat Ava galerii + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Hiljutised kontaktid @@ -295,7 +327,8 @@ Sõnum %1$s - Signali kõne %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Värskenda kontaktiloendit Keegi on puudu? Ehk aitab värskendamine + + Find people you know on Signal + + Allow access to your contacts Rohkem @@ -2712,6 +2749,14 @@ Otsi telefoninumbri järgi Otsi kasutajanime järgi + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal vajab kontaktide kuvamiseks nendele ligipääsu. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e781012998..3231c4e12b 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -225,6 +225,38 @@ Argazkia Aldatu kamera Ireki galeria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Azkenaldiko kontaktuak @@ -295,7 +327,8 @@ Mezu %1$s - Signal Dei %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Freskatu kontaktuak Norbait faltan? Saiatu freskatzen + + Find people you know on Signal + + Allow access to your contacts Gehiago @@ -2712,6 +2749,14 @@ Bilatu telefono-zenbakiaren arabera Bilatu erabiltzaile-izenaren arabera + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Singalek zure kontaktuak lortzeko baimena behar du erakutsi ahal izateko. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 1ef912fc5b..e38d924b73 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -225,6 +225,38 @@ گرفتن تصویر تغییر دوربین باز کردن گالری + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: آخرین مخاطبین @@ -295,7 +327,8 @@ ارسال پیام به %1$s - تماس سیگنال با %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ تازه‌سازی مخاطبان کسی از قلم افتاده است؟ تازه‌سازی کنید + + Find people you know on Signal + + Allow access to your contacts بیشتر @@ -2712,6 +2749,14 @@ جستجو با شماره تلفن جستجو با نام کاربری + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. سیگنال به دسترسی مخاطبین برای نمایش آن‌ها نیاز دارد. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a571aa5582..c4bc655ba9 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -225,6 +225,38 @@ Ota kuva Vaihda kamera Avaa galleria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Viimeisimmät yhteystiedot @@ -295,7 +327,8 @@ Viesti: %1$s - Signal-puhelu: %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Päivitä yhteystiedot Puuttuuko joku? Kokeile päivittää + + Find people you know on Signal + + Allow access to your contacts Lisää @@ -2712,6 +2749,14 @@ Etsi puhelinnumerolla Etsi käyttäjänimellä + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal tarvitsee luvan käyttää yhteystietojasi, jotta se voi näyttää ne. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d0d9cb393a..cc9a0ba2c1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -225,6 +225,38 @@ Prendre Changer d’appareil photo Ouvrir la galerie + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contacts récents @@ -295,7 +327,8 @@ Message %1$s - Appel Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Actualiser la liste des contacts Il vous manque quelqu\'un ? Essayez d\'actualiser la page + + Find people you know on Signal + + Allow access to your contacts Plus @@ -2712,6 +2749,14 @@ Rechercher par numéro de téléphone Rechercher par nom d’utilisateur + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal a besoin d’accéder à vos contacts afin de les afficher. diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 20535dc0fa..9eb471a24b 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -225,6 +225,38 @@ Gabh é Athraigh an grianghrafadán Oscail an gailearaí + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Teagmhálaithe le déanaí @@ -298,7 +330,8 @@ Teachtaireacht %1$s - Glao Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2931,6 +2964,10 @@ Athnuaigh teagmhálaithe Duine ar iarraidh? Triail athnuachan + + Find people you know on Signal + + Allow access to your contacts Tuilleadh @@ -2967,6 +3004,14 @@ Cuardaigh de réir uimhir ghutháin Aimsigh de réir ainm úsáideora + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Tá gá ag Signal riochtain ar do theagmhálaithe chun iad a thaispeáint. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index f73f51bf0e..e473e834ff 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -225,6 +225,38 @@ Capturar Cambiar cámara Abrir galería + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contactos recentes @@ -295,7 +327,8 @@ Mensaxe %1$s - Chamada Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Actualizar contactos Botas en falla a alguén? Actualiza a listaxe + + Find people you know on Signal + + Allow access to your contacts Máis @@ -2712,6 +2749,14 @@ Buscar por número de teléfono Buscar por usuario + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal necesita acceder aos teus contactos para poder mostralos. diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 6ca77448c4..450aa7485d 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -225,6 +225,38 @@ કેપ્ચર કેમેરો બદલો ગેલેરી ખોલો + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: તાજેતરના સંપર્કો @@ -295,7 +327,8 @@ મેસેજ %1$s - Signal કૉલ %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ સંપર્કો રિફ્રેશ કરો કોઈ ખૂટે છે? રિફ્રેશ કરી જુઓ + + Find people you know on Signal + + Allow access to your contacts વધુ @@ -2712,6 +2749,14 @@ ફોન નંબર દ્વારા શોધો ઉપયોગકર્તા નામ દ્વારા શોધો + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ને તમારા સંપર્કોને પ્રદર્શિત કરવા માટે તેમને એક્સેસની જરૂર છે. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index d45e538906..861e2fa32d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -225,6 +225,38 @@ कैप्चर कैमरा बदलें गैलरी खोलें + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: हाल ही के संपर्क @@ -295,7 +327,8 @@ %1$s को मेसेज - %1$s को Signal कॉल + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ संपर्क रिफ्रेश करें कोई लापता है? रिफ्रेश करने का प्रयास करें + + Find people you know on Signal + + Allow access to your contacts अधिक @@ -2712,6 +2749,14 @@ फोन नंबर द्वारा खोजें यूज़रनेम द्वारा खोजें + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. आपके संपर्कों को प्रदर्शित करने के लिए Signal को आपके संपर्कों तक पहुंच की आवश्यकता है। diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index c80ba3a1bc..ec4a1bf1b7 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -225,6 +225,38 @@ Slikaj Promijeni kameru Otvori galeriju + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nedavni kontakti @@ -297,7 +329,8 @@ Poruka %1$s - Signal poziv %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Osvježi popis kontakata Nedostaje li netko? Pokušajte osvježiti popis kontakata + + Find people you know on Signal + + Allow access to your contacts Više @@ -2882,6 +2919,14 @@ Traži po broju telefona Traži po korisničkom imenu + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal zahtjeva pristup vašim kontaktima kako bi ih mogao prikazati. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 9087f82618..43b3126ab8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -225,6 +225,38 @@ Rögzítés Kamera váltása Galéria megnyitása + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Friss kontaktok @@ -295,7 +327,8 @@ Üzenet %1$s - Signal hívás %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Névjegyek frissítése Hiányzik valaki? Próbáld meg frissíteni + + Find people you know on Signal + + Allow access to your contacts Továbbiak @@ -2712,6 +2749,14 @@ Keresés telefonszám alapján Keresés felhasználónév alapján + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. A Signalnak szüksége van a névjegyeidhez való hozzáférésre annak érdekében, hogy megjelenítse őket. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e64844c39e..66d66cda90 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -225,6 +225,38 @@ Ambil gambar Ganti kamera Buka galeri + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Kontak-kontak terbaru @@ -294,7 +326,8 @@ Pesan %1$s - Panggilan Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ Segarkan kontak Ada yang belum muncul? Coba segarkan + + Find people you know on Signal + + Allow access to your contacts Selanjutnya @@ -2627,6 +2664,14 @@ Cari menurut nomor telepon Cari menurut nama pengguna + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal memerlukan akses ke daftar kontak Anda supaya dapat menampilkannya. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dec9fd8e6e..0aa04ba1ac 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -225,6 +225,38 @@ Scatta Cambia camera Apri galleria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contatti recenti @@ -295,7 +327,8 @@ Messaggio %1$s - Chiamata Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Aggiorna contatti Non è presente un contatto? Aggiorna la pagina + + Find people you know on Signal + + Allow access to your contacts Altro @@ -2712,6 +2749,14 @@ Trovarmi tramite numero di telefono Cerca tramite nome utente + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal richiede l\'accesso ai tuoi contatti per poterli mostrare. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 784808ea77..dd072165ee 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -225,6 +225,38 @@ לכוד שנה מצלמה פתח גלריה + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: אנשי קשר אחרונים @@ -297,7 +329,8 @@ %1$s של הודעה - שיחת Signal אל %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ רענון אנשי קשר מישהו חסר? אפשר לנסות לרענן + + Find people you know on Signal + + Allow access to your contacts עוד @@ -2882,6 +2919,14 @@ מצא לפי מספר טלפון מצא לפי שם משתמש + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal צריך הרשאה אל אנשי הקשר שלך על מנת להציגם. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2d62d09292..d7a0894229 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -225,6 +225,38 @@ キャプチャ カメラを変更 ギャラリーを開く + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 最近使った連絡先 @@ -294,7 +326,8 @@ %1$s にメッセージを送る - Signal通話 %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 連絡先を更新 連絡先が見つからない場合は更新してみましょう + + Find people you know on Signal + + Allow access to your contacts その他 @@ -2627,6 +2664,14 @@ 電話番号で検索 ユーザーネームで検索 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. 連絡先を表示するには、Signalに連絡先へのアクセスを許可してください。 diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index cce2e6278a..ba54fa3629 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -225,6 +225,38 @@ გადაღება კამერის შეცვლა გალერეის გახსნა + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: ბოლო კონტაქტები @@ -295,7 +327,8 @@ შეტყობინება %1$s - Signal-ის ზარი %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ კონტაქტების გადატვირთვა ვინმე ვერ იპოვე? გადატვირთვა სცადე + + Find people you know on Signal + + Allow access to your contacts მეტი @@ -2712,6 +2749,14 @@ ძებნა მობილურის ნომრით ძებნა მომხმარებლის სახელით + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal-ს შენს კონტაქტებზე წვდომა სჭირდება, რათა ისინი აჩვენოს. diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 538fc31a15..6ea4b735a3 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -225,6 +225,38 @@ Түсіру Камераны ауыстыру Галереяны ашу + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Соңғы контактілер @@ -295,7 +327,8 @@ %1$s нөміріне хат жазу - Signal арқылы %1$s нөміріне қоңырау шалу + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Контактілерді жаңарту Біреу жоқ па? Жаңартып көріңіз + + Find people you know on Signal + + Allow access to your contacts Толығырақ @@ -2712,6 +2749,14 @@ Телефон нөмірі бойынша іздеу Пайдаланушы аты бойынша іздеу + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Контактілеріңізді көрсету үшін Signal-да оларды ашуға рұқсат болу керек. diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index 2db58ee3f8..e34c3ed032 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -225,6 +225,38 @@ ថត ប្តូរកាមេរ៉ា បើកវិចិត្រសាល + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: បញ្ជីទំនាក់ទំនងថ្មីៗ @@ -294,7 +326,8 @@ សារ %1$s - ការហៅចេញរបស់Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ ផ្ទុកឈ្មោះទំនាក់ទំនងឡើងវិញ បាត់នរណាម្នាក់ឬ? សាកល្បងផ្ទុកឡើងវិញ + + Find people you know on Signal + + Allow access to your contacts ច្រើនទៀត @@ -2627,6 +2664,14 @@ ស្វែងរកតាមលេខទូរសព្ទ ស្វែងរកតាមឈ្មោះអ្នកប្រើ + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ត្រូវការចូលប្រើប្រាស់បញ្ជីទំនាក់ទំនងរបស់អ្នក ដើម្បីបង្ហាញវា។ diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 15c9aedcd4..6b41d8badc 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -225,6 +225,38 @@ ಸೆರೆಹಿಡಿಯಿರಿ ಕ್ಯಾಮರಾ ಬದಲಾಯಿಸಿ ಗ್ಯಾಲರಿ ತೆರೆಯಿರಿ + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: ಇತ್ತೀಚಿನ ಸಂಪರ್ಕಗಳು @@ -295,7 +327,8 @@ ಸಂದೇಶ %1$s - Signal ಕರೆ %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ ಸಂಪರ್ಕಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ ಯಾರಾದರೂ ತಪ್ಪಿಹೋದಂತೆ ಅನಿಸುತ್ತಿದೆಯೇ? ರಿಫ್ರೆಶ್ ಮಾಡಲು ಪ್ರಯತ್ನಿಸಿ + + Find people you know on Signal + + Allow access to your contacts ಇನ್ನಷ್ಟು @@ -2712,6 +2749,14 @@ ಫೋನ್ ನಂಬರ್‌ನಿಂದ ಹುಡುಕಿ ಯೂಸರ್‌ನೇಮ್‌ನಿಂದ ಹುಡುಕಿ + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ಗೆ ನಿಮ್ಮ ಸಂಪರ್ಕಗಳನ್ನು ಪ್ರದರ್ಶಿಸಲು ಪ್ರವೇಶ ಅಗತ್ಯವಿದೆ. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8eba26d77c..d3064a38fa 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -225,6 +225,38 @@ 캡처 카메라 변경 갤러리 열기 + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 최근 연락처 @@ -294,7 +326,8 @@ %1$s에 메시지 보내기 - %1$s에 Signal 전화 + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 연락처 새로 고침 안 보이는 사람이 있나요? 새로 고쳐보세요. + + Find people you know on Signal + + Allow access to your contacts 더 보기 @@ -2627,6 +2664,14 @@ 전화번호로 검색 사용자명으로 검색 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal에서 연락처를 표시하려면 연락처 권한이 필요합니다. diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index bac75410e7..b72fe88428 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -225,6 +225,38 @@ Тартуу Камераны алмаштыруу Галереяны ачуу + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Соңку байланыштар @@ -294,7 +326,8 @@ Билдирүү %1$s - Signal аркылуу %1$s номерине чалуу + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ Байланыштарды жаңыртуу Кимдир-бирөө жетишпей жатабы? Жаңыртып көрүңүз + + Find people you know on Signal + + Allow access to your contacts Дагы @@ -2627,6 +2664,14 @@ Телефон номери боюнча издөө Колдонуучу аты боюнча издөө + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Байланыштарды көрсөтүү үчүн Signal\'га алар жеткиликтүү болушу керек. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 906ab24a13..486b3f15fa 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -225,6 +225,38 @@ Fotografuoti Keisti kamerą Atverti galeriją + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Paskiausi adresatai @@ -297,7 +329,8 @@ Žinutė %1$s - Signal skambutis %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Atnaujinti kontaktus Kažko trūksta? Bandyk atnaujinti + + Find people you know on Signal + + Allow access to your contacts Daugiau @@ -2882,6 +2919,14 @@ Rasti pagal telefono numerį Rasti pagal naudotojo vardą + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Norint rodyti jūsų adresatus, Signal reikia prieigos prie jų. diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index c045e5c31d..e077b77d4f 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -225,6 +225,38 @@ Uzņemt Nomainīt kameru Atvērt galeriju + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Pēdējie kontakti @@ -296,7 +328,8 @@ Ziņa %1$s - Signal zvans %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2763,6 +2796,10 @@ Atsvaidzināt kontaktus Kāda pietrūkst? Mēģiniet atsvaidzināt + + Find people you know on Signal + + Allow access to your contacts Vēl @@ -2797,6 +2834,14 @@ Meklēt pēc tālruņa numura Atrast pēc lietotājvārda + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal nepieciešama pieeja jūsu kontaktiem, lai varētu tos parādīt. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916d6163fb..76ef14aa0a 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -225,6 +225,38 @@ Сними Смени камера Отвори галерија + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Скорешни контакти @@ -295,7 +327,8 @@ Порака %1$s - Signal повик %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Освежете ги контактите Ви недостига некој? Пробајте да ги освежите контактите + + Find people you know on Signal + + Allow access to your contacts Повеќе @@ -2712,6 +2749,14 @@ Пронајдете со телефонски број Пронајдете со корисничко име + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. На Signal му треба пристап до Вашите контакти за да може да ги прикаже. diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d2a28cee18..1773b06fac 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -225,6 +225,38 @@ ക്യാപ്‌ചർ ചെയ്യുക ക്യാമറ മാറ്റുക ഗാലറി തുറക്കുക + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: സമീപകാല കോൺടാക്റ്റുകൾ @@ -295,7 +327,8 @@ സന്ദേശ൦ %1$s - %1$s എന്നയാളെ Signal കോള്‍ ചെയ്യുക + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ കോൺടാക്റ്റുകൾ പുതുക്കുക ആരെയെങ്കിലും മിസ് ചെയ്യുന്നുണ്ടോ? റീഫ്രഷ് ചെയ്ത് നോക്കൂ + + Find people you know on Signal + + Allow access to your contacts കൂടുതൽ @@ -2712,6 +2749,14 @@ ഫോൺ നമ്പർ ഉപയോഗിച്ച് കണ്ടെത്തുക ഉപയോക്തൃനാമം ഉപയോഗിച്ച് കണ്ടെത്തുക + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal-ന് നിങ്ങളുടെ കോൺടാക്റ്റുകൾ പ്രദർശിപ്പിക്കുന്നതിന് അവയിലേക്ക് ആക്സസ് ആവശ്യമാണ്. diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 64bcfbff6a..cc593eb8ac 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -225,6 +225,38 @@ कॅप्चर करा कॅमेरा बदला गॅलरी उघडा + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: अलीकडील संपर्क @@ -295,7 +327,8 @@ संदेश %1$s - Signal कॉल %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ संपर्क रिफ्रेश करा कोणाची आठवण येत आहे? रिफ्रेश करून पहा + + Find people you know on Signal + + Allow access to your contacts अधिक @@ -2712,6 +2749,14 @@ फोन नंबरद्वारे शोधा वापरकर्ता नावाने शोधा + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. आपले संपर्क दाखविण्यासाठी Signal ला त्यांना अॅक्सेस करण्याची गरज आहे. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 192f32a13f..18a069de52 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -225,6 +225,38 @@ Tangkap Tukar kamera Buka geleri + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Kenalan terbaharu @@ -294,7 +326,8 @@ Mesej %1$s - Panggilan Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ Segar semula kenalan Rindukan seseorang? Cuba segarkan semula + + Find people you know on Signal + + Allow access to your contacts Lagi @@ -2627,6 +2664,14 @@ Cari dengan nombor telefon Cari dengan nama pengguna + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal memerlukan akses kepada kenalan anda untuk memaparkannya. diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 69d0cbd273..856e209ffc 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -225,6 +225,38 @@ ဓာတ်ပုံရိုက်သည် ကင်မရာပြောင်းရန် ပုံပြခန်းကို ဖွင့်ပါ + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: နောက်ဆုံး အဆက်အသွယ်များ @@ -294,7 +326,8 @@ စာတို %1$s - Signal ခေါ်ဆိုမှု %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ အဆက်အသွယ်များကို ပြန်လည်လုပ်ဆောင်ခြင်း တစ်စုံတစ်ဦး ပျောက်နေပါသလား။ ပြန်လည်လုပ်ဆောင်ကြည့်ပါ + + Find people you know on Signal + + Allow access to your contacts နောက်ထပ် @@ -2627,6 +2664,14 @@ ဖုန်းနံပါတ်ဖြင့် ရှာရန် သုံးစွဲသူအမည်ဖြင့် ရှာရန် + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. မိမိ၏ အဆက်အသွယ်များကို ပြသရန် Signal က အဆက်အသွယ်များကို ရယူရန်လိုသည်။ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index fae9673a5a..a0854c55bb 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -225,6 +225,38 @@ Ta bilde Bytt kamera Åpne galleri + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nylige kontakter @@ -295,7 +327,8 @@ Melding %1$s - Signal-anrop %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Oppdater kontakter Mangler det noen? Last inn kontaktene på nytt + + Find people you know on Signal + + Allow access to your contacts Mer @@ -2712,6 +2749,14 @@ Søk på telefonnummer Søk på brukernavn + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal må ha tilgang til kontaktene dine for å kunne vise dem. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5ed1bf92b0..23588f488f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -225,6 +225,38 @@ Opnemen Camera wisselen Galerij openen + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Recente contactpersonen @@ -295,7 +327,8 @@ Bericht sturen naar %1$s - Signal-oproep %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Contacten verversen Ontbreekt er iemand? Probeer te verversen + + Find people you know on Signal + + Allow access to your contacts Meer opties @@ -2712,6 +2749,14 @@ Zoeken op telefoonnummer Zoeken op gebruikersnaam + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal heeft toegang tot je contacten nodig om contactpersonen te kunnen weergeven. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 129c5eeb40..899f0351a6 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -225,6 +225,38 @@ ਤਸਵੀਰ ਖਿੱਚੋ ਕੈਮਰਾ ਬਦਲੋ ਗੈਲਰੀ ਖੋਲ੍ਹੋ + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: ਤਾਜ਼ਾ ਸੰਪਰਕ @@ -295,7 +327,8 @@ ਸੁਨੇਹਾ %1$s - Signal ਕਾਲ %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ ਸੰਪਰਕਾਂ ਨੂੰ ਤਾਜ਼ਾ ਕਰੋ ਕੋਈ ਸੰਪਰਕ ਨਹੀਂ ਦਿਖ ਰਿਹਾ? ਤਾਜ਼ਾ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ + + Find people you know on Signal + + Allow access to your contacts ਹੋਰ @@ -2712,6 +2749,14 @@ ਫ਼ੋਨ ਨੰਬਰ ਨਾਲ ਲੱਭੋ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਨਾਲ ਲੱਭੋ + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ਨੂੰ ਉਹਨਾਂ ਨੂੰ ਪ੍ਰਦਰਸ਼ਿਤ ਕਰਨ ਲਈ ਤੁਹਾਡੇ ਸੰਪਰਕਾਂ ਤੱਕ ਪਹੁੰਚ ਦੀ ਲੋੜ ਹੈ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 07a4653dba..87ca4ae0b2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -225,6 +225,38 @@ Zrób zdjęcie Zmień aparat Otwórz galerię + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Ostatnie kontakty @@ -297,7 +329,8 @@ Wiadomość %1$s - Połączenie Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Odśwież kontakty Brakuje kogoś? Spróbuj odświeżyć + + Find people you know on Signal + + Allow access to your contacts Więcej @@ -2882,6 +2919,14 @@ Znajdź po numerze telefonu Znajdź po nazwie użytkownika + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal wymaga dostępu do Twoich kontaktów w celu wyświetlenia ich. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ee067610e6..5d4c9ead66 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -225,6 +225,38 @@ Capturar Mudar de câmera Abrir galeria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contatos recentes @@ -295,7 +327,8 @@ Mensagem %1$s - Chamada Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Atualizar contatos Está faltando alguém? Tente atualizar + + Find people you know on Signal + + Allow access to your contacts Mais @@ -2712,6 +2749,14 @@ Encontrar pelo número de telefone Encontrar pelo nome de usuário + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. O Signal precisa de acesso aos seus contatos para poder exibi-los. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3f4c60ae51..71e47a9f20 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -225,6 +225,38 @@ Capturar Alterar câmara Abrir galeria + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contactos recentes @@ -295,7 +327,8 @@ Mensagem %1$s - Chamada do Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Atualizar contactos Falta alguém? Tente atualizar + + Find people you know on Signal + + Allow access to your contacts Mais @@ -2712,6 +2749,14 @@ Procurar por número de telefone Procurar por nome de utilizador + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. O Signal precisa de ter acesso aos contactos para os poder mostrar. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 22991f5dfa..846c69e2cb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -225,6 +225,38 @@ Capturează Schimbă camera Deschide galerie + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Contacte recente @@ -296,7 +328,8 @@ Mesaj %1$s - Apel Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2763,6 +2796,10 @@ Reîncarcă contactele Îți lipsește cineva? Încearcă să reîncarci + + Find people you know on Signal + + Allow access to your contacts Mai multe @@ -2797,6 +2834,14 @@ Caută după numărul de telefon Caută după nume de utilizator + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal are nevoie de acces la contactele tale pentru a le afișa. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bff5cb95eb..40a7f6334e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -225,6 +225,38 @@ Снять Сменить камеру Открыть галерею + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Недавние контакты @@ -297,7 +329,8 @@ Сообщение %1$s - Позвонить через Signal на %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Обновить контакты Не можете кого-то найти? Попробуйте обновить + + Find people you know on Signal + + Allow access to your contacts Больше @@ -2882,6 +2919,14 @@ Поиск по номеру телефона Поиск по имени пользователя + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal необходим доступ к вашим контактам, чтобы отобразить их. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 25f0444670..3092e8fa19 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -225,6 +225,38 @@ Zachytiť Zmeniť fotoaparát Otvoriť galériu + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nedávne kontakty @@ -297,7 +329,8 @@ Správa %1$s - Signal Hovor %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Obnoviť kontakty Niekto tu chýba? Skúste obnoviť + + Find people you know on Signal + + Allow access to your contacts Viac @@ -2882,6 +2919,14 @@ Nájsť podľa telefónneho čísla Nájsť podľa používateľského mena + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal potrebuje prístup k Vašim kontaktom, aby ich mohol zobraziť. diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index beef93a383..ca0a7f0e9c 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -225,6 +225,38 @@ Zajemi Zamenjaj kamero Odpri galerijo + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Nedavni stiki @@ -297,7 +329,8 @@ Sporočilo %1$s - Klic Signal: %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Osveži stike Pogrešate koga? Poskusite osvežiti + + Find people you know on Signal + + Allow access to your contacts Več @@ -2882,6 +2919,14 @@ Iskanje po telefonski številki Iskanje po uporabniškem imenu + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Za prikaz imen iz vašega imenika potrebuje aplikacija Signal dostop do stikov. diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 523c099154..a25bf9f1d2 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -225,6 +225,38 @@ Shkrep Ndërroni kameran Hap albumet + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Kontakte më të fundit @@ -295,7 +327,8 @@ %1$s mesazhi - Telefonatë Signal me %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Rifresko kontaktet A të mungon njeri? Provo të rifreskosh + + Find people you know on Signal + + Allow access to your contacts Më shumë @@ -2712,6 +2749,14 @@ Gjeni sipas numrit të telefonit Gjeni sipas emrit të përdoruesit + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Që të mund t\\’i shfaqë, Signal-i lyp leje përdorimi të kontakteve. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 6dc6ce58bd..f93e9ef4d4 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -225,6 +225,38 @@ Сними Промени камеру Отвори галерију + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Недавни контакти @@ -295,7 +327,8 @@ Порука: %1$s - Позив преко Signal-a: %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Освежите контакте Неко вам недостаје? Пробајте да освежите + + Find people you know on Signal + + Allow access to your contacts Још @@ -2712,6 +2749,14 @@ Пронађите по броју телефона Пронађите корисничким именом + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal-у је потребан приступ вашим контактима да би их приказао. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b0f52453a4..26c2127232 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -225,6 +225,38 @@ Ta foto Byt kamera Öppna galleri + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Senaste kontakter @@ -295,7 +327,8 @@ Meddelande %1$s - Signal-samtal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Uppdatera kontakter Saknar du någon? Prova att uppdatera + + Find people you know on Signal + + Allow access to your contacts Mer @@ -2712,6 +2749,14 @@ Hitta via telefonnummer Hitta via användarnamn + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal behöver åtkomst till dina kontakter för att visa dem. diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 3c3a758c22..b3d10a95a9 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -225,6 +225,38 @@ Piga Geuza kamera Fungua kitunzio + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Mawasiliano ya hivi karibuni @@ -295,7 +327,8 @@ Ujumbe %1$s - Simu ya Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Huisha upya wawasiliani Umemkosa mtu? Jaribu kuhuisha upya + + Find people you know on Signal + + Allow access to your contacts Zaidi @@ -2712,6 +2749,14 @@ Tafuta kwa nambari ya simu Tafuta kwa jina la mtumiaji + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal inahitaji upatikanaji wa wawasiliani yako ili kuonyesha. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index a500e3ca2c..ec6e71e840 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -225,6 +225,38 @@ பிடிப்பு கேமராவை மாற்றவும் கேலரியை திற + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: சமீபத்திய தொடர்புகள் @@ -295,7 +327,8 @@ செய்தி %1$s - Signal அழைப்பு %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ தொடர்பு பட்டியலை புதுப்பி யாரையாவது காணவில்லையா? புதுப்பித்து முயல்க + + Find people you know on Signal + + Allow access to your contacts மேலும் @@ -2712,6 +2749,14 @@ தொலைபேசி எண் மூலம் கண்டுபிடிக்கவும் பயனர் பெயர் மூலம் கண்டறியவும் + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. உங்கள் தொடர்புகளைக் காண்பிக்க சிக்னலுக்கு அணுகல் தேவை. diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 2fa9d350f9..efe61a0797 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -225,6 +225,38 @@ క్యాప్చర్ కెమెరాను మార్చండి చిత్రశాల ను తెరువు + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: ఇటీవలి పరిచయాలు @@ -295,7 +327,8 @@ %1$sకి సందేశం పంపు - Signal కాల్ %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ కాంటాక్ట్‌లను రిఫ్రెష్ చేయండి ఏవరిదైనా కాంటాక్ట్ కనిపించడం లేదా? రిఫ్రెష్ చేసి చూడండి + + Find people you know on Signal + + Allow access to your contacts మరిన్ని @@ -2712,6 +2749,14 @@ ఫోన్ నెంబర్ ద్వారా కనుగొనండి యూజర్‌నేమ్ ద్వారా కనుగొనండి + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. మీ పరిచయాలను Signal ప్రదర్శించడానికి పరిచయాలకు ప్రాప్యత అవసరం diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 13b72278ba..be49865b6b 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -225,6 +225,38 @@ ถ่ายรูป เปลี่ยนกล้อง เปิดอัลบั้มภาพ + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: ผู้ที่ติดต่อล่าสุด @@ -294,7 +326,8 @@ ข้อความ %1$s - สาย Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ รีเฟรชรายชื่อผู้ติดต่อ หากไม่เห็นผู้ติดต่อ ให้ลองกดรีเฟรชรายชื่อ + + Find people you know on Signal + + Allow access to your contacts เพิ่มเติม @@ -2627,6 +2664,14 @@ ค้นหาด้วยหมายเลขโทรศัพท์ ค้นหาด้วยชื่อผู้ใช้ + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ต้องการเข้าถึงผู้ติดต่อของคุณ เพื่อจะแสดงข้อมูลเหล่านั้นได้ diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 361e9d79f9..213824a20c 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -225,6 +225,38 @@ Kunan Magpalit ng Camera Buksan ang gallery + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Mga bagong kontak @@ -295,7 +327,8 @@ Mensahe %1$s - Tawag sa Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ I-refresh ang contacts May nawawala ka bang contact? Subukang mag-refresh + + Find people you know on Signal + + Allow access to your contacts More @@ -2712,6 +2749,14 @@ Hanapin gamit ang phone number Hanapin gamit ang username + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Kailangan ng Signal ng access sa iyong mga kontak upang maipakita ang mga ito. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 056a0a1710..ee04f0b88a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -225,6 +225,38 @@ Yakala Kamerayı değiştir Galeriyi aç + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Son kişiler @@ -295,7 +327,8 @@ İleti %1$s - Signal Araması %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ Kişileri yenile Birisi mi eksik? Yenilemeyi dene + + Find people you know on Signal + + Allow access to your contacts Daha fazla @@ -2712,6 +2749,14 @@ Telefon numarasına göre bul Kullanıcı adına göre bul + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal\'in kişilerinizi gösterebilmek için erişime ihtiyacı var. diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 453f649c19..7196b099ee 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -225,6 +225,38 @@ ئېكران تۇتۇش كامېرا ئالماشتۇر سۈرەتداننى ئاچ + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: يېقىنقى ئالاقەداشلار @@ -294,7 +326,8 @@ %1$s گە ئۇچۇر قىلىش - %1$s نى Signal دا چاقىرىش + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ ئالاقىداشلارنى يېڭىلاش بىرەرسى تېپىلمىدىمۇ؟ يېڭىلاپ سىناپ بېقىڭ + + Find people you know on Signal + + Allow access to your contacts تېخىمۇ كۆپ @@ -2627,6 +2664,14 @@ تېلېفون نومۇرى بىلەن تېپىڭ ئىشلەتكۈچى نامى بويىچە ئىزدەش + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal ئۇلارنى كۆرسىتىش ئۈچۈن ئالاقەداشلىرىڭىزنى زىيارەت قىلىشى كېرەك. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index aac1295c97..ef250506b8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -225,6 +225,38 @@ Захопити Змінити камеру Відкрити галерею + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Нещодавні контакти @@ -297,7 +329,8 @@ Повідомлення %1$s - Виклик у Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2847,6 +2880,10 @@ Оновити контакти Когось не вистачає? Спробуйте оновити + + Find people you know on Signal + + Allow access to your contacts Більше @@ -2882,6 +2919,14 @@ Пошук за номером телефону Пошук за ім\'ям користувача + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal потребує дозволів \"Контакти\", щоб показати контакти. @@ -3197,9 +3242,9 @@ - Щоб перевірити наявність наскрізного шифрування з користувачем %1$s, порівняйте наведені вище цифри із цифрами на пристрої вашого співрозмовника. Користувач також може відсканувати ваш код за допомогою свого пристрою. + Щоб перевірити наявність наскрізного шифрування з користувачем %1$s, порівняйте наведені вище цифри із цифрами на пристрої цього співрозмовника. Також можна зісканувати код з пристрою користувача. Натисніть, щоб сканувати - Збіг вдалий + Коди збігаються Не вдалося перевірити код безпеки Завантаження… Позначити як перевірений @@ -4099,13 +4144,13 @@ - Вхідний голосовий виклик Signal + Вхідний аудіовиклик у Signal Вхідний відеовиклик Signal Вхідний груповий виклик Signal - Триває голосовий виклик Signal + Триває аудіовиклик у Signal Триває відеовиклик Signal @@ -6707,7 +6752,7 @@ Почати відеодзвінок - Почати голосовий виклик + Почати аудіовиклик diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index e94f65c97f..db9cbaad0e 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -225,6 +225,38 @@ کھینچیں کیمرہ تبدیل کریں گیلری کھولیں + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: موجودہ رابطے @@ -295,7 +327,8 @@ پیغام %1$s - Signal کال %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2679,6 +2712,10 @@ رابطوں کو ری فریش کریں کوئی مل نہیں رہا؟ ری فریش کرتے رہیں + + Find people you know on Signal + + Allow access to your contacts مزید @@ -2712,6 +2749,14 @@ فون نمبر سے تلاش کریں یوزر نیم سے تلاش کریں + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal کو ظاہر کرنے کے لئے آپ کے رابطوں تک رسائی کی ضرورت ہے۔ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f342483687..80cf0ee5d3 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -225,6 +225,38 @@ Chụp Chuyển camera Mở bộ sưu tập + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Liên hệ gần đây @@ -294,7 +326,8 @@ Tin nhắn %1$s - Cuộc gọi Signal %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ Làm mới danh bạ Thiếu ai đó? Hãy thử làm mới + + Find people you know on Signal + + Allow access to your contacts Thêm @@ -2627,6 +2664,14 @@ Tìm theo số điện thoại Tìm theo tên người dùng + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal cần quyền truy cập danh bạ để hiển thị chúng. diff --git a/app/src/main/res/values-yue/strings.xml b/app/src/main/res/values-yue/strings.xml index 85d928cf6a..3bba168727 100644 --- a/app/src/main/res/values-yue/strings.xml +++ b/app/src/main/res/values-yue/strings.xml @@ -225,6 +225,38 @@ 撳掣 轉相機 打開圖片庫 + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 最近嘅聯絡人 @@ -294,7 +326,8 @@ 寫個訊息畀 %1$s - 同 %1$s 用 Signal 通話 + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 重新整理聯絡人 揾唔到某個人?試下重新整理 + + Find people you know on Signal + + Allow access to your contacts 更多 @@ -2627,6 +2664,14 @@ 用電話冧把嚟搵 用用戶名稱嚟搵 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal 要存取您嘅聯絡人,先可以顯示畀您睇。 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6d301a80cf..a5d40be6d9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -225,6 +225,38 @@ 拍照 切换相机 打开相册 + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 最近联系人 @@ -294,7 +326,8 @@ 消息 %1$s - Signal 呼叫 %1$s + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 刷新联系人 找不到某个好友?请刷新试试 + + Find people you know on Signal + + Allow access to your contacts 更多 @@ -2627,6 +2664,14 @@ 按手机号码查找 按用户名查找 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. 为了显示联系人,Signal 需要其访问权限。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 7898dbc2d6..e470f3caf5 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -225,6 +225,38 @@ 擷取 切換鏡頭 開啟圖片庫 + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 最近的聯絡人 @@ -294,7 +326,8 @@ 傳送訊息給 %1$s - 與 %1$s 進行 Signal 通話 + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 重新整理聯絡人 找不到某人?嘗試重新整理 + + Find people you know on Signal + + Allow access to your contacts 更多 @@ -2627,6 +2664,14 @@ 以電話號碼搜尋 以用戶名稱搜尋 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal 需要存取您的聯絡人以便為您顯示。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 94e4270586..3db6f2528d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -225,6 +225,38 @@ 擷取 切換相機 開啟相簿 + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: 最近的聯絡人 @@ -294,7 +326,8 @@ 傳送訊息給 %1$s - 與 %1$s 進行 Signal 通話 + Signal Voice Call %1$s + Signal Video Call %1$s @@ -2595,6 +2628,10 @@ 重新整理聯絡人 找不到某人?嘗試重新整理 + + Find people you know on Signal + + Allow access to your contacts 更多 @@ -2627,6 +2664,14 @@ 以電話號碼搜尋 以使用者名稱搜尋 + + Allow access to contacts + + To find people you know on Signal: + + Allow access + + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. Signal 需要存取你的\"聯絡人\"以顯示聯絡人資訊。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96d1187385..d8f4313f52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2758,7 +2758,7 @@ To find people you know on Signal: Allow access - + Allow access to your contacts. Your contacts are encrypted and not visible to the Signal service. diff --git a/app/static-ips.gradle.kts b/app/static-ips.gradle.kts index acd8bac774..29432a2514 100644 --- a/app/static-ips.gradle.kts +++ b/app/static-ips.gradle.kts @@ -1,5 +1,5 @@ rootProject.extra["service_ips"] = """new String[]{"13.248.212.111","76.223.92.165"}""" -rootProject.extra["storage_ips"] = """new String[]{"142.251.41.19"}""" +rootProject.extra["storage_ips"] = """new String[]{"142.250.65.211"}""" rootProject.extra["cdn_ips"] = """new String[]{"18.238.49.106","18.238.49.6","18.238.49.66","18.238.49.90"}""" rootProject.extra["cdn2_ips"] = """new String[]{"104.18.37.148","172.64.150.108"}""" rootProject.extra["cdn3_ips"] = """new String[]{"104.18.37.148","172.64.150.108"}""" From 5741dfc00bc65ac168c05b44d59a0574f5c2d3b2 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 16 May 2024 10:24:48 -0400 Subject: [PATCH 16/20] Bump version to 7.8.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f997ab113..4d7ef58c21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ plugins { apply(from = "static-ips.gradle.kts") -val canonicalVersionCode = 1419 -val canonicalVersionName = "7.7.2" +val canonicalVersionCode = 1420 +val canonicalVersionName = "7.8.0" val postFixSize = 100 val abiPostFix: Map = mapOf( From e0f3b3580566daa105d6e19064591347e4ee0a25 Mon Sep 17 00:00:00 2001 From: Clark Chen Date: Thu, 16 May 2024 12:30:33 -0400 Subject: [PATCH 17/20] Fix missing archive_thumbnail_cdn column. --- .../main/java/org/thoughtcrime/securesms/database/MediaTable.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index 6855225b4c..ac982c6253 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -54,6 +54,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_NAME}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_ID}, + ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_THUMBNAIL_CDN}, ${MessageTable.TABLE_NAME}.${MessageTable.TYPE}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED}, From 241bf065e829afee0a11a3c4b5a9ed00eaf1fdcb Mon Sep 17 00:00:00 2001 From: Clark Date: Thu, 16 May 2024 13:09:58 -0400 Subject: [PATCH 18/20] Fix missing thumbnail_file column in media query. --- .../main/java/org/thoughtcrime/securesms/database/MediaTable.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index ac982c6253..773bb7975d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -28,6 +28,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE}, + ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_KEY}, From 1bf9695cff346bb35c7801c418aecd93469552cf Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 16 May 2024 15:45:51 -0400 Subject: [PATCH 19/20] Update translations and other static files. --- app/src/main/res/values-uk/strings.xml | 2 +- app/static-ips.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ef250506b8..220a04b8f4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -838,7 +838,7 @@ Завтра - Сьогодні ввечері + Сьогодні diff --git a/app/static-ips.gradle.kts b/app/static-ips.gradle.kts index 29432a2514..35bd7fbde7 100644 --- a/app/static-ips.gradle.kts +++ b/app/static-ips.gradle.kts @@ -1,5 +1,5 @@ rootProject.extra["service_ips"] = """new String[]{"13.248.212.111","76.223.92.165"}""" -rootProject.extra["storage_ips"] = """new String[]{"142.250.65.211"}""" +rootProject.extra["storage_ips"] = """new String[]{"142.250.80.115"}""" rootProject.extra["cdn_ips"] = """new String[]{"18.238.49.106","18.238.49.6","18.238.49.66","18.238.49.90"}""" rootProject.extra["cdn2_ips"] = """new String[]{"104.18.37.148","172.64.150.108"}""" rootProject.extra["cdn3_ips"] = """new String[]{"104.18.37.148","172.64.150.108"}""" From eb114de5c8486f6cb3f82a7ce5baed0564b3f6c8 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 16 May 2024 15:50:48 -0400 Subject: [PATCH 20/20] Bump version to 7.8.1 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4d7ef58c21..ab04069e9a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ plugins { apply(from = "static-ips.gradle.kts") -val canonicalVersionCode = 1420 -val canonicalVersionName = "7.8.0" +val canonicalVersionCode = 1421 +val canonicalVersionName = "7.8.1" val postFixSize = 100 val abiPostFix: Map = mapOf(