diff --git a/.editorconfig b/.editorconfig index c0a90ddfbb..79e27bbed7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,8 @@ root = true -[*.kt] +[*.{kt,kts}] indent_size = 2 ktlint_standard_trailing-comma-on-call-site = disable ktlint_standard_trailing-comma-on-declaration-site = disable ktlink_standard_spacing-between-declarations-with-annotations = disable +ktlint_code_style = intellij_idea \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b287ee6864..4c333c1eb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,11 +33,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: Run tests diff --git a/app/build.gradle b/app/build.gradle index d9e60680cf..4a9daa6952 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,8 +56,8 @@ ext { MAPS_API_KEY = getEnv('CI_MAPS_API_KEY') ?: mapsApiKey } -def canonicalVersionCode = 1283 -def canonicalVersionName = "6.24.4" +def canonicalVersionCode = 1289 +def canonicalVersionName = "6.25.5" def mollyRevision = 0 def postFixSize = 100 @@ -160,7 +160,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = '1.3.2' + kotlinCompilerExtensionVersion = '1.4.4' } if (mollyRevision < 0 || mollyRevision >= postFixSize) { @@ -394,10 +394,14 @@ android { setIgnore(true) } } -} -tasks.withType(JavaCompile) { - options.compilerArgs << "-Xmaxerrs" << "1000" + android.buildTypes.each { + if (it.name != 'release') { + sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java" + } else { + sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java" + } + } } dependencies { @@ -538,9 +542,8 @@ dependencies { exclude group: 'com.google.protobuf', module: 'protobuf-java' } testImplementation testLibs.robolectric.shadows.multidex - testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { - force = true - } + testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric + testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric testImplementation testLibs.hamcrest.hamcrest testImplementation testLibs.mockk diff --git a/app/proguard/proguard.cfg b/app/proguard/proguard.cfg index 18882f9888..0fd325e5aa 100644 --- a/app/proguard/proguard.cfg +++ b/app/proguard/proguard.cfg @@ -10,4 +10,11 @@ # Protobuf lite -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } --keep class androidx.window.** { *; } \ No newline at end of file +-keep class androidx.window.** { *; } + +# AGP generated dont warns +-dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl +-dontwarn org.slf4j.impl.StaticLoggerBinder +-dontwarn sun.net.spi.nameservice.NameService +-dontwarn sun.net.spi.nameservice.NameServiceDescriptor \ No newline at end of file diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt index 5e741f466a..da869e7757 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt @@ -142,6 +142,7 @@ class ConversationItemPreviewer { 1024, 1024, Optional.empty(), + Optional.empty(), Optional.of("/not-there.jpg"), false, false, diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt index ac4dc97d14..a28483185f 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt @@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.ServiceId import java.util.Optional import java.util.UUID +@Suppress("ClassName") @RunWith(AndroidJUnit4::class) class RecipientTableTest_getAndPossiblyMerge { diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index 29caef911f..c35d9d9249 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.SecurePreferenceManager import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponseProcessor @@ -84,7 +85,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() password = Util.getSecret(18), registrationId = registrationRepository.registrationId, profileKey = registrationRepository.getProfileKey("+15555550101"), - preKeyCollections = RegistrationRepository.generatePreKeys()!!, + aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI), + pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI), fcmToken = null, pniRegistrationId = registrationRepository.pniRegistrationId, recoveryPassword = "asdfasdfasdfasdf" diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt new file mode 100644 index 0000000000..09a5dd1266 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/ConversationElementGenerator.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory +import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey +import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly +import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly +import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import java.security.SecureRandom +import kotlin.time.Duration.Companion.milliseconds + +/** + * Generates random conversation messages via the given set of parameters. + */ +class ConversationElementGenerator { + private val mappingModelCache = mutableMapOf>() + private val random = SecureRandom() + + private val wordBank = listOf( + "A", + "Test", + "Message", + "To", + "Display", + "Content", + "In", + "Bubbles", + "User", + "Signal", + "The" + ) + + fun getMappingModel(key: ConversationElementKey): MappingModel<*> { + val cached = mappingModelCache[key] + if (cached != null) { + return cached + } + + val messageModel = generateMessage(key) + mappingModelCache[key] = messageModel + return messageModel + } + + private fun getIncomingType(): Long { + return MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT + } + + private fun getSentOutgoingType(): Long { + return MessageTypes.BASE_SENT_TYPE or MessageTypes.SECURE_MESSAGE_BIT + } + + private fun generateMessage(key: ConversationElementKey): MappingModel<*> { + val messageId = key.requireMessageId() + val now = getNow() + + val testMessageWordLength = random.nextInt(40) + 1 + val testMessage = (0 until testMessageWordLength).map { + wordBank.random() + }.joinToString(" ") + + val isIncoming = random.nextBoolean() + + val record = MediaMmsMessageRecord( + messageId, + if (isIncoming) Recipient.UNKNOWN else Recipient.self(), + 0, + if (isIncoming) Recipient.self() else Recipient.UNKNOWN, + now, + now, + now, + 1, + 1, + testMessage, + SlideDeck(), + if (isIncoming) getIncomingType() else getSentOutgoingType(), + emptySet(), + emptySet(), + 0, + 0, + 0, + false, + 1, + null, + emptyList(), + emptyList(), + false, + emptyList(), + false, + false, + now, + 1, + now, + null, + StoryType.NONE, + null, + null, + null, + null, + -1, + null, + null, + 0 + ) + + val conversationMessage = ConversationMessageFactory.createWithUnresolvedData( + ApplicationDependencies.getApplication(), + record, + Recipient.UNKNOWN + ) + + return if (isIncoming) { + IncomingTextOnly(conversationMessage) + } else { + OutgoingTextOnly(conversationMessage) + } + } + + private fun getNow(): Long { + val now = System.currentTimeMillis() + return now - random.nextInt(20.milliseconds.inWholeMilliseconds.toInt()).toLong() + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt new file mode 100644 index 0000000000..a3153a1509 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import org.signal.paging.PagedDataSource +import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey +import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import kotlin.math.min + +class InternalConversationTestDataSource( + private val size: Int, + private val generator: ConversationElementGenerator +) : PagedDataSource> { + override fun size(): Int = size + + override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList> { + val end = min(start + length, totalSize) + return (start until end).map { + load(ConversationElementKey.forMessage(it.toLong()))!! + }.toMutableList() + } + + override fun getKey(data: MappingModel<*>): ConversationElementKey { + check(data is ConversationMessageElement) + + return ConversationElementKey.forMessage(data.conversationMessage.messageRecord.id) + } + + override fun load(key: ConversationElementKey?): MappingModel<*>? { + return key?.let { generator.getMappingModel(it) } + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt new file mode 100644 index 0000000000..bdb2354eb2 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.concurrent.LifecycleDisposable +import org.signal.core.util.logging.Log +import org.signal.ringrtc.CallLinkRootKey +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager +import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState +import org.thoughtcrime.securesms.contactshare.Contact +import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener +import org.thoughtcrime.securesms.conversation.ConversationItem +import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette +import org.thoughtcrime.securesms.conversation.colors.Colorizer +import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer +import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart +import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2 +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.databinding.ConversationTestFragmentBinding +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange +import org.thoughtcrime.securesms.linkpreview.LinkPreview +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.util.doAfterNextLayout + +class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fragment) { + + companion object { + private val TAG = Log.tag(InternalConversationTestFragment::class.java) + } + + private val binding by ViewBinderDelegate(ConversationTestFragmentBinding::bind) + private val viewModel: InternalConversationTestViewModel by viewModels() + private val lifecycleDisposable = LifecycleDisposable() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = ConversationAdapterV2( + lifecycleOwner = viewLifecycleOwner, + glideRequests = GlideApp.with(this), + clickListener = ClickListener(), + hasWallpaper = false, + colorizer = Colorizer() + ) + + var startTime = 0L + var firstRender = true + lifecycleDisposable.bindTo(viewLifecycleOwner) + adapter.setPagingController(viewModel.controller) + lifecycleDisposable += viewModel.data.observeOn(AndroidSchedulers.mainThread()).subscribeBy { + if (firstRender) { + startTime = System.currentTimeMillis() + } + adapter.submitList(it) { + if (firstRender) { + firstRender = false + binding.root.doAfterNextLayout { + val endTime = System.currentTimeMillis() + Log.d(TAG, "First render in ${endTime - startTime} millis") + } + } + } + } + + binding.root.layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true) + binding.root.adapter = adapter + + RecyclerViewColorizer(binding.root).apply { + setChatColors(ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)) + } + } + + private inner class ClickListener : ItemClickListener { + override fun onQuoteClicked(messageRecord: MmsMessageRecord?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onLinkPreviewClicked(linkPreview: LinkPreview) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onStickerClicked(stickerLocator: StickerLocator) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onAddToContactsClicked(contact: Contact) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageSharedContactClicked(choices: MutableList) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteSharedContactClicked(choices: MutableList) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageWithErrorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePause(uri: Uri) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNoteSeekTo(uri: Uri, position: Double) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onChatSessionRefreshLearnMoreClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onBadDecryptLearnMoreClicked(author: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onJoinGroupCallClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onEnableCallNotificationsClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onPlayInlineContent(conversationMessage: ConversationMessage?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onChangeNumberUpdateContact(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onCallToAction(action: String) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onDonateClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onBlockJoinRequest(recipient: Recipient) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onRecipientNameClicked(target: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onInviteToSignalClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onActivatePaymentsClicked() { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onSendPaymentClicked(recipientId: RecipientId) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onUrlClicked(url: String): Boolean { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + return true + } + + override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onGiftBadgeRevealed(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onItemClick(item: MultiselectPart?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onItemLongClick(itemView: View?, item: MultiselectPart?) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt new file mode 100644 index 0000000000..8ecdc6a654 --- /dev/null +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestViewModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import androidx.lifecycle.ViewModel +import org.signal.paging.PagedData +import org.signal.paging.PagingConfig + +class InternalConversationTestViewModel : ViewModel() { + private val generator = ConversationElementGenerator() + private val dataSource = InternalConversationTestDataSource( + 500, + generator + ) + + private val config = PagingConfig.Builder().setPageSize(25) + .setBufferPages(2) + .build() + + private val pagedData = PagedData.createForObservable(dataSource, config) + + val controller = pagedData.controller + val data = pagedData.data +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ffd954b123..d94549aaa8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + @@ -1033,6 +1034,11 @@ + + forPointer(Optional pointer.get().asPointer().getRemoteId().toString(), encodedKey, null, pointer.get().asPointer().getDigest().orElse(null), + pointer.get().asPointer().getincrementalDigest().orElse(null), fastPreflightId, pointer.get().asPointer().getVoiceNote(), pointer.get().asPointer().isBorderless(), @@ -137,6 +139,7 @@ public static Optional forPointer(SignalServiceDataMessage.Quote.Quo thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, null, thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, + thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null, null, false, false, @@ -166,6 +169,7 @@ public static Optional forPointer(SignalServiceProtos.DataMessage.Qu thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, null, thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, + thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null, null, false, false, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java index e2e2c44d41..6748b64738 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java @@ -16,7 +16,7 @@ public class TombstoneAttachment extends Attachment { public TombstoneAttachment(@NonNull String contentType, boolean quote) { - super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null); + super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java index eb0fdad184..4cc9b99875 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -52,7 +52,7 @@ public UriAttachment(@NonNull Uri dataUri, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties) { - super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); + super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); this.dataUri = Objects.requireNonNull(dataUri); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/BluetoothVoiceNoteUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/audio/BluetoothVoiceNoteUtil.kt index c6821088b2..61d681b52b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/BluetoothVoiceNoteUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/BluetoothVoiceNoteUtil.kt @@ -25,14 +25,14 @@ sealed interface BluetoothVoiceNoteUtil { fun destroy() companion object { - fun create(context: Context, listener: () -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil { + fun create(context: Context, listener: (Boolean) -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil { return if (Build.VERSION.SDK_INT >= 31) BluetoothVoiceNoteUtil31(listener) else BluetoothVoiceNoteUtilLegacy(context, listener, bluetoothPermissionDeniedHandler) } } } @RequiresApi(31) -private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoiceNoteUtil { +private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : BluetoothVoiceNoteUtil { override fun connectBluetoothScoConnection() { val audioManager = ApplicationDependencies.getAndroidCallAudioManager() val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice @@ -40,13 +40,15 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic val result: Boolean = audioManager.setCommunicationDevice(device) if (result) { Log.d(TAG, "Successfully set Bluetooth device as active communication device.") + listener(true) } else { Log.d(TAG, "Found Bluetooth device but failed to set it as active communication device.") + listener(false) } } else { Log.d(TAG, "Could not find Bluetooth device in list of communications devices, falling back to current input.") + listener(false) } - listener() } override fun disconnectBluetoothScoConnection() { @@ -64,15 +66,23 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic * @param listener This will be executed on the main thread after the Bluetooth connection connects, or if it doesn't. * @param bluetoothPermissionDeniedHandler called when we detect the Bluetooth permission has been denied to our app. */ -private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: () -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil { +private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: (Boolean) -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil { private val commandAndControlThread: HandlerThread = SignalExecutors.getAndStartHandlerThread("voice-note-audio", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD) private val uiThreadHandler = Handler(context.mainLooper) private val audioHandler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread.looper) private val deviceUpdatedListener: AudioDeviceUpdatedListener = object : AudioDeviceUpdatedListener { override fun onAudioDeviceUpdated() { - if (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) { - Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.") - uiThreadHandler.post { listener() } + when (signalBluetoothManager.state) { + SignalBluetoothManager.State.CONNECTED -> { + Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.") + uiThreadHandler.post { listener(true) } + } + SignalBluetoothManager.State.ERROR, + SignalBluetoothManager.State.PERMISSION_DENIED -> { + Log.w(TAG, "Unable to complete Bluetooth connection due to ${signalBluetoothManager.state}. Starting voice note recording anyway on UI thread.") + uiThreadHandler.post { listener(false) } + } + else -> Log.d(TAG, "Current Bluetooth connection state: ${signalBluetoothManager.state}.") } } } @@ -105,7 +115,7 @@ private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: ( bluetoothPermissionDeniedHandler() hasWarnedAboutBluetooth = true } - listener() + listener(false) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinkJoinButton.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinkJoinButton.kt index 4c8e765d60..d0ccd94214 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinkJoinButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinkJoinButton.kt @@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.calls.links import android.content.Context import android.util.AttributeSet +import androidx.annotation.ColorRes import androidx.appcompat.widget.LinearLayoutCompat +import androidx.core.content.ContextCompat import com.google.android.material.button.MaterialButton import org.thoughtcrime.securesms.R @@ -22,6 +24,10 @@ class CallLinkJoinButton @JvmOverloads constructor( private val joinButton: MaterialButton = findViewById(R.id.join_button) + fun setTextColor(@ColorRes textColorResId: Int) { + joinButton.setTextColor(ContextCompat.getColor(context, textColorResId)) + } + fun setJoinClickListener(onClickListener: OnClickListener) { joinButton.setOnClickListener(onClickListener) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt index 9d046144a3..eb5cc80b93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt @@ -51,6 +51,16 @@ object CallLinks { } } + @JvmStatic + fun isCallLink(url: String): Boolean { + if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) { + Log.w(TAG, "Invalid url prefix.") + return false + } + + return url.split("#").last().startsWith("key=") + } + @JvmStatic fun parseUrl(url: String): CallLinkRootKey? { if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt index ecd234a965..62de03c17e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkBottomSheetDialogFragment.kt @@ -109,7 +109,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment Rows.TextRow( text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name), - modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked) + onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked ) Rows.ToggleRow( @@ -124,19 +124,19 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment Rows.TextRow( text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal), icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24), - modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked) + onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked ) Rows.TextRow( text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link), icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24), - modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked) + onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked ) Rows.TextRow( text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link), icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24), - modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked) + onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked ) Buttons.MediumTonal( diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt index b3075bf518..99f2bbb087 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsFragment.kt @@ -10,7 +10,6 @@ import android.content.Intent import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -247,7 +246,7 @@ private fun CallLinkDetails( Rows.TextRow( text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name), - modifier = Modifier.clickable(onClick = callback::onEditNameClicked) + onClick = callback::onEditNameClicked ) Rows.ToggleRow( @@ -261,14 +260,14 @@ private fun CallLinkDetails( Rows.TextRow( text = stringResource(id = R.string.CallLinkDetailsFragment__share_link), icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24), - modifier = Modifier.clickable(onClick = callback::onShareClicked) + onClick = callback::onShareClicked ) Rows.TextRow( text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link), icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24), foregroundTint = MaterialTheme.colorScheme.error, - modifier = Modifier.clickable(onClick = callback::onDeleteClicked) + onClick = callback::onDeleteClicked ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsRepository.kt index 33f21e7fc1..0be11cfcb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/details/CallLinkDetailsRepository.kt @@ -5,17 +5,15 @@ package org.thoughtcrime.securesms.calls.links.details -import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.concurrent.MaybeCompat import org.signal.core.util.orNull -import org.thoughtcrime.securesms.database.CallLinkTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager @@ -24,7 +22,7 @@ class CallLinkDetailsRepository( private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager ) { fun refreshCallLinkState(callLinkRoomId: CallLinkRoomId): Disposable { - return Maybe.fromCallable { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) } + return MaybeCompat.fromCallable { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) } .flatMapSingle { callLinkManager.readCallLink(it.credentials!!) } .subscribeOn(Schedulers.io()) .subscribeBy { result -> @@ -36,7 +34,7 @@ class CallLinkDetailsRepository( } fun watchCallLinkRecipient(callLinkRoomId: CallLinkRoomId): Observable { - return Maybe.fromCallable { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() } + return MaybeCompat.fromCallable { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() } .flatMapObservable { Recipient.observable(it) } .distinctUntilChanged { a, b -> a.hasSameContent(b) } .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt index 055e3df947..d265197553 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt @@ -230,7 +230,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal val count = callLogActionMode.getCount() MaterialAlertDialogBuilder(requireContext()) .setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count)) - .setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ -> + .setPositiveButton(R.string.CallLogFragment__delete) { _, _ -> performDeletion(count, viewModel.stageSelectionDeletion()) callLogActionMode.end() } @@ -363,7 +363,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal override fun deleteCall(call: CallLogRow) { MaterialAlertDialogBuilder(requireContext()) .setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1)) - .setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ -> + .setPositiveButton(R.string.CallLogFragment__delete) { _, _ -> performDeletion(1, viewModel.stageCallDeletion(call)) } .show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt index 334b69d4d9..fb6a345e22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRepository.kt @@ -68,7 +68,7 @@ class CallLogRepository( ): Completable { return Completable.fromAction { SignalDatabase.calls.deleteNonAdHocCallEvents(selectedCallRowIds) - }.observeOn(Schedulers.io()) + }.subscribeOn(Schedulers.io()) } fun deleteAllCallLogsExcept( @@ -77,7 +77,7 @@ class CallLogRepository( ): Completable { return Completable.fromAction { SignalDatabase.calls.deleteAllNonAdHocCallEventsExcept(selectedCallRowIds, missedOnly) - }.observeOn(Schedulers.io()) + }.subscribeOn(Schedulers.io()) } /** @@ -105,7 +105,7 @@ class CallLogRepository( SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() }.doOnDispose { SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() - }.observeOn(Schedulers.io()) + }.subscribeOn(Schedulers.io()) } /** @@ -133,7 +133,7 @@ class CallLogRepository( SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() }.doOnDispose { SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() - }.observeOn(Schedulers.io()) + }.subscribeOn(Schedulers.io()) } fun peekCallLinks(): Completable { @@ -158,6 +158,6 @@ class CallLogRepository( } ApplicationDependencies.getJobManager().addAll(jobs) - }.observeOn(Schedulers.io()) + }.subscribeOn(Schedulers.io()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeTextStyleWatcher.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeTextStyleWatcher.kt index 2de670ac40..db3230e6c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ComposeTextStyleWatcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ComposeTextStyleWatcher.kt @@ -46,11 +46,17 @@ class ComposeTextStyleWatcher : TextWatcher { try { if (editStart < 0 || editEnd < 0 || editStart >= editEnd || (editStart == 0 && editEnd == s.length)) { + textSnapshotPriorToChange = null return } val change = s.subSequence(editStart, editEnd) - if (change.isEmpty() || textSnapshotPriorToChange == null || (editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) || TextUtils.equals(textSnapshotPriorToChange, change)) { + if (change.isEmpty() || + textSnapshotPriorToChange == null || + (editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) || + TextUtils.equals(textSnapshotPriorToChange, change) || + editEnd - editStart > 1 + ) { textSnapshotPriorToChange = null return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 68406b5421..057d252ecd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -318,8 +318,14 @@ private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale l } else if (MessageRecordUtil.isScheduled(messageRecord)) { dateView.setText(DateUtils.getOnlyTimeString(getContext(), locale, ((MediaMmsMessageRecord) messageRecord).getScheduledDate())); } else { - String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()); - if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) messageRecord).isEditMessage()) { + long timestamp = messageRecord.getTimestamp(); + if (messageRecord.isEditMessage()) { + if (displayMode == ConversationItemDisplayMode.EDIT_HISTORY) { + timestamp = messageRecord.getDateSent(); + } + } + String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp); + if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage()) { date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date); } dateView.setText(date); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java b/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java index 585a896077..b37e10869a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java @@ -2,26 +2,32 @@ import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Parcelable; import android.util.AttributeSet; import android.view.View; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; -import android.widget.FrameLayout; -import android.widget.ImageView; -import org.signal.core.util.logging.Log; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; + +import org.signal.core.util.DimensionUnit; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ViewUtil; + +public class DeliveryStatusView extends AppCompatImageView { -public class DeliveryStatusView extends FrameLayout { + private static final String STATE_KEY = "DeliveryStatusView.STATE"; + private static final String ROOT_KEY = "DeliveryStatusView.ROOT"; - private static final String TAG = Log.tag(DeliveryStatusView.class); + private final int horizontalPadding = (int) DimensionUnit.DP.toPixels(2); - private final RotateAnimation rotationAnimation; - private final ImageView pendingIndicator; - private final ImageView sentIndicator; - private final ImageView deliveredIndicator; - private final ImageView readIndicator; + private RotateAnimation rotationAnimation; + + private State state = State.NONE; public DeliveryStatusView(Context context) { this(context, null); @@ -34,75 +40,157 @@ public DeliveryStatusView(Context context, AttributeSet attrs) { public DeliveryStatusView(final Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - inflate(context, R.layout.delivery_status_view, this); - - this.deliveredIndicator = findViewById(R.id.delivered_indicator); - this.sentIndicator = findViewById(R.id.sent_indicator); - this.pendingIndicator = findViewById(R.id.pending_indicator); - this.readIndicator = findViewById(R.id.read_indicator); - - rotationAnimation = new RotateAnimation(0, 360f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f); - rotationAnimation.setInterpolator(new LinearInterpolator()); - rotationAnimation.setDuration(1500); - rotationAnimation.setRepeatCount(Animation.INFINITE); - if (attrs != null) { TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0); setTint(typedArray.getColor(R.styleable.DeliveryStatusView_iconColor, getResources().getColor(R.color.core_white))); typedArray.recycle(); } + + setNone(); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle stateBundle = (Bundle) state; + State s = State.fromCode(stateBundle.getInt(STATE_KEY, State.NONE.code)); + + switch (s) { + case NONE: + setNone(); + break; + case PENDING: + setPending(); + break; + case SENT: + setSent(); + break; + case DELIVERED: + setDelivered(); + break; + case READ: + setRead(); + break; + } + + Parcelable root = stateBundle.getParcelable(ROOT_KEY); + super.onRestoreInstanceState(root); + } else { + super.onRestoreInstanceState(state); + } + } + + @Override + protected @Nullable Parcelable onSaveInstanceState() { + Parcelable root = super.onSaveInstanceState(); + Bundle stateBundle = new Bundle(); + + stateBundle.putParcelable(ROOT_KEY, root); + stateBundle.putInt(STATE_KEY, state.code); + + return stateBundle; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (state == State.PENDING && rotationAnimation == null) { + final float pivotXValue; + if (ViewUtil.isLtr(this)) { + pivotXValue = (w - getPaddingEnd()) / 2f; + } else { + pivotXValue = ((w - getPaddingEnd()) / 2f) + getPaddingEnd(); + } + + final float pivotYValue = (h - getPaddingTop() - getPaddingBottom()) / 2f; + + rotationAnimation = new RotateAnimation(0, 360f, + Animation.ABSOLUTE, pivotXValue, + Animation.ABSOLUTE, pivotYValue); + + rotationAnimation.setInterpolator(new LinearInterpolator()); + rotationAnimation.setDuration(1500); + rotationAnimation.setRepeatCount(Animation.INFINITE); + + startAnimation(rotationAnimation); + } + } + + @Override + public void clearAnimation() { + super.clearAnimation(); + rotationAnimation = null; } public void setNone() { - this.setVisibility(View.GONE); + state = State.NONE; + clearAnimation(); + setVisibility(View.GONE); } public boolean isPending() { - return pendingIndicator.getVisibility() == View.VISIBLE; + return state == State.PENDING; } public void setPending() { - this.setVisibility(View.VISIBLE); - pendingIndicator.setVisibility(View.VISIBLE); - pendingIndicator.startAnimation(rotationAnimation); - sentIndicator.setVisibility(View.GONE); - deliveredIndicator.setVisibility(View.GONE); - readIndicator.setVisibility(View.GONE); + state = State.PENDING; + setVisibility(View.VISIBLE); + ViewUtil.setPaddingStart(this, 0); + ViewUtil.setPaddingEnd(this, horizontalPadding); + setImageResource(R.drawable.ic_delivery_status_sending); } public void setSent() { - this.setVisibility(View.VISIBLE); - pendingIndicator.setVisibility(View.GONE); - pendingIndicator.clearAnimation(); - sentIndicator.setVisibility(View.VISIBLE); - deliveredIndicator.setVisibility(View.GONE); - readIndicator.setVisibility(View.GONE); + state = State.SENT; + setVisibility(View.VISIBLE); + ViewUtil.setPaddingStart(this, horizontalPadding); + ViewUtil.setPaddingEnd(this, 0); + clearAnimation(); + setImageResource(R.drawable.ic_delivery_status_sent); } public void setDelivered() { - this.setVisibility(View.VISIBLE); - pendingIndicator.setVisibility(View.GONE); - pendingIndicator.clearAnimation(); - sentIndicator.setVisibility(View.GONE); - deliveredIndicator.setVisibility(View.VISIBLE); - readIndicator.setVisibility(View.GONE); + state = State.DELIVERED; + setVisibility(View.VISIBLE); + ViewUtil.setPaddingStart(this, horizontalPadding); + ViewUtil.setPaddingEnd(this, 0); + clearAnimation(); + setImageResource(R.drawable.ic_delivery_status_delivered); } public void setRead() { - this.setVisibility(View.VISIBLE); - pendingIndicator.setVisibility(View.GONE); - pendingIndicator.clearAnimation(); - sentIndicator.setVisibility(View.GONE); - deliveredIndicator.setVisibility(View.GONE); - readIndicator.setVisibility(View.VISIBLE); + state = State.READ; + setVisibility(View.VISIBLE); + ViewUtil.setPaddingStart(this, horizontalPadding); + ViewUtil.setPaddingEnd(this, 0); + clearAnimation(); + setImageResource(R.drawable.ic_delivery_status_read); } public void setTint(int color) { - pendingIndicator.setColorFilter(color); - deliveredIndicator.setColorFilter(color); - sentIndicator.setColorFilter(color); - readIndicator.setColorFilter(color); + setColorFilter(color); + } + + private enum State { + NONE(0), + PENDING(1), + SENT(2), + DELIVERED(3), + READ(4); + + final int code; + + State(int code) { + this.code = code; + } + + static State fromCode(int code) { + for (State state : State.values()) { + if (state.code == code) { + return state; + } + } + + return NONE; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt index b3a3863b7c..7adc9fef5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareConstraintLayout.kt @@ -26,6 +26,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor( private var inputId: Int? = null private var input: Fragment? = null + val isInputShowing: Boolean + get() = input != null + lateinit var fragmentManager: FragmentManager var listener: Listener? = null @@ -34,10 +37,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor( hideInput(resetKeyboardGuideline = false) } - fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) { + fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = false) { if (fragmentCreator.id == inputId) { - hideInput(resetKeyboardGuideline = true) - toggled(false) + if (showSoftKeyOnHide) { + showSoftkey(imeTarget) + } else { + hideInput(resetKeyboardGuideline = true) + } } else { hideInput(resetKeyboardGuideline = false) showInput(fragmentCreator, imeTarget) @@ -55,6 +61,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor( fragmentManager .beginTransaction() .replace(R.id.input_container, input!!) + .runOnCommit { (input as? InputFragment)?.show() } .commit() overrideKeyboardGuidelineWithPreviousHeight() @@ -66,6 +73,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor( private fun hideInput(resetKeyboardGuideline: Boolean) { val inputHidden = input != null input?.let { + (input as? InputFragment)?.hide() fragmentManager .beginTransaction() .remove(it) @@ -94,4 +102,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor( fun onInputShown() fun onInputHidden() } + + interface InputFragment { + fun show() + fun hide() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 846cc565ca..217b7d1c37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -55,9 +55,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) } private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) } + private val listeners: MutableList = mutableListOf() private val keyboardAnimator = KeyboardInsetAnimator() private val displayMetrics = DisplayMetrics() private var overridingKeyboard: Boolean = false + private var previousKeyboardHeight: Int = 0 init { ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat -> @@ -74,6 +76,14 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( } } + fun addKeyboardStateListener(listener: KeyboardStateListener) { + listeners += listener + } + + fun removeKeyboardStateListener(listener: KeyboardStateListener) { + listeners.remove(listener) + } + private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) { val isLtr = ViewUtil.isLtr(this) @@ -96,6 +106,18 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( keyboardAnimator.endingGuidelineEnd = windowInsets.bottom } } + + if (previousKeyboardHeight != keyboardInsets.bottom) { + listeners.forEach { + if (previousKeyboardHeight <= 0) { + it.onKeyboardShown() + } else { + it.onKeyboardHidden() + } + } + } + + previousKeyboardHeight = keyboardInsets.bottom } protected fun overrideKeyboardGuidelineWithPreviousHeight() { @@ -157,6 +179,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val Guideline?.guidelineEnd: Int get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd + interface KeyboardStateListener { + fun onKeyboardShown() + fun onKeyboardHidden() + } + /** * Adjusts the [keyboardGuideline] to move with the IME keyboard opening or closing. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java index 13afe57ada..eb3dcb961f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java @@ -170,9 +170,13 @@ public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPr spinner.setVisibility(GONE); noPreview.setVisibility(GONE); + CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); if (!Util.isEmpty(linkPreview.getTitle())) { title.setText(linkPreview.getTitle()); title.setVisibility(VISIBLE); + } else if (callLinkRootKey != null) { + title.setText(R.string.Recipient_signal_call); + title.setVisibility(VISIBLE); } else { title.setVisibility(GONE); } @@ -180,6 +184,9 @@ public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPr if (showDescription && !Util.isEmpty(linkPreview.getDescription())) { description.setText(linkPreview.getDescription()); description.setVisibility(VISIBLE); + } else if (callLinkRootKey != null) { + description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call); + description.setVisibility(VISIBLE); } else { description.setVisibility(GONE); } @@ -206,7 +213,6 @@ public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPr site.setVisibility(GONE); } - CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); if (showThumbnail && linkPreview.getThumbnail().isPresent()) { thumbnail.setVisibility(VISIBLE); thumbnailState.applyState(thumbnail); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt index 54c151f6b6..6fdb8492b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt @@ -1,10 +1,12 @@ package org.thoughtcrime.securesms.components +import android.annotation.SuppressLint import android.app.Dialog import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.View +import androidx.annotation.Discouraged import androidx.fragment.app.DialogFragment import androidx.navigation.fragment.navArgs import org.thoughtcrime.securesms.R @@ -12,10 +14,13 @@ import org.thoughtcrime.securesms.R /** * Displays a small progress spinner in a card view, as a non-cancellable dialog fragment. */ -class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) { +class ProgressCardDialogFragment +@Discouraged("Use create() instead.") +constructor() : DialogFragment(R.layout.progress_card_dialog) { companion object { - fun create(title: String): ProgressCardDialogFragment { + @SuppressLint("DiscouragedApi") + fun create(title: String? = null): ProgressCardDialogFragment { return ProgressCardDialogFragment().apply { arguments = ProgressCardDialogFragmentArgs.Builder(title).build().toBundle() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java b/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java index 03b464c2fc..ff7d20f174 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java @@ -125,6 +125,7 @@ public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Curs .signature(signature) .diskCacheStrategy(DiskCacheStrategy.NONE) .transition(DrawableTransitionOptions.withCrossFade()) + .centerCrop() .into(viewHolder.imageView); viewHolder.imageView.setOnClickListener(v -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 36e4137b63..8acc77c0be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -28,6 +28,7 @@ import com.bumptech.glide.request.RequestOptions; import org.signal.core.util.logging.Log; +import org.signal.glide.transforms.SignalDownsampleStrategy; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.database.AttachmentTable; @@ -454,6 +455,7 @@ public ListenableFuture setImageResource(@NonNull GlideRequests glideRe GlideRequest request = glideRequests.load(new DecryptableUri(uri)) .diskCacheStrategy(DiskCacheStrategy.NONE) + .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .listener(listener); if (animate) { @@ -486,6 +488,7 @@ public ListenableFuture setImageResource(@NonNull GlideRequests glideRe GlideRequest request = glideRequests.load(model) .diskCacheStrategy(DiskCacheStrategy.NONE) .placeholder(model.getPlaceholder()) + .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .transition(withCrossFade()); request = override(request, width, height); @@ -554,6 +557,7 @@ public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri()))) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .transition(withCrossFade())); boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java index 49f4b2f70b..93dca90b79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java @@ -105,7 +105,6 @@ public void show() { if (!isInitialised) initView(); setVisibility(VISIBLE); - if (keyboardListener != null) keyboardListener.onShown(); keyboardPagerFragment.show(); } @@ -113,7 +112,6 @@ public void show() { public void hide(boolean immediate) { setVisibility(GONE); onCloseEmojiSearchInternal(false); - if (keyboardListener != null) keyboardListener.onHidden(); Log.i(TAG, "hide()"); keyboardPagerFragment.hide(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 6034f67d0e..0188350738 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -6,6 +6,7 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import androidx.navigation.Navigation +import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.thoughtcrime.securesms.R diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 6008676c1f..8830475108 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -141,7 +141,13 @@ class ChangeNumberRepository( } } - VerifyResponse.from(changeNumberResponse, null, null) + VerifyResponse.from( + response = changeNumberResponse, + kbsData = null, + pin = null, + aciPreKeyCollection = null, + pniPreKeyCollection = null + ) }.subscribeOn(Schedulers.single()) .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } } @@ -191,7 +197,13 @@ class ChangeNumberRepository( } } - VerifyResponse.from(changeNumberResponse, kbsData, pin) + VerifyResponse.from( + response = changeNumberResponse, + kbsData = kbsData, + pin = pin, + aciPreKeyCollection = null, + pniPreKeyCollection = null + ) }.subscribeOn(Schedulers.single()) .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } } @@ -341,7 +353,7 @@ class ChangeNumberRepository( val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) { PreKeyUtil.generateAndStoreLastResortKyberPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey) } else { - PreKeyUtil.generateKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) + PreKeyUtil.generateLastRestortKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) } devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt index 7d801429a1..7edce7a3b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt @@ -82,6 +82,7 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon val processor: RegistrationSessionProcessor = (result as RequestCodeResult.RequestedVerificationCode).processor if (processor.hasResult()) { + Log.i(TAG, "Successfully requested SMS code.") findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment) } else if (processor.captchaRequired(viewModel.excludedChallenges)) { Log.i(TAG, "Unable to request sms code due to captcha required") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index a172e6d499..63daabcf9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -564,6 +564,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } dividerPref() + clickPref( + title = DSLSettingsText.from("Launch ConversationTestFragment"), + onClick = { + findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalConversationTestFragment()) + } + ) + switchPref( title = DSLSettingsText.from("Use V2 ConversationFragment"), isChecked = state.useConversationFragmentV2, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerPaint.kt b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerPaint.kt index e62bab17e0..064c26876b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerPaint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerPaint.kt @@ -11,7 +11,6 @@ import androidx.annotation.MainThread import org.signal.core.util.DimensionUnit import org.signal.core.util.dp import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.util.AccessibilityUtil import org.thoughtcrime.securesms.util.Util import kotlin.random.Random @@ -65,14 +64,14 @@ object SpoilerPaint { bounds.bottom + strokeWidth.toInt() ) - update(!AccessibilityUtil.areAnimationsDisabled(ApplicationDependencies.getApplication())) + update() } /** * Invoke every time before you need to use the [shader]. */ @MainThread - fun update(animationsEnabled: Boolean) { + fun update() { val now = System.currentTimeMillis() var dt = now - lastDrawTime if (dt < 48) { @@ -87,7 +86,7 @@ object SpoilerPaint { // To avoid that, we draw into a buffer, then swap the buffer into the shader when it's fully drawn. val canvas = Canvas(bufferBitmap) bufferBitmap.eraseColor(Color.TRANSPARENT) - draw(canvas, if (animationsEnabled) dt else 0) + draw(canvas, dt) val swap = shaderBitmap shaderBitmap = bufferBitmap diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt index 8f77a75f23..8a84673e56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/spoiler/SpoilerRendererDelegate.kt @@ -38,7 +38,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor( private val animator = TimeAnimator().apply { setTimeListener { _, _, _ -> - SpoilerPaint.update(systemAnimationsEnabled) + SpoilerPaint.update() view.invalidate() } } @@ -114,7 +114,7 @@ class SpoilerRendererDelegate @JvmOverloads constructor( hasSpoilersToRender = true } - if (hasSpoilersToRender) { + if (hasSpoilersToRender && systemAnimationsEnabled) { if (!animatorRunning) { animator.start() animatorRunning = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index f5a6ed3384..dc26d5d2ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -263,6 +263,7 @@ private void setPipAvatar(@NonNull Recipient recipient) { .fallback(fallbackPhoto.asCallCard(getContext())) .error(fallbackPhoto.asCallCard(getContext())) .diskCacheStrategy(DiskCacheStrategy.ALL) + .fitCenter() .into(pipAvatar); pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt index fd13c8d6e9..0344c90992 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt @@ -103,8 +103,7 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context, } } - val label = context.getString(currentOutput.labelRes) - Log.i(TAG, "Switching to $label") + Log.i(TAG, "Switching to $currentOutput") val drawableState = super.onCreateDrawableState(extraSpace + extra.size) mergeDrawableStates(drawableState, extra) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index 198d30ee13..7e92092d99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -337,8 +337,10 @@ public long getHeaderId(int position) { if (scheduledMessagesMode) { calendar.setTimeInMillis(((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate()); - } else { + } else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) { calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateSent()); + } else { + calendar.setTimeInMillis(conversationMessage.getConversationTimestamp()); } return calendar.get(Calendar.YEAR) * 1000L + calendar.get(Calendar.DAY_OF_YEAR); } @@ -355,6 +357,8 @@ public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int positi if (scheduledMessagesMode) { viewHolder.setText(DateUtils.getScheduledMessagesDateHeaderString(viewHolder.itemView.getContext(), locale, ((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate())); + } else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) { + viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateSent())); } else { viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getConversationTimestamp())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 5c440f4f9c..0bd4805fcd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -1546,9 +1546,7 @@ public void onItemLongClick(View itemView, MultiselectPart item) { MessageRecord messageRecord = item.getConversationMessage().getMessageRecord(); - if (messageRecord.isSecure() && - !messageRecord.isRemoteDelete() && - !messageRecord.isUpdate() && + if (MessageRecordUtil.isValidReactionTarget(messageRecord) && !recipient.get().isBlocked() && !messageRequestViewModel.shouldShowMessageRequest() && (!recipient.get().isGroup() || recipient.get().isActiveGroup()) && diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 16ac45d4a0..b7faff6b16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -1118,6 +1118,7 @@ private void setMediaAttributes(@NonNull MessageRecord messageRecord, CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); if (callLinkRootKey != null) { joinCallLinkStub.setVisibility(View.VISIBLE); + joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOnSurface); joinCallLinkStub.get().setJoinClickListener(v -> { if (eventListener != null) { eventListener.onJoinCallLink(callLinkRootKey); @@ -1733,7 +1734,7 @@ private void setStoryReactionLabel(@NonNull MessageRecord record) { } private void setHasBeenQuoted(@NonNull ConversationMessage message) { - if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EXTRA_CONDENSED) { + if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EDIT_HISTORY) { quotedIndicator.setVisibility(VISIBLE); quotedIndicator.setOnClickListener(quotedIndicatorClickListener); } else if (quotedIndicator != null) { @@ -1860,7 +1861,7 @@ private void setMessageShape(@NonNull MessageRecord current, @NonNull Optional { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MaskDrawable.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MaskDrawable.java deleted file mode 100644 index 547333d3c2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MaskDrawable.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Path; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Drawable which lets you punch a hole through another drawable. - * - * TODO: Remove in favor of ClipProjectionDrawable - */ -public final class MaskDrawable extends Drawable { - - private final RectF bounds = new RectF(); - private final Path clipPath = new Path(); - - private Rect clipRect; - private float[] clipPathRadii; - - private final Drawable wrapped; - - public MaskDrawable(@NonNull Drawable wrapped) { - this.wrapped = wrapped; - } - - @Override - public void draw(@NonNull Canvas canvas) { - if (clipRect == null) { - wrapped.draw(canvas); - return; - } - - canvas.save(); - - if (clipPathRadii != null) { - clipPath.reset(); - bounds.set(clipRect); - clipPath.addRoundRect(bounds, clipPathRadii, Path.Direction.CW); - canvas.clipPath(clipPath, Region.Op.DIFFERENCE); - } else { - canvas.clipRect(clipRect, Region.Op.DIFFERENCE); - } - - wrapped.draw(canvas); - canvas.restore(); - } - - @Override - public void setAlpha(int alpha) { - wrapped.setAlpha(alpha); - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - wrapped.setColorFilter(colorFilter); - } - - @Override - public int getOpacity() { - return wrapped.getOpacity(); - } - - @Override - public void setBounds(int left, int top, int right, int bottom) { - super.setBounds(left, top, right, bottom); - wrapped.setBounds(left, top, right, bottom); - } - - @Override - public boolean getPadding(@NonNull Rect padding) { - return wrapped.getPadding(padding); - } - - public void setMask(@Nullable Rect mask) { - this.clipRect = new Rect(mask); - - invalidateSelf(); - } - - public void setCorners(@Nullable float[] clipPathRadii) { - this.clipPathRadii = clipPathRadii; - - invalidateSelf(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/NameColors.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/NameColors.kt index 3df05e9268..dfd693307c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/NameColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/NameColors.kt @@ -45,11 +45,13 @@ object NameColors { } private fun getSessionGroupRecipients(groupId: GroupId, sessionMemberCache: MutableMap>): LiveData> { - val fullMembers = LiveGroup(groupId).fullMembers.map { members: List? -> - Stream.of(members) - .map { it.member } - .toList() - } + val fullMembers = LiveGroup(groupId) + .fullMembers + .map { members: List? -> + Stream.of(members) + .map { it.member } + .toList() + } return fullMembers.map { currentMembership: List? -> val cachedMembers: MutableSet = MapUtil.getOrDefault(sessionMemberCache, groupId, HashSet()).toMutableSet() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt index 1283dd6116..d99f345a13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt @@ -12,6 +12,7 @@ import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.StreamUtil +import org.signal.core.util.concurrent.MaybeCompat import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.components.location.SignalPlace @@ -70,7 +71,7 @@ class DraftRepository( } fun getShareOrDraftData(): Maybe> { - return Maybe.fromCallable> { getShareOrDraftDataInternal() } + return MaybeCompat.fromCallable { getShareOrDraftDataInternal() } .observeOn(Schedulers.io()) } @@ -214,7 +215,7 @@ class DraftRepository( @Deprecated("Not needed for CFv2") fun loadDraftQuote(serialized: String): Maybe { - return Maybe.fromCallable { loadDraftQuoteInternal(serialized) } + return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) } } private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? { @@ -233,7 +234,7 @@ class DraftRepository( @Deprecated("Not needed for CFv2") fun loadDraftMessageEdit(serialized: String): Maybe { - return Maybe.fromCallable { loadDraftMessageEditInternal(serialized) } + return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) } } private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt index 748bef2950..52e3fd1287 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest import org.thoughtcrime.securesms.conversation.ConversationItem +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.wallpaper.ChatWallpaper @@ -395,9 +396,11 @@ class MultiselectItemDecoration( } } - canvas.clipPath(path) - canvas.drawShade() - canvas.restore() + if (!SignalStore.internalValues().useConversationFragmentV2()) { + canvas.clipPath(path) + canvas.drawShade() + canvas.restore() + } } } @@ -413,9 +416,11 @@ class MultiselectItemDecoration( } } - canvas.clipPath(path, Region.Op.DIFFERENCE) - canvas.drawShade() - canvas.restore() + if (!SignalStore.internalValues().useConversationFragmentV2()) { + canvas.clipPath(path, Region.Op.DIFFERENCE) + canvas.drawShade() + canvas.restore() + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt index e8d4a05391..be14c00dbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt @@ -1,13 +1,17 @@ package org.thoughtcrime.securesms.conversation.ui.edit +import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf +import androidx.core.view.doOnNextLayout import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.concurrent.LifecycleDisposable import org.thoughtcrime.securesms.R @@ -35,6 +39,7 @@ import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.StickyHeaderDecoration import org.thoughtcrime.securesms.util.ViewModelFactory import org.thoughtcrime.securesms.util.fragments.requireListener import java.util.Locale @@ -44,8 +49,6 @@ import java.util.Locale */ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { - override val peekHeightPercentage: Float = 0.4f - private val binding: MessageEditHistoryBottomSheetBinding by ViewBinderDelegate(MessageEditHistoryBottomSheetBinding::bind) private val originalMessageId: Long by lazy { requireArguments().getLong(ARGUMENT_ORIGINAL_MESSAGE_ID) } private val conversationRecipient: Recipient by lazy { Recipient.resolved(requireArguments().getParcelable(ARGUMENT_CONVERSATION_RECIPIENT_ID)!!) } @@ -53,10 +56,17 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { private val disposables: LifecycleDisposable = LifecycleDisposable() + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + dialog.behavior.skipCollapsed = true + dialog.setOnShowListener { + dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + return dialog + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view = MessageEditHistoryBottomSheetBinding.inflate(inflater, container, false).root - view.minimumHeight = (resources.displayMetrics.heightPixels * peekHeightPercentage).toInt() - return view + return MessageEditHistoryBottomSheetBinding.inflate(inflater, container, false).root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -74,7 +84,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { conversationRecipient.hasWallpaper(), colorizer ).apply { - setCondensedMode(ConversationItemDisplayMode.EXTRA_CONDENSED) + setCondensedMode(ConversationItemDisplayMode.EDIT_HISTORY) } binding.editHistoryList.apply { @@ -82,6 +92,10 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { adapter = messageAdapter itemAnimator = null addItemDecoration(OriginalMessageSeparatorDecoration(context, R.string.EditMessageHistoryDialog_title)) + doOnNextLayout { + // Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view + addItemDecoration(StickyHeaderDecoration(messageAdapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE)) + } } val recyclerViewColorizer = RecyclerViewColorizer(binding.editHistoryList) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryReplacement.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryReplacement.kt index 31691f3c7b..83f2d72c17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryReplacement.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryReplacement.kt @@ -1,6 +1,11 @@ package org.thoughtcrime.securesms.conversation.ui.inlinequery import android.content.Context +import android.text.SpannableStringBuilder +import android.text.Spanned +import org.thoughtcrime.securesms.components.mention.MentionAnnotation +import org.thoughtcrime.securesms.database.MentionUtil +import org.thoughtcrime.securesms.recipients.Recipient /** * Encapsulate how to replace a query with a user selected result. @@ -13,4 +18,18 @@ sealed class InlineQueryReplacement(@get:JvmName("isKeywordSearch") val keywordS return emoji } } + + class Mention(private val recipient: Recipient, keywordSearch: Boolean) : InlineQueryReplacement(keywordSearch) { + override fun toCharSequence(context: Context): CharSequence { + val builder = SpannableStringBuilder().apply { + append(MentionUtil.MENTION_STARTER) + append(recipient.getDisplayName(context)) + append(" ") + } + + builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipient.id), 0, builder.length - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + + return builder + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsController.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsController.kt index 6758e1690e..75f9cd7f6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsController.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.ui.inlinequery -import android.content.Context import android.view.View import android.view.ViewGroup import androidx.lifecycle.DefaultLifecycleObserver @@ -17,7 +16,6 @@ import org.thoughtcrime.securesms.util.doOnEachLayout * Controller for inline search results. */ class InlineQueryResultsController( - private val context: Context, private val viewModel: InlineQueryViewModel, private val anchor: View, private val container: ViewGroup, @@ -44,6 +42,7 @@ class InlineQueryResultsController( } }) + canShow = editText.hasFocus() editText.addOnFocusChangeListener { _, hasFocus -> canShow = hasFocus updateList(previousResults ?: emptyList()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsControllerV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsControllerV2.kt new file mode 100644 index 0000000000..ba9b542f0c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryResultsControllerV2.kt @@ -0,0 +1,134 @@ +package org.thoughtcrime.securesms.conversation.ui.inlinequery + +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.DimensionUnit +import org.signal.core.util.concurrent.LifecycleDisposable +import org.signal.core.util.concurrent.addTo +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.ComposeText +import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragmentV2 +import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel +import org.thoughtcrime.securesms.util.doOnEachLayout + +/** + * Controller for inline search results. + */ +class InlineQueryResultsControllerV2( + private val parentFragment: Fragment, + private val viewModel: InlineQueryViewModelV2, + private val anchor: View, + private val container: ViewGroup, + editText: ComposeText +) : InlineQueryResultsPopup.Callback { + + companion object { + private const val MENTION_TAG = "mention_fragment_tag" + } + + private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable() + private var emojiPopup: InlineQueryResultsPopup? = null + private var mentionFragment: MentionsPickerFragmentV2? = null + private var previousResults: InlineQueryViewModelV2.Results? = null + private var canShow: Boolean = false + private var isLandscape: Boolean = false + + init { + lifecycleDisposable.bindTo(parentFragment.viewLifecycleOwner) + + viewModel + .results + .subscribeBy { updateList(it) } + .addTo(lifecycleDisposable) + + parentFragment.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + dismiss() + } + }) + + canShow = editText.hasFocus() + editText.addOnFocusChangeListener { _, hasFocus -> + canShow = hasFocus + updateList(previousResults ?: InlineQueryViewModelV2.None) + } + + anchor.doOnEachLayout { emojiPopup?.updateWithAnchor() } + } + + override fun onSelection(model: AnyMappingModel) { + viewModel.onSelection(model) + } + + override fun onDismiss() { + emojiPopup = null + } + + fun onOrientationChange(isLandscape: Boolean) { + this.isLandscape = isLandscape + + if (isLandscape) { + dismiss() + } else { + updateList(previousResults ?: InlineQueryViewModelV2.None) + } + } + + private fun updateList(results: InlineQueryViewModelV2.Results) { + previousResults = results + if (results is InlineQueryViewModelV2.None || !canShow || isLandscape) { + dismiss() + } else if (results is InlineQueryViewModelV2.EmojiResults) { + showEmojiPopup(results) + } else if (results is InlineQueryViewModelV2.MentionResults) { + showMentionsPickerFragment(results) + } + } + + private fun showEmojiPopup(results: InlineQueryViewModelV2.EmojiResults) { + if (emojiPopup != null) { + emojiPopup?.setResults(results.results) + } else { + emojiPopup = InlineQueryResultsPopup( + anchor = anchor, + container = container, + results = results.results, + baseOffsetX = DimensionUnit.DP.toPixels(16f).toInt(), + callback = this + ).show() + } + } + + private fun showMentionsPickerFragment(results: InlineQueryViewModelV2.MentionResults) { + if (mentionFragment == null) { + mentionFragment = parentFragment.childFragmentManager.findFragmentByTag(MENTION_TAG) as? MentionsPickerFragmentV2 + if (mentionFragment == null) { + mentionFragment = MentionsPickerFragmentV2() + parentFragment.childFragmentManager.commit { + replace(R.id.mention_fragment_container, mentionFragment!!) + runOnCommit { mentionFragment!!.updateList(results.results) } + } + } + } else { + parentFragment.childFragmentManager.commit { + show(mentionFragment!!) + } + } + } + + private fun dismiss() { + emojiPopup?.dismiss() + emojiPopup = null + + mentionFragment?.let { + parentFragment.childFragmentManager.commit { + hide(it) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModel.kt index ca16dcb827..149725ca22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModel.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel /** * Activity (at least) scope view model for managing inline queries. The view model needs to be larger scope so it can - * be shared between the fragment requesting the search and the instace of [InlineQueryResultsFragment] used for displaying + * be shared between the fragment requesting the search and the instance of [InlineQueryResultsFragment] used for displaying * the results. */ class InlineQueryViewModel( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModelV2.kt new file mode 100644 index 0000000000..9024736b7a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/inlinequery/InlineQueryViewModelV2.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.conversation.ui.inlinequery + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.subjects.BehaviorSubject +import io.reactivex.rxjava3.subjects.PublishSubject +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel +import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewState +import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepositoryV2 +import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepository +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel + +/** + * Activity (at least) scope view model for managing inline queries. The view model needs to be larger scope so it can + * be shared between the fragment requesting the search and the fragment used for displaying the results. + */ +class InlineQueryViewModelV2( + private val recipientRepository: ConversationRecipientRepository, + private val mentionsPickerRepository: MentionsPickerRepositoryV2 = MentionsPickerRepositoryV2(), + private val emojiSearchRepository: EmojiSearchRepository = EmojiSearchRepository(ApplicationDependencies.getApplication()), + private val recentEmojis: RecentEmojiPageModel = RecentEmojiPageModel(ApplicationDependencies.getApplication(), TextSecurePreferences.RECENT_STORAGE_KEY) +) : ViewModel() { + + private val querySubject: PublishSubject = PublishSubject.create() + private val selectionSubject: PublishSubject = PublishSubject.create() + private val isMentionsShowingSubject: BehaviorSubject = BehaviorSubject.createDefault(false) + + val results: Observable + val selection: Observable = selectionSubject.observeOn(AndroidSchedulers.mainThread()) + val isMentionsShowing: Observable = isMentionsShowingSubject.observeOn(AndroidSchedulers.mainThread()) + + init { + results = querySubject.switchMap { query -> + when (query) { + is InlineQuery.Emoji -> queryEmoji(query) + is InlineQuery.Mention -> queryMentions(query) + InlineQuery.NoQuery -> Observable.just(None) + } + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun onQueryChange(inlineQuery: InlineQuery) { + querySubject.onNext(inlineQuery) + } + + private fun queryEmoji(query: InlineQuery.Emoji): Observable { + return emojiSearchRepository + .submitQuery(query.query) + .map { r -> if (r.isEmpty()) None else EmojiResults(toMappingModels(r, query.keywordSearch)) } + .toObservable() + } + + private fun queryMentions(query: InlineQuery.Mention): Observable { + return recipientRepository + .groupRecord + .take(1) + .switchMap { group -> + if (group.isPresent) { + mentionsPickerRepository.search(query.query, group.get().members) + .map { results -> if (results.isEmpty()) None else MentionResults(results.map { MentionViewState(it) }) } + .toObservable() + } else { + Observable.just(None) + } + } + } + + fun onSelection(model: AnyMappingModel) { + when (model) { + is InlineQueryEmojiResult.Model -> { + recentEmojis.onCodePointSelected(model.preferredEmoji) + selectionSubject.onNext(InlineQueryReplacement.Emoji(model.preferredEmoji, model.keywordSearch)) + } + is MentionViewState -> { + selectionSubject.onNext(InlineQueryReplacement.Mention(model.recipient, false)) + } + } + } + + fun setIsMentionsShowing(showing: Boolean) { + isMentionsShowingSubject.onNext(showing) + } + + companion object { + fun toMappingModels(emojiWithLabels: List, keywordSearch: Boolean): List { + val emojiValues = SignalStore.emojiValues() + return emojiWithLabels + .distinct() + .map { emoji -> + InlineQueryEmojiResult.Model( + canonicalEmoji = emoji, + preferredEmoji = emojiValues.getPreferredVariation(emoji), + keywordSearch = keywordSearch + ) + } + } + } + + sealed interface Results + object None : Results + data class EmojiResults(val results: List) : Results + data class MentionResults(val results: List) : Results +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerFragmentV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerFragmentV2.kt new file mode 100644 index 0000000000..e879c77803 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerFragmentV2.kt @@ -0,0 +1,126 @@ +package org.thoughtcrime.securesms.conversation.ui.mentions + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.concurrent.LifecycleDisposable +import org.signal.core.util.concurrent.addTo +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2 +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.VibrateUtil +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder + +/** + * Show inline query results for mentions in a group during message compose. + */ +class MentionsPickerFragmentV2 : LoggingFragment() { + + private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable() + private val viewModel: InlineQueryViewModelV2 by activityViewModels() + + private lateinit var adapter: MentionsPickerAdapter + private lateinit var list: RecyclerView + private lateinit var behavior: BottomSheetBehavior + + private val lockSheetAfterListUpdate = Runnable { behavior.setHideable(false) } + private val handler = Handler(Looper.getMainLooper()) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.mentions_picker_fragment, container, false) + list = view.findViewById(R.id.mentions_picker_list) + behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet)) + initializeBehavior() + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + lifecycleDisposable.bindTo(viewLifecycleOwner) + + initializeList() + viewModel + .results + .subscribeBy { + if (it !is InlineQueryViewModelV2.MentionResults) { + updateList(emptyList()) + } else { + updateList(it.results) + } + } + .addTo( + lifecycleDisposable + ) + + viewModel + .isMentionsShowing + .subscribeBy { isShowing -> + if (isShowing && VibrateUtil.isHapticFeedbackEnabled(requireContext())) { + VibrateUtil.vibrateTick(requireContext()) + } + } + .addTo(lifecycleDisposable) + } + + private fun initializeBehavior() { + behavior.isHideable = true + behavior.state = BottomSheetBehavior.STATE_HIDDEN + behavior.addBottomSheetCallback(object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + adapter.submitList(emptyList()) + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit + }) + } + + private fun initializeList() { + adapter = MentionsPickerAdapter(MentionEventListener()) { updateBottomSheetBehavior(adapter.itemCount) } + + list.layoutManager = LinearLayoutManager(requireContext()) + list.adapter = adapter + list.itemAnimator = null + } + + fun updateList(mappingModels: List>) { + if (adapter.itemCount > 0 && mappingModels.isEmpty()) { + updateBottomSheetBehavior(0) + } else { + adapter.submitList(mappingModels) + } + } + + private fun updateBottomSheetBehavior(count: Int) { + val isShowing = count > 0 + if (isShowing) { + list.scrollToPosition(0) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + handler.post(lockSheetAfterListUpdate) + } else { + handler.removeCallbacks(lockSheetAfterListUpdate) + behavior.isHideable = true + behavior.state = BottomSheetBehavior.STATE_HIDDEN + } + viewModel.setIsMentionsShowing(isShowing) + } + + private inner class MentionEventListener : RecipientViewHolder.EventListener { + override fun onModelClick(model: MentionViewState) { + viewModel.onSelection(model) + } + + override fun onClick(recipient: Recipient) = Unit + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepositoryV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepositoryV2.kt new file mode 100644 index 0000000000..03fb210734 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepositoryV2.kt @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.conversation.ui.mentions + +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.RecipientTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Search for members that match the query for rendering in the mentions picker during message compose. + */ +class MentionsPickerRepositoryV2( + private val recipients: RecipientTable = SignalDatabase.recipients +) { + fun search(query: String, members: List): Single> { + return if (query.isBlank() || members.isEmpty()) { + Single.just(emptyList()) + } else { + Single + .fromCallable { recipients.queryRecipientsForMentions(query, members) } + .subscribeOn(Schedulers.io()) + } + } +} 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 403b61c46a..f7aeaed120 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 @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.contactshare.Contact import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity import org.thoughtcrime.securesms.conversation.MessageSendType import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity @@ -32,6 +33,7 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba private val contactShareLauncher = fragment.registerForActivityResult(ContactShareEditor) { contacts -> callbacks.onSendContacts(contacts) } private val mediaSelectionLauncher = fragment.registerForActivityResult(MediaSelection) { result -> callbacks.onMediaSend(result) } + private val gifSearchLauncher = fragment.registerForActivityResult(GifSearch) { result -> callbacks.onMediaSend(result) } fun launchContactShareEditor(uri: Uri, chatColors: ChatColors) { contactShareLauncher.launch(uri to chatColors) @@ -41,14 +43,18 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba mediaSelectionLauncher.launch(MediaSelectionInput(mediaList, recipientId, text)) } - private object MediaSelection : ActivityResultContract() { + fun launchGifSearch(recipientId: RecipientId, text: CharSequence?) { + gifSearchLauncher.launch(GifSearchInput(recipientId, text)) + } + + private object MediaSelection : ActivityResultContract() { override fun createIntent(context: Context, input: MediaSelectionInput): Intent { val (media, recipientId, text) = input return MediaSelectionActivity.editor(context, MessageSendType.SignalMessageSendType, media, recipientId, text) } - override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult { - return MediaSendActivityResult.fromData(intent!!) + override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { + return intent?.let { MediaSendActivityResult.fromData(intent) } } } @@ -63,10 +69,27 @@ class ConversationActivityResultContracts(fragment: Fragment, private val callba } } + private object GifSearch : ActivityResultContract() { + override fun createIntent(context: Context, input: GifSearchInput): Intent { + return Intent(context, GiphyActivity::class.java).apply { + putExtra(GiphyActivity.EXTRA_IS_MMS, false) + putExtra(GiphyActivity.EXTRA_RECIPIENT_ID, input.recipientId) + putExtra(GiphyActivity.EXTRA_TRANSPORT, MessageSendType.SignalMessageSendType) + putExtra(GiphyActivity.EXTRA_TEXT, input.text) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): MediaSendActivityResult? { + return intent?.let { MediaSendActivityResult.fromData(intent) } + } + } + private data class MediaSelectionInput(val media: List, val recipientId: RecipientId, val text: CharSequence?) + private data class GifSearchInput(val recipientId: RecipientId, val text: CharSequence?) + interface Callbacks { fun onSendContacts(contacts: List) - fun onMediaSend(result: MediaSendActivityResult) + fun onMediaSend(result: MediaSendActivityResult?) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index d35aacd2c9..53c7fb39e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -189,7 +189,7 @@ class ConversationAdapterV2( */ fun pulseAtPosition(position: Int) { if (position >= 0 && position < itemCount) { - // todo [cody] adjust for typing indicator + // TODO [cfv2] adjust for typing indicator val correctedPosition = position recordToPulse = getConversationMessage(correctedPosition) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 34ac734c1a..fb4490296f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -29,6 +29,7 @@ import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.View.OnFocusChangeListener +import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.inputmethod.EditorInfo import android.widget.ImageButton @@ -52,6 +53,8 @@ import androidx.core.view.doOnPreDraw import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentResultListener +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit import androidx.fragment.app.viewModels import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -96,11 +99,15 @@ import org.thoughtcrime.securesms.components.ConversationSearchBottomBar import org.thoughtcrime.securesms.components.HidingLinearLayout import org.thoughtcrime.securesms.components.InputAwareConstraintLayout import org.thoughtcrime.securesms.components.InputPanel +import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout import org.thoughtcrime.securesms.components.ProgressCardDialogFragment import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs import org.thoughtcrime.securesms.components.ScrollToPositionDelegate import org.thoughtcrime.securesms.components.SendButton import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.components.emoji.EmojiEventListener +import org.thoughtcrime.securesms.components.emoji.MediaKeyboard +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.components.mention.MentionAnnotation import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar @@ -148,6 +155,11 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsControllerV2 +import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2 import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel import org.thoughtcrime.securesms.conversation.v2.keyboard.AttachmentKeyboardFragment @@ -181,8 +193,17 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiatio import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult import org.thoughtcrime.securesms.invites.InviteActions +import org.thoughtcrime.securesms.keyboard.KeyboardPage +import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment +import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel +import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment +import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment +import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment +import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment +import org.thoughtcrime.securesms.keyboard.sticker.StickerSearchDialogFragment import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.linkpreview.LinkPreview +import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2 import org.thoughtcrime.securesms.longmessage.LongMessageFragment import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory @@ -190,23 +211,26 @@ import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory.create import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult -import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository import org.thoughtcrime.securesms.messagerequests.MessageRequestState import org.thoughtcrime.securesms.mms.AttachmentManager import org.thoughtcrime.securesms.mms.AudioSlide +import org.thoughtcrime.securesms.mms.GifSlide import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.QuoteModel import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.SlideFactory import org.thoughtcrime.securesms.mms.StickerSlide +import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment +import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment @@ -220,7 +244,11 @@ import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity import org.thoughtcrime.securesms.revealable.ViewOnceUtil import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet +import org.thoughtcrime.securesms.sms.MessageSender +import org.thoughtcrime.securesms.stickers.StickerEventListener import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.stickers.StickerManagementActivity +import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity @@ -230,6 +258,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.ContextUtil import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.DeleteDialog +import org.thoughtcrime.securesms.util.Dialogs import org.thoughtcrime.securesms.util.DrawableUtil import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.FullscreenHelper @@ -241,12 +270,14 @@ import org.thoughtcrime.securesms.util.StorageUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.WindowUtil +import org.thoughtcrime.securesms.util.activityViewModel import org.thoughtcrime.securesms.util.concurrent.ListenableFuture import org.thoughtcrime.securesms.util.doAfterNextLayout import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.getRecordQuoteType import org.thoughtcrime.securesms.util.hasAudio import org.thoughtcrime.securesms.util.hasGiftBadge +import org.thoughtcrime.securesms.util.isValidReactionTarget import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.views.Stub import org.thoughtcrime.securesms.util.visible @@ -256,6 +287,7 @@ import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil import java.util.Locale import java.util.Optional import java.util.concurrent.ExecutionException +import kotlin.time.Duration.Companion.milliseconds /** * A single unified fragment for Conversations. @@ -263,12 +295,20 @@ import java.util.concurrent.ExecutionException class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment), ReactWithAnyEmojiBottomSheetDialogFragment.Callback, - ReactionsBottomSheetDialogFragment.Callback { + ReactionsBottomSheetDialogFragment.Callback, + EmojiKeyboardPageFragment.Callback, + EmojiEventListener, + GifKeyboardPageFragment.Host, + StickerEventListener, + StickerKeyboardPageFragment.Callback, + MediaKeyboard.MediaKeyboardListener, + EmojiSearchFragment.Callback { companion object { private val TAG = Log.tag(ConversationFragment::class.java) private const val ACTION_PINNED_SHORTCUT = "action_pinned_shortcut" private const val SAVED_STATE_IS_SEARCH_REQUESTED = "is_search_requested" + private const val EMOJI_SEARCH_FRAGMENT_TAG = "EmojiSearchFragment" } private val args: ConversationIntents.Args by lazy { @@ -295,6 +335,12 @@ class ConversationFragment : ) } + private val linkPreviewViewModel: LinkPreviewViewModelV2 by viewModel { + LinkPreviewViewModelV2( + enablePlaceholder = false + ) + } + private val groupCallViewModel: ConversationGroupCallViewModel by viewModels( factoryProducer = { ConversationGroupCallViewModel.Factory(args.threadId, conversationRecipientRepository) @@ -319,9 +365,30 @@ class ConversationFragment : ConversationSearchViewModel(getString(R.string.note_to_self)) } + private val keyboardPagerViewModel: KeyboardPagerViewModel by activityViewModels() + + private val stickerViewModel: StickerSuggestionsViewModel by viewModel { + StickerSuggestionsViewModel() + } + + private val inlineQueryViewModel: InlineQueryViewModelV2 by activityViewModel { + InlineQueryViewModelV2(recipientRepository = conversationRecipientRepository) + } + + private val inlineQueryController: InlineQueryResultsControllerV2 by lazy { + InlineQueryResultsControllerV2( + this, + inlineQueryViewModel, + inputPanel, + (requireView() as ViewGroup), + composeText + ) + } + private val conversationTooltips = ConversationTooltips(this) private val colorizer = Colorizer() private val textDraftSaveDebouncer = Debouncer(500) + private val recentEmojis: RecentEmojiPageModel by lazy { RecentEmojiPageModel(ApplicationDependencies.getApplication(), TextSecurePreferences.RECENT_STORAGE_KEY) } private lateinit var layoutManager: LinearLayoutManager private lateinit var markReadHelper: MarkReadHelper @@ -341,6 +408,7 @@ class ConversationFragment : private var pinnedShortcutReceiver: BroadcastReceiver? = null private var searchMenuItem: MenuItem? = null private var isSearchRequested: Boolean = false + private var previousPages: Set? = null private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy { override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) { @@ -415,6 +483,7 @@ class ConversationFragment : container.fragmentManager = childFragmentManager ToolbarDependentMarginListener(binding.toolbar) + initializeMediaKeyboard() } override fun onViewStateRestored(savedInstanceState: Bundle?) { @@ -458,6 +527,7 @@ class ConversationFragment : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) ToolbarDependentMarginListener(binding.toolbar) + inlineQueryController.onOrientationChange(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) } override fun onDestroyView() { @@ -479,6 +549,85 @@ class ConversationFragment : clearFocusedItem() } + override fun openEmojiSearch() { + val fragment = childFragmentManager.findFragmentByTag(EMOJI_SEARCH_FRAGMENT_TAG) + if (fragment == null) { + childFragmentManager.commit { + add(R.id.emoji_search_container, EmojiSearchFragment(), EMOJI_SEARCH_FRAGMENT_TAG) + } + } + } + + override fun closeEmojiSearch() { + val fragment = childFragmentManager.findFragmentByTag(EMOJI_SEARCH_FRAGMENT_TAG) + if (fragment != null) { + childFragmentManager.commit(allowStateLoss = true) { + remove(fragment) + } + } + } + + override fun onEmojiSelected(emoji: String?) { + if (emoji != null) { + inputPanel.onEmojiSelected(emoji) + recentEmojis.onCodePointSelected(emoji) + } + } + + override fun onKeyEvent(keyEvent: KeyEvent?) { + if (keyEvent != null) { + inputPanel.onKeyEvent(keyEvent) + } + } + + override fun openStickerSearch() { + StickerSearchDialogFragment.show(childFragmentManager) + } + + override fun onStickerSelected(sticker: StickerRecord) { + sendSticker( + stickerRecord = sticker, + clearCompose = false + ) + } + + override fun onStickerManagementClicked() { + startActivity(StickerManagementActivity.getIntent(requireContext())) + container.hideInput() + } + + override fun isMms(): Boolean { + return false + } + + override fun openGifSearch() { + val recipientId = viewModel.recipientSnapshot?.id ?: return + conversationActivityResultContracts.launchGifSearch(recipientId, composeText.textTrimmed) + } + + override fun onGifSelectSuccess(blobUri: Uri, width: Int, height: Int) { + setMedia( + uri = blobUri, + mediaType = SlideFactory.MediaType.from(BlobProvider.getMimeType(blobUri))!!, + width = width, + height = height, + videoGif = true + ) + } + + override fun onShown() { + inputPanel.mediaKeyboardListener.onShown() + } + + override fun onHidden() { + inputPanel.mediaKeyboardListener.onHidden() + closeEmojiSearch() + } + + override fun onKeyboardChanged(page: KeyboardPage) { + inputPanel.mediaKeyboardListener.onKeyboardChanged(page) + } + private fun observeConversationThread() { var firstRender = true disposables += viewModel @@ -586,6 +735,7 @@ class ConversationFragment : val keyboardEvents = KeyboardEvents() container.listener = keyboardEvents + container.addKeyboardStateListener(keyboardEvents) requireActivity() .onBackPressedDispatcher .addCallback( @@ -660,10 +810,41 @@ class ConversationFragment : .addTo(disposables) initializeSearch() + initializeLinkPreviews() + initializeStickerSuggestions() + initializeInlineSearch() inputPanel.setListener(InputPanelListener()) } + private fun initializeInlineSearch() { + inlineQueryController.onOrientationChange(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) + + composeText.apply { + setInlineQueryChangedListener(object : InlineQueryChangedListener { + override fun onQueryChanged(inlineQuery: InlineQuery) { + inlineQueryViewModel.onQueryChange(inlineQuery) + } + }) + + setMentionValidator { annotations -> + val recipient = viewModel.recipientSnapshot ?: return@setMentionValidator annotations + + val validIds = recipient.participantIds + .map { MentionAnnotation.idToMentionAnnotationValue(it) } + .toSet() + + annotations.filterNot { validIds.contains(it.value) } + } + } + + inlineQueryViewModel + .selection + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { r: InlineQueryReplacement -> composeText.replaceText(r) } + .addTo(disposables) + } + private fun presentInputReadyState(inputReadyState: InputReadyState) { presentConversationTitle(inputReadyState.conversationRecipient) @@ -986,9 +1167,7 @@ class ConversationFragment : val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(binding.conversationItemRecycler, callback, maxPlayback) binding.conversationItemRecycler.addItemDecoration( - GiphyMp4ItemDecoration(callback) { translationY: Float -> - binding.reactionsShade.translationY = translationY + binding.conversationItemRecycler.height - }, + GiphyMp4ItemDecoration(callback), 0 ) return callback @@ -1020,6 +1199,55 @@ class ConversationFragment : } } + private fun initializeLinkPreviews() { + linkPreviewViewModel.linkPreviewState + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { state -> + if (state.isLoading) { + inputPanel.setLinkPreviewLoading() + } else if (state.hasLinks() && !state.linkPreview.isPresent) { + inputPanel.setLinkPreviewNoPreview(state.error) + } else { + inputPanel.setLinkPreview(GlideApp.with(this), state.linkPreview) + } + + updateToggleButtonState() + } + .addTo(disposables) + } + + private fun initializeMediaKeyboard() { + val isSystemEmojiPreferred = SignalStore.settings().isPreferSystemEmoji + val keyboardMode: TextSecurePreferences.MediaKeyboardMode = TextSecurePreferences.getMediaKeyboardMode(requireContext()) + + inputPanel.showMediaKeyboardToggle(true) + + val keyboardPage = when (keyboardMode) { + TextSecurePreferences.MediaKeyboardMode.EMOJI -> if (isSystemEmojiPreferred) KeyboardPage.STICKER else KeyboardPage.EMOJI + TextSecurePreferences.MediaKeyboardMode.STICKER -> KeyboardPage.STICKER + TextSecurePreferences.MediaKeyboardMode.GIF -> KeyboardPage.GIF + } + + inputPanel.setMediaKeyboardToggleMode(keyboardPage) + keyboardPagerViewModel.switchToPage(keyboardPage) + } + + private fun initializeStickerSuggestions() { + stickerViewModel.stickers + .subscribeBy(onNext = inputPanel::setStickerSuggestions) + .addTo(disposables) + } + + private fun updateLinkPreviewState() { + // TODO [cfv2] include viewModel.isPushAvailable && in check + if (!attachmentManager.isAttachmentPresent && context != null) { + linkPreviewViewModel.onEnabled() + linkPreviewViewModel.onTextChanged(composeText.textTrimmed.toString(), composeText.selectionStart, composeText.selectionEnd) + } else { + linkPreviewViewModel.onUserCancel() + } + } + private fun updateToggleButtonState() { val buttonToggle: AnimatingToggle = binding.conversationInputPanel.buttonToggle val quickAttachment: HidingLinearLayout = binding.conversationInputPanel.quickAttachmentToggle @@ -1054,7 +1282,7 @@ class ConversationFragment : buttonToggle.display(sendButton) quickAttachment.hide() - if (!attachmentManager.isAttachmentPresent) { // todo [cfv2] && !linkPreviewViewModel.hasLinkPreviewUi()) { + if (!attachmentManager.isAttachmentPresent && !linkPreviewViewModel.hasLinkPreviewUi) { inlineAttachment.show() } else { inlineAttachment.hide() @@ -1077,6 +1305,8 @@ class ConversationFragment : ) sendMessageWithoutComposeInput(slide, clearCompose = clearCompose) + + viewModel.updateStickerLastUsedTime(stickerRecord, System.currentTimeMillis().milliseconds) } private fun sendMessageWithoutComposeInput( @@ -1092,7 +1322,8 @@ class ConversationFragment : mentions = emptyList(), bodyRanges = null, messageToEdit = null, - quote = null + quote = null, + linkPreviews = emptyList() ) } @@ -1105,9 +1336,12 @@ class ConversationFragment : scheduledDate: Long = -1, slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null, contacts: List = emptyList(), - clearCompose: Boolean = true + clearCompose: Boolean = true, + linkPreviews: List = linkPreviewViewModel.onSend(), + preUploadResults: List = emptyList(), + afterSendComplete: () -> Unit = {} ) { - val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup == true) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() } + val metricId = viewModel.recipientSnapshot?.let { if (it.isGroup) SignalLocalMetrics.GroupMessageSend.start() else SignalLocalMetrics.IndividualMessageSend.start() } val send: Completable = viewModel.sendMessage( metricId = metricId, @@ -1118,7 +1352,9 @@ class ConversationFragment : quote = quote, mentions = mentions, bodyRanges = bodyRanges, - contacts = contacts + contacts = contacts, + linkPreviews = linkPreviews, + preUploadResults = preUploadResults ) disposables += send @@ -1136,7 +1372,10 @@ class ConversationFragment : is RecipientFormattingException -> toast(R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, Toast.LENGTH_LONG) } }, - onComplete = this::onSendComplete + onComplete = { + onSendComplete() + afterSendComplete() + } ) } @@ -1153,7 +1392,7 @@ class ConversationFragment : scrollToPositionDelegate.resetScrollPosition() attachmentManager.cleanup() - // todo [cfv2] updateLinkPreviewState(); + updateLinkPreviewState() draftViewModel.onSendComplete() @@ -1349,13 +1588,6 @@ class ConversationFragment : reactionDelegate.setOnHideListener(onHideListener) reactionDelegate.show(requireActivity(), viewModel.recipientSnapshot!!, conversationMessage, conversationGroupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel) composeText.clearFocus() - - /* - // TODO [cfv2] - if (attachmentKeyboardStub.resolved()) { - attachmentKeyboardStub.get().hide(true); - } - */ } //region Message action handling @@ -1431,7 +1663,7 @@ class ConversationFragment : } private fun performAttachmentSave(attachments: Set) { - val progressDialog = ProgressCardDialogFragment() + val progressDialog = ProgressCardDialogFragment.create() progressDialog.arguments = ProgressCardDialogFragmentArgs.Builder( resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, attachments.size, attachments.size) ).build().toBundle() @@ -1893,9 +2125,7 @@ class ConversationFragment : val messageRecord = item.getMessageRecord() val recipient = viewModel.recipientSnapshot ?: return - if (messageRecord.isSecure && - !messageRecord.isRemoteDelete && - !messageRecord.isUpdate && + if (messageRecord.isValidReactionTarget() && !recipient.isBlocked && !viewModel.hasMessageRequestState && (!recipient.isGroup || recipient.isActiveGroup) && @@ -1926,8 +2156,7 @@ class ConversationFragment : val snapshot = ConversationItemSelection.snapshotView(itemView, binding.conversationItemRecycler, messageRecord, videoBitmap) - // TODO [cfv2] -- Should only have a focused view if the keyboard was open. - val focusedView = null // itemView.rootView.findFocus() + val focusedView = if (container.isInputShowing) null else itemView.rootView.findFocus() val bodyBubble = itemView.bodyBubble!! val selectedConversationModel = SelectedConversationModel( snapshot, @@ -1957,7 +2186,6 @@ class ConversationFragment : } val conversationItem: ConversationItem = itemView - val isAttachmentKeyboardOpen = false /* TODO [cfv2] -- isAttachmentKeyboardOpen */ handleReaction( item.conversationMessage, ReactionsToolbarListener(item.conversationMessage), @@ -1996,10 +2224,6 @@ class ConversationFragment : if (showScrollButtons) { viewModel.setShowScrollButtons(true) } - - if (isAttachmentKeyboardOpen) { - // listener.openAttachmentKeyboard(); - } } } ) @@ -2042,7 +2266,7 @@ class ConversationFragment : val recipient: Recipient? = viewModel.recipientSnapshot return ConversationOptionsMenu.Snapshot( recipient = recipient, - isPushAvailable = true, // TODO [cfv2] + isPushAvailable = recipient?.isRegistered == true && Recipient.self().isRegistered, canShowAsBubble = Observable.empty(), isActiveGroup = recipient?.isActiveGroup == true, isActiveV2Group = recipient?.let { it.isActiveGroup && it.isPushV2Group } == true, @@ -2344,7 +2568,7 @@ class ConversationFragment : inner class ActionModeCallback : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.title = calculateSelectedItemCount() - // TODO [cfv2] listener.onMessageActionToolbarOpened(); + // TODO [cfv2] scheduled message - listener.onMessageActionToolbarOpened(); setCorrectActionModeMenuVisibility() return true } @@ -2356,7 +2580,7 @@ class ConversationFragment : override fun onDestroyActionMode(mode: ActionMode) { adapter.clearSelection() setBottomActionBarVisibility(false) - // TODO [cfv2] listener.onMessageActionToolbarClosed(); + // TODO [cfv2] scheduled message - listener.onMessageActionToolbarClosed(); binding.conversationItemRecycler.invalidateItemDecorations() actionMode = null } @@ -2373,8 +2597,70 @@ class ConversationFragment : ) } - override fun onMediaSend(result: MediaSendActivityResult) { - // TODO [cfv2] media send + override fun onMediaSend(result: MediaSendActivityResult?) { + if (result == null) { + return + } + + val recipientSnapshot = viewModel.recipientSnapshot + if (result.recipientId != recipientSnapshot?.id) { + Log.w(TAG, "Result's recipientId did not match ours! Result: " + result.recipientId + ", Ours: " + recipientSnapshot?.id) + toast(R.string.ConversationActivity_error_sending_media) + return + } + + if (result.isPushPreUpload) { + sendPreUploadMediaMessage(result) + return + } + + val slides: List = result.nonUploadedMedia.mapNotNull { + when { + MediaUtil.isVideoType(it.mimeType) -> VideoSlide(requireContext(), it.uri, it.size, it.isVideoGif, it.width, it.height, it.caption.orNull(), it.transformProperties.orNull()) + MediaUtil.isGif(it.mimeType) -> GifSlide(requireContext(), it.uri, it.size, it.width, it.height, it.isBorderless, it.caption.orNull()) + MediaUtil.isImageType(it.mimeType) -> ImageSlide(requireContext(), it.uri, it.mimeType, it.size, it.width, it.height, it.isBorderless, it.caption.orNull(), null, it.transformProperties.orNull()) + else -> { + Log.w(TAG, "Asked to send an unexpected mimeType: '${it.mimeType}'. Skipping.") + null + } + } + } + + sendMessage( + body = result.body, + mentions = result.mentions, + bodyRanges = result.bodyRanges, + messageToEdit = null, + quote = if (result.isViewOnce) null else inputPanel.quote.orNull(), + scheduledDate = result.scheduledTime, + slideDeck = SlideDeck().apply { slides.forEach { addSlide(it) } }, + contacts = emptyList(), + clearCompose = true, + linkPreviews = emptyList() + ) { + viewModel.deleteSlideData(slides) + } + } + + private fun sendPreUploadMediaMessage(result: MediaSendActivityResult) { + if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && result.bodyRanges != null && result.bodyRanges.rangesCount > 0) { + Dialogs.showFormattedTextDialog(requireContext()) { sendPreUploadMediaMessage(result) } + return + } + + sendMessage( + body = result.body, + mentions = result.mentions, + bodyRanges = result.bodyRanges, + messageToEdit = null, + quote = if (result.isViewOnce) null else inputPanel.quote.orNull(), + scheduledDate = result.scheduledTime, + slideDeck = null, + contacts = emptyList(), + clearCompose = true, + linkPreviews = emptyList(), + preUploadResults = result.preUploadResults + ) } } @@ -2650,7 +2936,8 @@ class ConversationFragment : if (composeText.textTrimmed.isEmpty() || beforeLength == 0) { composeText.postDelayed({ updateToggleButtonState() }, 50) } - // todo [cfv2] stickerViewModel.onInputTextUpdated(s.toString()) + + stickerViewModel.onInputTextUpdated(s.toString()) } override fun onFocusChange(v: View, hasFocus: Boolean) { @@ -2660,7 +2947,7 @@ class ConversationFragment : } override fun onCursorPositionChanged(start: Int, end: Int) { - // todo [cfv2] linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), start, end); + linkPreviewViewModel.onTextChanged(composeText.textTrimmed.toString(), start, end) } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { @@ -2753,11 +3040,11 @@ class ConversationFragment : } override fun onEmojiToggle() { - // TODO [cfv2] Not yet implemented + container.toggleInput(MediaKeyboardFragmentCreator, composeText, showSoftKeyOnHide = true) } override fun onLinkPreviewCanceled() { - // TODO [cfv2] Not yet implemented + linkPreviewViewModel.onUserCancel() } override fun onStickerSuggestionSelected(sticker: StickerRecord) { @@ -2777,13 +3064,18 @@ class ConversationFragment : override fun onEnterEditMode() { updateToggleButtonState() - // TODO [cfv2] -- Save keyboard pager state and force emoji + previousPages = keyboardPagerViewModel.pages().value + keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI) + onKeyboardChanged(KeyboardPage.EMOJI) } override fun onExitEditMode() { updateToggleButtonState() draftViewModel.deleteMessageEditDraft() - // TODO [cfv2] -- Restore keyboard pager pages + if (previousPages != null) { + keyboardPagerViewModel.setPages(previousPages!!) + previousPages = null + } } } @@ -2793,7 +3085,9 @@ class ConversationFragment : private inner class AttachmentManagerListener : AttachmentManager.AttachmentListener { override fun onAttachmentChanged() { - // TODO [cfv2] implement + // TODO [cfv2] handleSecurityChange(viewModel.getConversationStateSnapshot().getSecurityInfo()); + updateToggleButtonState() + updateLinkPreviewState() } override fun onLocationRemoved() { @@ -2821,14 +3115,19 @@ class ConversationFragment : AttachmentKeyboardButton.PAYMENT -> AttachmentManager.selectPayment(this@ConversationFragment, viewModel.recipientSnapshot!!) } } else if (media != null) { - startActivityForResult(MediaSelectionActivity.editor(requireActivity(), sendButton.selectedSendType, listOf(media), viewModel.recipientSnapshot!!.id, composeText.textTrimmed), 12) + conversationActivityResultContracts.launchMediaEditor(listOf(media), viewModel.recipientSnapshot!!.id, composeText.textTrimmed) } container.hideInput() } } - private inner class KeyboardEvents : OnBackPressedCallback(false), InputAwareConstraintLayout.Listener { + private object MediaKeyboardFragmentCreator : InputAwareConstraintLayout.FragmentCreator { + override val id: Int = 2 + override fun create(): Fragment = KeyboardPagerFragment() + } + + private inner class KeyboardEvents : OnBackPressedCallback(false), InputAwareConstraintLayout.Listener, InsetAwareConstraintLayout.KeyboardStateListener { override fun handleOnBackPressed() { container.hideInput() } @@ -2840,6 +3139,12 @@ class ConversationFragment : override fun onInputHidden() { isEnabled = false } + + override fun onKeyboardShown() = Unit + + override fun onKeyboardHidden() { + closeEmojiSearch() + } } //endregion @@ -2851,6 +3156,19 @@ class ConversationFragment : viewModel.updateIdentityRecords() } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onStickerPackInstalled(event: StickerPackInstallEvent?) { + if (event == null) { + return + } + + EventBus.getDefault().removeStickyEvent(event) + + if (!inputPanel.isStickerMode) { + conversationTooltips.displayStickerPackInstalledTooltip(inputPanel.mediaKeyboardToggleAnchorView, event) + } + } + //endregion private inner class SearchEventListener : ConversationSearchBottomBar.EventListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index 01a1311bf0..9c22de7fc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -23,6 +23,7 @@ import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.SingleEmitter import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.StreamUtil +import org.signal.core.util.concurrent.MaybeCompat import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.dp import org.signal.core.util.logging.Log @@ -67,16 +68,19 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.database.model.StickerRecord import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.messagerequests.MessageRequestState import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.OutgoingMessage import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.QuoteModel +import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil import org.thoughtcrime.securesms.providers.BlobProvider @@ -85,6 +89,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.search.MessageResult import org.thoughtcrime.securesms.sms.MessageSender +import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.DrawableUtil import org.thoughtcrime.securesms.util.MediaUtil @@ -98,6 +103,7 @@ import org.thoughtcrime.securesms.util.requireTextSlide import java.io.IOException import java.util.Optional import kotlin.math.max +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class ConversationRepository( @@ -193,10 +199,12 @@ class ConversationRepository( quote: QuoteModel?, mentions: List, bodyRanges: BodyRangeList?, - contacts: List + contacts: List, + linkPreviews: List, + preUploadResults: List ): Completable { val sendCompletable = Completable.create { emitter -> - if (body.isEmpty() && slideDeck?.containsMediaSlide() != true) { + if (body.isEmpty() && slideDeck?.containsMediaSlide() != true && preUploadResults.isEmpty()) { emitter.onError(InvalidMessageException("Message is empty!")) return@create } @@ -218,17 +226,30 @@ class ConversationRepository( outgoingQuote = quote, messageToEdit = messageToEdit?.id ?: 0, mentions = mentions, - sharedContacts = contacts + sharedContacts = contacts, + linkPreviews = linkPreviews, + attachments = slideDeck?.asAttachments() ?: emptyList() ) - MessageSender.send( - ApplicationDependencies.getApplication(), - message, - threadId, - MessageSender.SendType.SIGNAL, - metricId - ) { - emitter.onComplete() + if (preUploadResults.isEmpty()) { + MessageSender.send( + ApplicationDependencies.getApplication(), + message, + threadId, + MessageSender.SendType.SIGNAL, + metricId + ) { + emitter.onComplete() + } + } else { + MessageSender.sendPushWithPreUploadedMedia( + ApplicationDependencies.getApplication(), + message, + preUploadResults, + threadId + ) { + emitter.onComplete() + } } } @@ -373,7 +394,7 @@ class ConversationRepository( } fun getTemporaryViewOnceUri(mmsMessageRecord: MmsMessageRecord): Maybe { - return Maybe.fromCallable { + return MaybeCompat.fromCallable { Log.i(TAG, "Copying the view-once photo to temp storage and deleting underlying media.") try { @@ -522,6 +543,23 @@ class ConversationRepository( return oldConversationRepository.resolveMessageToEdit(conversationMessage) } + fun deleteSlideData(slides: List) { + SignalExecutors.BOUNDED_IO.execute { + slides + .mapNotNull(Slide::getUri) + .filter(BlobProvider::isAuthority) + .forEach { + BlobProvider.getInstance().delete(applicationContext, it) + } + } + } + + fun updateStickerLastUsedTime(stickerRecord: StickerRecord, timestamp: Duration) { + SignalExecutors.BOUNDED_IO.execute { + SignalDatabase.stickers.updateStickerLastUsedTime(stickerRecord.rowId, timestamp.inWholeMilliseconds) + } + } + /** * Glide target for a contact photo which expects an error drawable, and publishes * the result to the given emitter. diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTooltips.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTooltips.kt index 24148445ea..261a4e6b0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTooltips.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationTooltips.kt @@ -9,6 +9,8 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.TooltipPopup import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent +import org.thoughtcrime.securesms.util.TextSecurePreferences /** * Any and all tooltips that the conversation can display, and a light amount of related presentation logic. @@ -51,6 +53,19 @@ class ConversationTooltips(fragment: Fragment) { .show(TooltipPopup.POSITION_BELOW) } + /** + * Displayed to teach the user about sticker packs + */ + /** + * Displayed after a sticker pack is installed + */ + fun displayStickerPackInstalledTooltip(anchor: View, event: StickerPackInstallEvent) { + TooltipPopup.forTarget(anchor) + .setText(R.string.ConversationActivity_sticker_pack_installed) + .setIconGlideModel(event.iconGlideModel) + .show(TooltipPopup.POSITION_ABOVE) + } + /** * ViewModel which holds different bits of session-local persistent state for different tooltips. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index ef1079ea34..c3505501aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -42,23 +42,28 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.Quote import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.database.model.StickerRecord import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository import org.thoughtcrime.securesms.messagerequests.MessageRequestState import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.QuoteModel +import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.search.MessageResult +import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.hasGiftBadge import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.wallpaper.ChatWallpaper import java.util.Optional +import kotlin.time.Duration /** * ConversationViewModel, which operates solely off of a thread id that never changes. @@ -297,7 +302,9 @@ class ConversationViewModel( quote: QuoteModel?, mentions: List, bodyRanges: BodyRangeList?, - contacts: List + contacts: List, + linkPreviews: List, + preUploadResults: List ): Completable { return repository.sendMessage( threadId = threadId, @@ -310,7 +317,9 @@ class ConversationViewModel( quote = quote, mentions = mentions, bodyRanges = bodyRanges, - contacts = contacts + contacts = contacts, + linkPreviews = linkPreviews, + preUploadResults = preUploadResults ).observeOn(AndroidSchedulers.mainThread()) } @@ -351,4 +360,12 @@ class ConversationViewModel( fun resolveMessageToEdit(conversationMessage: ConversationMessage): Single { return repository.resolveMessageToEdit(conversationMessage) } + + fun deleteSlideData(slides: List) { + repository.deleteSlideData(slides) + } + + fun updateStickerLastUsedTime(stickerRecord: StickerRecord, timestamp: Duration) { + repository.updateStickerLastUsedTime(stickerRecord, timestamp) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/StickerSuggestionsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/StickerSuggestionsViewModel.kt new file mode 100644 index 0000000000..55470a9281 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/StickerSuggestionsViewModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.processors.BehaviorProcessor +import org.thoughtcrime.securesms.database.model.StickerRecord +import org.thoughtcrime.securesms.stickers.StickerSearchRepository + +class StickerSuggestionsViewModel( + private val stickerSearchRepository: StickerSearchRepository = StickerSearchRepository() +) : ViewModel() { + + private val stickerSearchProcessor = BehaviorProcessor.createDefault("") + + val stickers: Flowable> = stickerSearchProcessor + .switchMapSingle { stickerSearchRepository.searchByEmoji(it) } + + fun onInputTextUpdated(text: String) { + stickerSearchProcessor.onNext(text) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/VoiceMessageRecordingDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/VoiceMessageRecordingDelegate.kt index 68e09afe4b..204c2be9bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/VoiceMessageRecordingDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/VoiceMessageRecordingDelegate.kt @@ -47,7 +47,7 @@ class VoiceMessageRecordingDelegate( private val voiceRecorderWakeLock = VoiceRecorderWakeLock(fragment.requireActivity()) private val bluetoothVoiceNoteUtil = BluetoothVoiceNoteUtil.create( fragment.requireContext(), - this::beginRecording, + this::onBluetoothConnectionAttempt, this::onBluetoothPermissionDenied ) @@ -104,6 +104,10 @@ class VoiceMessageRecordingDelegate( bluetoothVoiceNoteUtil.connectBluetoothScoConnection() } + private fun onBluetoothConnectionAttempt(success: Boolean) { + beginRecording() + } + @Suppress("DEPRECATION") private fun beginRecording() { val vibrator = ServiceUtil.getVibrator(fragment.requireContext()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt index b0bc2722f1..42f60dfc15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -32,13 +32,18 @@ import org.whispersystems.signalservice.api.push.ServiceId private typealias ConversationElement = MappingModel<*> sealed interface ConversationElementKey { + + fun requireMessageId(): Long = error("Not implemented for this key") + companion object { fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id) val threadHeader: ConversationElementKey = ThreadHeaderKey } } -private data class MessageBackedKey(val id: Long) : ConversationElementKey +private data class MessageBackedKey(val id: Long) : ConversationElementKey { + override fun requireMessageId(): Long = id +} private object ThreadHeaderKey : ConversationElementKey /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt index ff6da0f845..ea9a2aa8c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/keyboard/AttachmentKeyboardFragment.kt @@ -12,6 +12,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.addTo @@ -62,6 +63,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_ conversationViewModel = ViewModelProvider(requireParentFragment()).get(ConversationViewModel::class.java) conversationViewModel .recipient + .observeOn(AndroidSchedulers.mainThread()) .subscribeBy { attachmentKeyboardView.setWallpaperEnabled(it.hasWallpaper()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationFilterLatch.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationFilterLatch.kt index e67f79f201..c14d1e47dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationFilterLatch.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationFilterLatch.kt @@ -6,5 +6,5 @@ package org.thoughtcrime.securesms.conversationlist */ enum class ConversationFilterLatch { SET, - RESET; + RESET } 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 46b7c78289..8facc46555 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -1251,7 +1251,7 @@ private void fadeOutButtonsAndMegaphone(int fadeDuration) { if (cameraFab != null) { ViewUtil.fadeOut(cameraFab, fadeDuration); } - if (megaphoneContainer.resolved()) { + if (megaphoneContainer != null && megaphoneContainer.resolved()) { ViewUtil.fadeOut(megaphoneContainer.get(), fadeDuration); } } @@ -1263,7 +1263,7 @@ private void fadeInButtonsAndMegaphone(int fadeDuration) { if (cameraFab != null) { ViewUtil.fadeIn(cameraFab, fadeDuration); } - if (megaphoneContainer.resolved()) { + if (megaphoneContainer != null && megaphoneContainer.resolved()) { ViewUtil.fadeIn(megaphoneContainer.get(), fadeDuration); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterPullState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterPullState.kt index 68541e925b..9388ec54e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterPullState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/FilterPullState.kt @@ -40,5 +40,5 @@ enum class FilterPullState { /** * The filter is being removed and the animation is running */ - CLOSING; + CLOSING } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 3e2b9c3eb6..989e529de6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -50,21 +50,21 @@ public class PreKeyUtil { private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30); public synchronized static @NonNull List generateAndStoreOneTimeEcPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { - int preKeyIdOffset = metadataStore.getNextEcOneTimePreKeyId(); - final List records = generateOneTimeEcPreKeys(preKeyIdOffset); + int startingId = metadataStore.getNextEcOneTimePreKeyId(); + final List records = generateOneTimeEcPreKeys(startingId); - storeOneTimeEcPreKeys(protocolStore, metadataStore, preKeyIdOffset, records); + storeOneTimeEcPreKeys(protocolStore, metadataStore, records); return records; } - public synchronized static List generateOneTimeEcPreKeys(int preKeyIdOffset) { + public synchronized static List generateOneTimeEcPreKeys(int startingId) { Log.i(TAG, "Generating one-time EC prekeys..."); List records = new ArrayList<>(BATCH_SIZE); for (int i = 0; i < BATCH_SIZE; i++) { - int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; + int preKeyId = (startingId + i) % Medium.MAX_VALUE; ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); @@ -74,47 +74,64 @@ public synchronized static List generateOneTimeEcPreKeys(int preKe return records; } - public synchronized static void storeOneTimeEcPreKeys(@NonNull SignalProtocolStore protocolStore, PreKeyMetadataStore metadataStore, int preKeyIdOffset, List prekeys) { + public synchronized static void storeOneTimeEcPreKeys(@NonNull SignalProtocolStore protocolStore, PreKeyMetadataStore metadataStore, List prekeys) { Log.i(TAG, "Storing one-time EC prekeys..."); + if (prekeys.isEmpty()) { + Log.w(TAG, "Empty list of one-time EC prekeys! Nothing to store."); + return; + } + for (PreKeyRecord record : prekeys) { protocolStore.storePreKey(record.getId(), record); } - metadataStore.setNextEcOneTimePreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); + + int lastId = prekeys.get(prekeys.size() - 1).getId(); + + metadataStore.setNextEcOneTimePreKeyId((lastId + 1) % Medium.MAX_VALUE); } public synchronized static @NonNull List generateAndStoreOneTimeKyberPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { - int preKeyIdOffset = metadataStore.getNextKyberPreKeyId(); - List records = generateOneTimeKyberPreKeyRecords(preKeyIdOffset, protocolStore.getIdentityKeyPair().getPrivateKey()); + int startingId = metadataStore.getNextKyberPreKeyId(); + List records = generateOneTimeKyberPreKeyRecords(startingId, protocolStore.getIdentityKeyPair().getPrivateKey()); - storeOneTimeKyberPreKeys(protocolStore, metadataStore, preKeyIdOffset, records); + storeOneTimeKyberPreKeys(protocolStore, metadataStore, records); return records; } @NonNull - public static List generateOneTimeKyberPreKeyRecords(int preKeyIdOffset, @NonNull ECPrivateKey privateKey) { + public static List generateOneTimeKyberPreKeyRecords(int startingId, @NonNull ECPrivateKey privateKey) { Log.i(TAG, "Generating one-time kyber prekeys..."); List records = new LinkedList<>(); for (int i = 0; i < BATCH_SIZE; i++) { - int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; + int preKeyId = (startingId + i) % Medium.MAX_VALUE; KyberPreKeyRecord record = generateKyberPreKey(preKeyId, privateKey); records.add(record); } + return records; } - public synchronized static void storeOneTimeKyberPreKeys(@NonNull SignalProtocolStore protocolStore, PreKeyMetadataStore metadataStore, int preKeyIdOffset, List prekeys) { + public synchronized static void storeOneTimeKyberPreKeys(@NonNull SignalProtocolStore protocolStore, PreKeyMetadataStore metadataStore, List prekeys) { Log.i(TAG, "Storing one-time kyber prekeys..."); + if (prekeys.isEmpty()) { + Log.w(TAG, "Empty list of kyber prekeys! Nothing to store."); + return; + } + for (KyberPreKeyRecord record : prekeys) { protocolStore.storeKyberPreKey(record.getId(), record); } - metadataStore.setNextKyberPreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); + + int lastId = prekeys.get(prekeys.size() - 1).getId(); + + metadataStore.setNextKyberPreKeyId((lastId + 1) % Medium.MAX_VALUE); } public synchronized static @NonNull SignedPreKeyRecord generateAndStoreSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { @@ -127,7 +144,8 @@ public synchronized static void storeOneTimeKyberPreKeys(@NonNull SignalProtocol { int signedPreKeyId = metadataStore.getNextSignedPreKeyId(); SignedPreKeyRecord record = generateSignedPreKey(signedPreKeyId, privateKey); - storeSignedPreKey(protocolStore, metadataStore, signedPreKeyId, record); + + storeSignedPreKey(protocolStore, metadataStore, record); return record; } @@ -145,11 +163,11 @@ public synchronized static void storeOneTimeKyberPreKeys(@NonNull SignalProtocol } } - public synchronized static void storeSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, int signedPreKeyId, SignedPreKeyRecord record) { - Log.i(TAG, "Storing signed prekeys..."); + public synchronized static void storeSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, SignedPreKeyRecord record) { + Log.i(TAG, "Storing signed prekey..."); - protocolStore.storeSignedPreKey(signedPreKeyId, record); - metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE); + protocolStore.storeSignedPreKey(record.getId(), record); + metadataStore.setNextSignedPreKeyId((record.getId() + 1) % Medium.MAX_VALUE); } public synchronized static @NonNull KyberPreKeyRecord generateAndStoreLastResortKyberPreKey(@NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { @@ -162,24 +180,28 @@ public synchronized static void storeSignedPreKey(@NonNull SignalProtocolStore p { int id = metadataStore.getNextKyberPreKeyId(); KyberPreKeyRecord record = generateKyberPreKey(id, privateKey); - storeLastResortKyberPreKey(protocolStore, metadataStore, id, record); + + storeLastResortKyberPreKey(protocolStore, metadataStore, record); return record; } - public synchronized static @NonNull KyberPreKeyRecord generateKyberPreKey(int id, @NonNull ECPrivateKey privateKey) { - Log.i(TAG, "Generating kyber prekeys..."); + public synchronized static @NonNull KyberPreKeyRecord generateLastRestortKyberPreKey(int id, @NonNull ECPrivateKey privateKey) { + Log.i(TAG, "Generating last resort kyber prekey..."); + return generateKyberPreKey(id, privateKey); + } + private synchronized static @NonNull KyberPreKeyRecord generateKyberPreKey(int id, @NonNull ECPrivateKey privateKey) { KEMKeyPair keyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024); byte[] signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize()); return new KyberPreKeyRecord(id, System.currentTimeMillis(), keyPair, signature); } - public synchronized static void storeLastResortKyberPreKey(@NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, int id, KyberPreKeyRecord record) { + public synchronized static void storeLastResortKyberPreKey(@NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, KyberPreKeyRecord record) { Log.i(TAG, "Storing kyber prekeys..."); - protocolStore.storeKyberPreKey(id, record); - metadataStore.setNextKyberPreKeyId((id + 1) % Medium.MAX_VALUE); + protocolStore.storeKyberPreKey(record.getId(), record); + metadataStore.setNextKyberPreKeyId((record.getId() + 1) % Medium.MAX_VALUE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ReentrantSessionLock.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ReentrantSessionLock.java index 0b03538c6e..994df829ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ReentrantSessionLock.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ReentrantSessionLock.java @@ -18,4 +18,8 @@ public Lock acquire() { LOCK.lock(); return LOCK::unlock; } + + public boolean isHeldByCurrentThread() { + return LOCK.isHeldByCurrentThread(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java index f8d323a605..cda18bcfbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java @@ -126,6 +126,7 @@ public class AttachmentTable extends DatabaseTable { static final String DISPLAY_ORDER = "display_order"; static final String UPLOAD_TIMESTAMP = "upload_timestamp"; static final String CDN_NUMBER = "cdn_number"; + static final String MAC_DIGEST = "incremental_mac_digest"; private static final String DIRECTORY = "parts"; @@ -143,7 +144,7 @@ public class AttachmentTable extends DatabaseTable { private static final String[] PROJECTION = new String[] {ROW_ID, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, CDN_NUMBER, CONTENT_LOCATION, DATA, - TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, + TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, MAC_DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, VIDEO_GIF, QUOTE, DATA_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH, @@ -188,7 +189,8 @@ public class AttachmentTable extends DatabaseTable { TRANSFER_FILE + " TEXT DEFAULT NULL, " + DISPLAY_ORDER + " INTEGER DEFAULT 0, " + UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0, " + - CDN_NUMBER + " INTEGER DEFAULT 0);"; + CDN_NUMBER + " INTEGER DEFAULT 0, " + + MAC_DIGEST + " BLOB);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -681,6 +683,7 @@ public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull Attachme contentValues.put(CDN_NUMBER, sourceAttachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation()); contentValues.put(DIGEST, sourceAttachment.getDigest()); + contentValues.put(MAC_DIGEST, sourceAttachment.getIncrementalDigest()); contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey()); contentValues.put(NAME, sourceAttachment.getRelay()); contentValues.put(SIZE, sourceAttachment.getSize()); @@ -729,6 +732,7 @@ public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attac values.put(CDN_NUMBER, attachment.getCdnNumber()); values.put(CONTENT_LOCATION, attachment.getLocation()); values.put(DIGEST, attachment.getDigest()); + values.put(MAC_DIGEST, attachment.getIncrementalDigest()); values.put(CONTENT_DISPOSITION, attachment.getKey()); values.put(NAME, attachment.getRelay()); values.put(SIZE, attachment.getSize()); @@ -1224,6 +1228,7 @@ public List getAttachments(@NonNull Cursor cursor) { object.getString(CONTENT_DISPOSITION), object.getString(NAME), null, + null, object.getString(FAST_PREFLIGHT_ID), object.getInt(VOICE_NOTE) == 1, object.getInt(BORDERLESS) == 1, @@ -1271,6 +1276,7 @@ public List getAttachments(@NonNull Cursor cursor) { cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), cursor.getString(cursor.getColumnIndexOrThrow(NAME)), cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), + cursor.getBlob(cursor.getColumnIndexOrThrow(MAC_DIGEST)), cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1, @@ -1337,6 +1343,7 @@ private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean contentValues.put(CDN_NUMBER, useTemplateUpload ? template.getCdnNumber() : attachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, useTemplateUpload ? template.getLocation() : attachment.getLocation()); contentValues.put(DIGEST, useTemplateUpload ? template.getDigest() : attachment.getDigest()); + contentValues.put(MAC_DIGEST, useTemplateUpload ? template.getIncrementalDigest() : attachment.getIncrementalDigest()); contentValues.put(CONTENT_DISPOSITION, useTemplateUpload ? template.getKey() : attachment.getKey()); contentValues.put(NAME, useTemplateUpload ? template.getRelay() : attachment.getRelay()); contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(attachment.getFileName())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index c7c89e6ed6..6ff71db05d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -1393,7 +1393,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT } enum class MemberSet(val includeSelf: Boolean, val includePending: Boolean) { - FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false), FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true); + FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false), FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true) } /** 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 5aa7d698b1..00bcc143b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -48,6 +48,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CAPTION}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.NAME}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP}, + ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MAC_DIGEST}, ${MessageTable.TABLE_NAME}.${MessageTable.TYPE}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT}, ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED}, @@ -55,7 +56,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID}, ${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID}, ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} as $THREAD_RECIPIENT_ID - FROM + FROM ${AttachmentTable.TABLE_NAME} LEFT JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID} LEFT JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} = ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} 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 254ac50d89..44afd96bf6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -4941,7 +4941,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat protected enum class ReceiptType(val columnName: String, val groupStatus: Int) { READ(READ_RECEIPT_COUNT, GroupReceiptTable.STATUS_READ), DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptTable.STATUS_DELIVERED), - VIEWED(VIEWED_RECEIPT_COUNT, GroupReceiptTable.STATUS_VIEWED); + VIEWED(VIEWED_RECEIPT_COUNT, GroupReceiptTable.STATUS_VIEWED) } data class SyncMessageId( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt index a2837fdaad..b42c5c7ecb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.database.Cursor import android.text.TextUtils +import net.zetetic.database.sqlcipher.SQLiteDatabase import org.intellij.lang.annotations.Language import org.signal.core.util.SqlUtil import org.signal.core.util.ThreadUtil @@ -231,7 +232,8 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa /** * Drops all tables and recreates them. */ - fun fullyResetTables() { + @JvmOverloads + fun fullyResetTables(db: SQLiteDatabase = writableDatabase.sqlCipherDatabase) { Log.w(TAG, "[fullyResetTables] Dropping tables and triggers...") writableDatabase.execSQL("DROP TABLE IF EXISTS $FTS_TABLE_NAME") writableDatabase.execSQL("DROP TABLE IF EXISTS ${FTS_TABLE_NAME}_config") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SqlCipherErrorHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SqlCipherErrorHandler.kt index 890ddb897a..d51cd42290 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SqlCipherErrorHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SqlCipherErrorHandler.kt @@ -16,6 +16,7 @@ import java.util.concurrent.atomic.AtomicReference /** * The default error handler wipes the file. This one instead prints some diagnostics and then crashes so the original corrupt file isn't lost. */ +@Suppress("ClassName") class SqlCipherErrorHandler(private val databaseName: String) : DatabaseErrorHandler { override fun onCorruption(db: SQLiteDatabase, message: String) { @@ -35,7 +36,7 @@ class SqlCipherErrorHandler(private val databaseName: String) : DatabaseErrorHan endCount++ } - attemptToClearFullTextSearchIndex() + attemptToClearFullTextSearchIndex(db) throw DatabaseCorruptedError_BothChecksPass(lines) } else if (!result.pragma1Passes && result.pragma2Passes) { throw DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses(lines) @@ -144,9 +145,9 @@ class SqlCipherErrorHandler(private val databaseName: String) : DatabaseErrorHan } } - private fun attemptToClearFullTextSearchIndex() { + private fun attemptToClearFullTextSearchIndex(db: SQLiteDatabase) { try { - SignalDatabase.messageSearch.fullyResetTables() + SignalDatabase.messageSearch.fullyResetTables(db) } catch (e: Throwable) { Log.w(TAG, "Failed to clear full text search index.", e) } 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 d06030c63b..ba305bc23c 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 @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V194_KyberPreKeyMig import org.thoughtcrime.securesms.database.helpers.migration.V195_GroupMemberForeignKeyMigration import org.thoughtcrime.securesms.database.helpers.migration.V196_BackCallLinksWithRecipientV2 import org.thoughtcrime.securesms.database.helpers.migration.V197_DropAvatarColorFromCallLinks +import org.thoughtcrime.securesms.database.helpers.migration.V198_AddMacDigestColumn /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -61,7 +62,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 197 + const val DATABASE_VERSION = 198 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -261,6 +262,10 @@ object SignalDatabaseMigrations { if (oldVersion < 197) { V197_DropAvatarColorFromCallLinks.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 198) { + V198_AddMacDigestColumn.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V150_UrgentMslFlagMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V150_UrgentMslFlagMigration.kt index 6fbe2845b1..94e61be153 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V150_UrgentMslFlagMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V150_UrgentMslFlagMigration.kt @@ -7,6 +7,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase * Adding an urgent flag to message envelopes to help with notifications. Need to track flag in * MSL table so can be resent with the correct urgency. */ +@Suppress("ClassName") object V150_UrgentMslFlagMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE msl_payload ADD COLUMN urgent INTEGER NOT NULL DEFAULT 1") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V151_MyStoryMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V151_MyStoryMigration.kt index 5b6e0a3b3d..c0392866b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V151_MyStoryMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V151_MyStoryMigration.kt @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.util.Base64 /** * Performs a check and ensures that MyStory exists at the correct distribution list id and correct distribution id. */ +@Suppress("ClassName") object V151_MyStoryMigration : SignalDatabaseMigration { private val TAG = Log.tag(V151_MyStoryMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V152_StoryGroupTypesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V152_StoryGroupTypesMigration.kt index c9816572bc..2e976166d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V152_StoryGroupTypesMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V152_StoryGroupTypesMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Marks story recipients with a new group type constant. */ +@Suppress("ClassName") object V152_StoryGroupTypesMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V153_MyStoryMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V153_MyStoryMigration.kt index cae9974c7a..93b5be18ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V153_MyStoryMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V153_MyStoryMigration.kt @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.util.Base64 /** * Performs a check and ensures that MyStory exists at the correct distribution list id and correct distribution id. */ +@Suppress("ClassName") object V153_MyStoryMigration : SignalDatabaseMigration { private val TAG = Log.tag(V153_MyStoryMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V154_PniSignaturesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V154_PniSignaturesMigration.kt index 418aeafc9c..23998c6b21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V154_PniSignaturesMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V154_PniSignaturesMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Introduces the tables and fields required to keep track of whether we need to send a PNI signature message and if the ones we've sent out have been received. */ +@Suppress("ClassName") object V154_PniSignaturesMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V155_SmsExporterMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V155_SmsExporterMigration.kt index ccf9680052..e6e0831914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V155_SmsExporterMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V155_SmsExporterMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Adds necessary book-keeping columns to SMS and MMS tables for SMS export. */ +@Suppress("ClassName") object V155_SmsExporterMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE mms ADD COLUMN export_state BLOB DEFAULT NULL") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V156_RecipientUnregisteredTimestampMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V156_RecipientUnregisteredTimestampMigration.kt index 16f60c79b7..a84a466d8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V156_RecipientUnregisteredTimestampMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V156_RecipientUnregisteredTimestampMigration.kt @@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit * Adds an 'unregistered timestamp' on a recipient to keep track of when they became unregistered. * Also updates all currently-unregistered users to have an unregistered time of "now". */ +@Suppress("ClassName") object V156_RecipientUnregisteredTimestampMigration : SignalDatabaseMigration { const val UNREGISTERED = 2 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt index 7dfec71f62..a1e4ffc1b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.helpers.migration import android.app.Application import net.zetetic.database.sqlcipher.SQLiteDatabase +@Suppress("ClassName") object V157_RecipeintHiddenMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE recipient ADD COLUMN hidden INTEGER DEFAULT 0") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt index 8cac5cf3ea..c89f41b6a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V158_GroupsLastForceUpdateTimestampMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Track last time we did a forced sanity check for this group with the server. */ +@Suppress("ClassName") object V158_GroupsLastForceUpdateTimestampMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE groups ADD COLUMN last_force_update_timestamp INTEGER DEFAULT 0") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V159_ThreadUnreadSelfMentionCount.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V159_ThreadUnreadSelfMentionCount.kt index 4fe845544c..f7dc51d0c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V159_ThreadUnreadSelfMentionCount.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V159_ThreadUnreadSelfMentionCount.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.helpers.migration import android.app.Application import net.zetetic.database.sqlcipher.SQLiteDatabase +@Suppress("ClassName") object V159_ThreadUnreadSelfMentionCount : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE thread ADD COLUMN unread_self_mention_count INTEGER DEFAULT 0") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V160_SmsMmsExportedIndexMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V160_SmsMmsExportedIndexMigration.kt index e93bc42c42..7fd0a530ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V160_SmsMmsExportedIndexMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V160_SmsMmsExportedIndexMigration.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.helpers.migration import android.app.Application import net.zetetic.database.sqlcipher.SQLiteDatabase +@Suppress("ClassName") object V160_SmsMmsExportedIndexMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("CREATE INDEX IF NOT EXISTS sms_exported_index ON sms (exported)") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V161_StorySendMessageIdIndex.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V161_StorySendMessageIdIndex.kt index 2e66f32237..27d414e0b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V161_StorySendMessageIdIndex.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V161_StorySendMessageIdIndex.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Adds an index to the story sends table to help with a new common query. */ +@Suppress("ClassName") object V161_StorySendMessageIdIndex : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("CREATE INDEX IF NOT EXISTS story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V162_ThreadUnreadSelfMentionCountFixup.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V162_ThreadUnreadSelfMentionCountFixup.kt index 273ca3a83a..301cea626c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V162_ThreadUnreadSelfMentionCountFixup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V162_ThreadUnreadSelfMentionCountFixup.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations * A bad cherry-pick for a database change requires us to attempt to alter the table again * to fix it. */ +@Suppress("ClassName") object V162_ThreadUnreadSelfMentionCountFixup : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt index 7a7cdc714f..2da677575f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V163_RemoteMegaphoneSnoozeSupportMigration.kt @@ -7,6 +7,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Add columns needed to track remote megaphone specific snooze rates. */ +@Suppress("ClassName") object V163_RemoteMegaphoneSnoozeSupportMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { if (columnMissing(db, "primary_action_data")) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V164_ThreadDatabaseReadIndexMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V164_ThreadDatabaseReadIndexMigration.kt index 0af825aba4..4ef7378082 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V164_ThreadDatabaseReadIndexMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V164_ThreadDatabaseReadIndexMigration.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.helpers.migration import android.app.Application import net.zetetic.database.sqlcipher.SQLiteDatabase +@Suppress("ClassName") object V164_ThreadDatabaseReadIndexMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("CREATE INDEX IF NOT EXISTS thread_read ON thread (read);") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt index dc38e56ecf..6ac6c6839a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V166_ThreadAndMessageForeignKeys.kt @@ -14,6 +14,7 @@ import org.signal.core.util.update * This one's a doozy. We want to add additional foreign key constraints between the thread, recipient, and message tables. This will let us know for sure * that there aren't threads with invalid recipients, or messages with invalid threads, or multiple threads for the same recipient. */ +@Suppress("ClassName") object V166_ThreadAndMessageForeignKeys : SignalDatabaseMigration { private val TAG = Log.tag(V166_ThreadAndMessageForeignKeys::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V167_RecreateReactionTriggers.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V167_RecreateReactionTriggers.kt index a87bfc3834..5253879b83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V167_RecreateReactionTriggers.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V167_RecreateReactionTriggers.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Forgot to recreate the triggers for the reactions table in [V166_ThreadAndMessageForeignKeys]. So we gotta fix stuff up and do it here. */ +@Suppress("ClassName") object V167_RecreateReactionTriggers : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt index 4b78e65f96..317ff73d5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt @@ -7,6 +7,7 @@ import org.signal.core.util.Stopwatch import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.keyvalue.SignalStore +@Suppress("ClassName") object V168_SingleMessageTableMigration : SignalDatabaseMigration { private val TAG = Log.tag(V168_SingleMessageTableMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V169_EmojiSearchIndexRank.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V169_EmojiSearchIndexRank.kt index 40fafa0eda..fc980d54b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V169_EmojiSearchIndexRank.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V169_EmojiSearchIndexRank.kt @@ -7,6 +7,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase * We want to add a new `rank` column to the emoji_search table, and we no longer use it as an FTS * table, so we can get rid of that too. */ +@Suppress("ClassName") object V169_EmojiSearchIndexRank : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt index 9ca79cfe2b..27af1faf50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V171_ThreadForeignKeyFix.kt @@ -13,6 +13,7 @@ import org.signal.core.util.update * When we ran [V166_ThreadAndMessageForeignKeys], we forgot to update the actual table definition in [ThreadTable]. * We could make this conditional, but I'd rather run it on everyone just so it's more predictable. */ +@Suppress("ClassName") object V171_ThreadForeignKeyFix : SignalDatabaseMigration { private val TAG = Log.tag(V171_ThreadForeignKeyFix::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V173_ScheduledMessagesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V173_ScheduledMessagesMigration.kt index 690eab7db3..ba95e1c814 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V173_ScheduledMessagesMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V173_ScheduledMessagesMigration.kt @@ -7,6 +7,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase * In order to support scheduled sending, we need to add another column to keep track of when to send the message. We also use this * column to hide future scheduled messages from views. */ +@Suppress("ClassName") object V173_ScheduledMessagesMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE mms ADD COLUMN scheduled_date INTEGER DEFAULT -1") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V176_AddScheduledDateToQuoteIndex.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V176_AddScheduledDateToQuoteIndex.kt index 26f10bc810..41c218616a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V176_AddScheduledDateToQuoteIndex.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V176_AddScheduledDateToQuoteIndex.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Expand quote index to included scheduled date so they can be excluded. */ +@Suppress("ClassName") object V176_AddScheduledDateToQuoteIndex : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("DROP INDEX IF EXISTS mms_quote_id_quote_author_index") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt index 4f8cb91852..25995337fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V178_ReportingTokenColumnMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * This adds a column to the Recipients table to store a spam reporting token. */ +@Suppress("ClassName") object V178_ReportingTokenColumnMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE recipient ADD COLUMN reporting_token BLOB DEFAULT NULL") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V179_CleanupDanglingMessageSendLogMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V179_CleanupDanglingMessageSendLogMigration.kt index 6ff95407cf..8ab7ad455e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V179_CleanupDanglingMessageSendLogMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V179_CleanupDanglingMessageSendLogMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * This cleans up some MSL entries that we left behind during a bad past migration. */ +@Suppress("ClassName") object V179_CleanupDanglingMessageSendLogMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("DELETE FROM msl_message WHERE payload_id NOT IN (SELECT _id FROM msl_payload)") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V180_RecipientNicknameMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V180_RecipientNicknameMigration.kt index 1c8a3fc9b6..4aa1dfd584 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V180_RecipientNicknameMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V180_RecipientNicknameMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Adds support for storing the systemNickname from storage service. */ +@Suppress("ClassName") object V180_RecipientNicknameMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE recipient ADD COLUMN system_nickname TEXT DEFAULT NULL") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V181_ThreadTableForeignKeyCleanup.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V181_ThreadTableForeignKeyCleanup.kt index 88a3731a7c..197603add9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V181_ThreadTableForeignKeyCleanup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V181_ThreadTableForeignKeyCleanup.kt @@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log * We saw evidence (via failed backup restores) that some people have recipients in their thread table that do not exist in the recipient table. * This is likely the result of a bad past migration, since a foreign key is in place. Cleaning it up now. */ +@Suppress("ClassName") object V181_ThreadTableForeignKeyCleanup : SignalDatabaseMigration { val TAG = Log.tag(V181_ThreadTableForeignKeyCleanup::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V182_CallTableMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V182_CallTableMigration.kt index c8dba43e9c..7033d66cec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V182_CallTableMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V182_CallTableMigration.kt @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.RecipientTable * Removes the 'NOT NULL' condition on message_id and peer, as with ad-hoc calling in place, these * can now be null. */ +@Suppress("ClassName") object V182_CallTableMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V183_CallLinkTableMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V183_CallLinkTableMigration.kt index 09d9db7013..2fc303bc64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V183_CallLinkTableMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V183_CallLinkTableMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Adds the CallLinkTable and modifies the CallTable to include an FK into it. */ +@Suppress("ClassName") object V183_CallLinkTableMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("CREATE TABLE call_link (_id INTEGER PRIMARY KEY)") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V184_CallLinkReplaceIndexMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V184_CallLinkReplaceIndexMigration.kt index 74caef6a6b..d1cafce082 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V184_CallLinkReplaceIndexMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V184_CallLinkReplaceIndexMigration.kt @@ -6,6 +6,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * [V183_CallLinkTableMigration] accidentally setup a unique constraint incorrectly and missed an index. This fixes it. */ +@Suppress("ClassName") object V184_CallLinkReplaceIndexMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsAndEditMessageMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsAndEditMessageMigration.kt index 542dc095ad..80bf67c83a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsAndEditMessageMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V185_MessageRecipientsAndEditMessageMigration.kt @@ -30,6 +30,7 @@ import org.whispersystems.signalservice.api.push.ACI * Changes needed for edit message. New foreign keys require recreating the table. * */ +@Suppress("ClassName") object V185_MessageRecipientsAndEditMessageMigration : SignalDatabaseMigration { private val TAG = Log.tag(V185_MessageRecipientsAndEditMessageMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V186_ForeignKeyIndicesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V186_ForeignKeyIndicesMigration.kt index fad3c0fcd9..bcdc570c62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V186_ForeignKeyIndicesMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V186_ForeignKeyIndicesMigration.kt @@ -19,6 +19,7 @@ import org.signal.core.util.requireNonNullString * * While I was at it, I looked at other columns that would need indices as well. */ +@Suppress("ClassName") object V186_ForeignKeyIndicesMigration : SignalDatabaseMigration { private val TAG = Log.tag(V186_ForeignKeyIndicesMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V187_MoreForeignKeyIndexesMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V187_MoreForeignKeyIndexesMigration.kt index 1c2dc2e66f..fbaadc2f57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V187_MoreForeignKeyIndexesMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V187_MoreForeignKeyIndexesMigration.kt @@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log /** * I found some other tables that didn't have the proper indexes setup to correspond with their foreign keys. */ +@Suppress("ClassName") object V187_MoreForeignKeyIndexesMigration : SignalDatabaseMigration { private val TAG = Log.tag(V187_MoreForeignKeyIndexesMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V188_FixMessageRecipientsAndEditMessageMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V188_FixMessageRecipientsAndEditMessageMigration.kt index 161ba591fe..c3208bf6e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V188_FixMessageRecipientsAndEditMessageMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V188_FixMessageRecipientsAndEditMessageMigration.kt @@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.push.ACI * the concept of a self. To do that, we're going to create a placeholder for self with a special ID (-2), and then * we're going to replace that ID with the true self after it's been created. */ +@Suppress("ClassName") object V188_FixMessageRecipientsAndEditMessageMigration : SignalDatabaseMigration { private val TAG = Log.tag(V188_FixMessageRecipientsAndEditMessageMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V189_CreateCallLinkTableColumnsAndRebuildFKReference.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V189_CreateCallLinkTableColumnsAndRebuildFKReference.kt index adb234090c..122fc2325e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V189_CreateCallLinkTableColumnsAndRebuildFKReference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V189_CreateCallLinkTableColumnsAndRebuildFKReference.kt @@ -12,6 +12,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase * Fleshes out the call link table and rebuilds the call event table. * At this point, there should be no records in the call link database. */ +@Suppress("ClassName") object V189_CreateCallLinkTableColumnsAndRebuildFKReference : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V191_UniqueMessageMigrationV2.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V191_UniqueMessageMigrationV2.kt index 6ce1951e39..e1f107ca1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V191_UniqueMessageMigrationV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V191_UniqueMessageMigrationV2.kt @@ -15,6 +15,7 @@ import org.signal.core.util.requireLong * * This migration safely removes those dupes, and then adds the desired unique constraint. */ +@Suppress("ClassName") object V191_UniqueMessageMigrationV2 : SignalDatabaseMigration { private val TAG = Log.tag(V191_UniqueMessageMigrationV2::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V192_CallLinkTableNullableRootKeys.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V192_CallLinkTableNullableRootKeys.kt index 360c6f954a..31c10b3518 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V192_CallLinkTableNullableRootKeys.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V192_CallLinkTableNullableRootKeys.kt @@ -13,6 +13,7 @@ import org.signal.core.util.logging.Log /** * Allow ROOT_KEY in CallLinkTable to be null. */ +@Suppress("ClassName") object V192_CallLinkTableNullableRootKeys : SignalDatabaseMigration { private val TAG = Log.tag(V192_CallLinkTableNullableRootKeys::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V193_BackCallLinksWithRecipient.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V193_BackCallLinksWithRecipient.kt index bec7522f88..5d6ff79ba0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V193_BackCallLinksWithRecipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V193_BackCallLinksWithRecipient.kt @@ -11,6 +11,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Due to a bug, this has been replaced by [V196_BackCallLinksWithRecipientV2] */ +@Suppress("ClassName") object V193_BackCallLinksWithRecipient : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V194_KyberPreKeyMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V194_KyberPreKeyMigration.kt index e9d5067b3b..7894774fb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V194_KyberPreKeyMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V194_KyberPreKeyMigration.kt @@ -11,6 +11,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Introduces [org.thoughtcrime.securesms.database.KyberPreKeyTable]. */ +@Suppress("ClassName") object V194_KyberPreKeyMigration : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V195_GroupMemberForeignKeyMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V195_GroupMemberForeignKeyMigration.kt index cf36f287f5..036836e097 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V195_GroupMemberForeignKeyMigration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V195_GroupMemberForeignKeyMigration.kt @@ -17,6 +17,7 @@ import org.signal.core.util.requireLong * Back CallLinks with a Recipient to ease integration and ensure we can support * different features which would require that relation in the future. */ +@Suppress("ClassName") object V195_GroupMemberForeignKeyMigration : SignalDatabaseMigration { private val TAG = Log.tag(V195_GroupMemberForeignKeyMigration::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V196_BackCallLinksWithRecipientV2.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V196_BackCallLinksWithRecipientV2.kt index e497275196..52aa1d2888 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V196_BackCallLinksWithRecipientV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V196_BackCallLinksWithRecipientV2.kt @@ -13,6 +13,7 @@ import org.signal.core.util.logging.Log /** * Cleans up the call events table and restricts peer to non-null. */ +@Suppress("ClassName") object V196_BackCallLinksWithRecipientV2 : SignalDatabaseMigration { private val TAG = Log.tag(V196_BackCallLinksWithRecipientV2::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V197_DropAvatarColorFromCallLinks.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V197_DropAvatarColorFromCallLinks.kt index c0b8306041..72d7bf39aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V197_DropAvatarColorFromCallLinks.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V197_DropAvatarColorFromCallLinks.kt @@ -11,6 +11,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase /** * Because getting the color is a simple modulo operation, there is no need to store it in the database. */ +@Suppress("ClassName") object V197_DropAvatarColorFromCallLinks : SignalDatabaseMigration { override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("ALTER TABLE call_link DROP COLUMN avatar_color") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt new file mode 100644 index 0000000000..ff9de352d7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V198_AddMacDigestColumn.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 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 + +/** + * New field migration. + */ +@Suppress("ClassName") +object V198_AddMacDigestColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE part ADD COLUMN incremental_mac_digest BLOB") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchForegroundService.kt b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchForegroundService.kt index 88613b200f..f611abf939 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchForegroundService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchForegroundService.kt @@ -5,6 +5,7 @@ import android.app.Service import android.content.Context import android.content.Intent import android.os.IBinder +import android.os.PowerManager import androidx.core.app.NotificationCompat import org.signal.core.util.PendingIntentFlags import org.signal.core.util.logging.Log @@ -12,16 +13,23 @@ import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds +import org.thoughtcrime.securesms.util.WakeLockUtil /** * Works with {@link FcmFetchManager} to exists as a service that will keep the app process running in the foreground while we fetch messages. */ class FcmFetchForegroundService : Service() { + private var wakeLock: PowerManager.WakeLock? = null + companion object { private val TAG = Log.tag(FcmFetchForegroundService::class.java) + + private const val WAKELOCK_TAG = "FcmForegroundService" private const val KEY_STOP_SELF = "stop_self" + private val WAKELOCK_TIMEOUT = FcmFetchManager.WEBSOCKET_DRAIN_TIMEOUT + /** * Android's requirement for calling [startForeground] is enforced _even if your service was stopped before it started_. * That means we can't just stop it normally, since we don't know if it got to start yet. @@ -44,10 +52,14 @@ class FcmFetchForegroundService : Service() { postForegroundNotification() return if (intent != null && intent.getBooleanExtra(KEY_STOP_SELF, false)) { + WakeLockUtil.release(wakeLock, WAKELOCK_TAG) stopForeground(true) stopSelf() START_NOT_STICKY } else { + if (wakeLock == null) { + wakeLock = WakeLockUtil.acquire(this, PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TIMEOUT, WAKELOCK_TAG) + } START_STICKY } } @@ -68,7 +80,10 @@ class FcmFetchForegroundService : Service() { override fun onDestroy() { Log.i(TAG, "onDestroy()") + WakeLockUtil.release(wakeLock, WAKELOCK_TAG) FcmFetchManager.onDestroyForegroundFetchService() + + wakeLock = null } override fun onBind(intent: Intent?): IBinder? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt index d1181fd66c..af00babbb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt @@ -3,13 +3,16 @@ package org.thoughtcrime.securesms.gcm import android.content.Context import android.content.Intent import android.os.Build +import android.os.PowerManager import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob import org.thoughtcrime.securesms.messages.WebSocketStrategy +import org.thoughtcrime.securesms.util.SignalLocalMetrics import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor +import kotlin.time.Duration.Companion.minutes /** * Our goals with FCM processing are as follows: @@ -33,6 +36,8 @@ object FcmFetchManager { private const val MAX_BLOCKING_TIME_MS = 500L private val EXECUTOR = SerialMonoLifoExecutor(SignalExecutors.UNBOUNDED) + val WEBSOCKET_DRAIN_TIMEOUT = 5.minutes.inWholeMilliseconds + @Volatile private var activeCount = 0 @@ -45,6 +50,8 @@ object FcmFetchManager { @Volatile private var startForegroundOnDestroy = false + private var wakeLock: PowerManager.WakeLock? = null + /** * @return True if a service was successfully started, otherwise false. */ @@ -88,7 +95,13 @@ object FcmFetchManager { } private fun fetch(context: Context) { - retrieveMessages(context) + val metricId = SignalLocalMetrics.PushWebsocketFetch.startFetch() + val success = retrieveMessages(context) + if (!success) { + SignalLocalMetrics.PushWebsocketFetch.onTimedOut(metricId) + } else { + SignalLocalMetrics.PushWebsocketFetch.onDrained(metricId) + } synchronized(this) { activeCount-- @@ -132,8 +145,8 @@ object FcmFetchManager { } @JvmStatic - fun retrieveMessages(context: Context) { - val success = ApplicationDependencies.getBackgroundMessageRetriever().retrieveMessages(context, WebSocketStrategy()) + fun retrieveMessages(context: Context): Boolean { + val success = ApplicationDependencies.getBackgroundMessageRetriever().retrieveMessages(context, WebSocketStrategy(WEBSOCKET_DRAIN_TIMEOUT)) if (success) { Log.i(TAG, "Successfully retrieved messages.") @@ -146,6 +159,8 @@ object FcmFetchManager { ApplicationDependencies.getJobManager().add(PushNotificationReceiveJob()) } } + + return success } fun onDestroyForegroundFetchService() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt index 5f78d1e2a2..bd4f901856 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt @@ -12,8 +12,8 @@ import kotlin.math.min * Decoration that will make the video display params update on each recycler redraw. */ class GiphyMp4ItemDecoration( - val callback: GiphyMp4PlaybackController.Callback, - val onRecyclerVerticalTranslationSet: (Float) -> Unit + private val callback: GiphyMp4PlaybackController.Callback, + private val onRecyclerVerticalTranslationSet: ((Float) -> Unit)? = null ) : RecyclerView.ItemDecoration() { override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { setParentRecyclerTranslationY(parent) @@ -26,7 +26,7 @@ class GiphyMp4ItemDecoration( private fun setParentRecyclerTranslationY(parent: RecyclerView) { if (parent.childCount == 0 || parent.canScrollVertically(-1) || parent.canScrollVertically(1)) { parent.translationY = 0f - onRecyclerVerticalTranslationSet(parent.translationY) + onRecyclerVerticalTranslationSet?.invoke(parent.translationY) } else { val threadHeaderViewHolder = parent.children .map { parent.getChildViewHolder(it) } @@ -35,7 +35,7 @@ class GiphyMp4ItemDecoration( if (threadHeaderViewHolder == null) { parent.translationY = 0f - onRecyclerVerticalTranslationSet(parent.translationY) + onRecyclerVerticalTranslationSet?.invoke(parent.translationY) return } @@ -51,7 +51,7 @@ class GiphyMp4ItemDecoration( val childTop: Int = threadHeaderViewHolder.itemView.top - toolbarMargin parent.translationY = min(0, -childTop).toFloat() - onRecyclerVerticalTranslationSet(parent.translationY) + onRecyclerVerticalTranslationSet?.invoke(parent.translationY) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java index 37e25c19b0..0478e27d2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java @@ -100,6 +100,7 @@ private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) { .placeholder(placeholder) .diskCacheStrategy(DiskCacheStrategy.ALL) .transition(DrawableTransitionOptions.withCrossFade()) + .centerCrop() .into(stillImage); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java index 0b69f9017d..b993f04d69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java @@ -137,7 +137,7 @@ public static void migrate(@NonNull Context context, @NonNull RecipientId recipi public static void performLocalMigration(@NonNull Context context, @NonNull GroupId.V1 gv1Id) throws IOException { Log.i(TAG, "Beginning local migration! V1 ID: " + gv1Id, new Throwable()); - try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock()) { + try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock(1000)) { if (SignalDatabase.groups().groupExists(gv1Id.deriveV2MigrationGroupId())) { Log.w(TAG, "Group was already migrated! Could have been waiting for the lock.", new Throwable()); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2ProcessingLock.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2ProcessingLock.java index 8987d02dfc..b7dc727e22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2ProcessingLock.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2ProcessingLock.java @@ -4,6 +4,9 @@ import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.ReentrantSessionLock; +import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.util.FeatureFlags; import java.io.Closeable; import java.util.concurrent.TimeUnit; @@ -17,15 +20,25 @@ public final class GroupsV2ProcessingLock { private GroupsV2ProcessingLock() { } - private static final Lock lock = new ReentrantLock(); + private static final ReentrantLock lock = new ReentrantLock(); @WorkerThread public static Closeable acquireGroupProcessingLock() throws GroupChangeBusyException { + if (FeatureFlags.internalUser()) { + if (!lock.isHeldByCurrentThread()) { + if (SignalDatabase.inTransaction()) { + throw new AssertionError("Tried to acquire the group lock inside of a database transaction!"); + } + if (ReentrantSessionLock.INSTANCE.isHeldByCurrentThread()) { + throw new AssertionError("Tried to acquire the group lock inside of the ReentrantSessionLock!!"); + } + } + } return acquireGroupProcessingLock(5000); } @WorkerThread - static Closeable acquireGroupProcessingLock(long timeoutMs) throws GroupChangeBusyException { + public static Closeable acquireGroupProcessingLock(long timeoutMs) throws GroupChangeBusyException { ThreadUtil.assertNotMainThread(); try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index b53272f5e8..11e2009b08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -247,6 +247,7 @@ private SignalServiceAttachmentPointer createAttachmentPointer(Attachment attach Optional.empty(), 0, 0, Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.getIncrementalDigest()), Optional.ofNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java index a4f6fba533..359f9fea48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java @@ -86,7 +86,7 @@ public void onRun() throws IOException { attachment.deleteOnExit(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis()); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, Optional.empty(), fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis()); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundServiceUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundServiceUtil.kt index 7c5445bd99..f9cadd24ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundServiceUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ForegroundServiceUtil.kt @@ -102,6 +102,22 @@ object ForegroundServiceUtil { } } + /** + * Identical to [startWhenCapable], but we swallow the error. + * + * @param timeout The maximum time you're willing to wait to create the conditions for a foreground service to start. + */ + @JvmOverloads + @JvmStatic + @WorkerThread + fun tryToStartWhenCapable(context: Context, intent: Intent, timeout: Long = DEFAULT_TIMEOUT) { + return try { + startWhenCapable(context, intent, timeout) + } catch (e: UnableToStartException) { + Log.w(TAG, "Failed to start foreground service", e) + } + } + /** * Does its best to start a foreground service with your task name, including possibly blocking and waiting until we are able to. * However, it is always possible that the attempt will fail, so always handle the [UnableToStartException]. 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 a8b0ebb8d9..4a28df02be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.java @@ -153,7 +153,7 @@ public void onPushSend() } try { - log(TAG, String.valueOf(message.getSentTimeMillis()), "Sending message: " + messageId + ", Recipient: " + message.getThreadRecipient().getId() + ", Thread: " + threadId + ", Attachments: " + buildAttachmentString(message.getAttachments())); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Sending message: " + messageId + ", Recipient: " + message.getThreadRecipient().getId() + ", Thread: " + threadId + ", Attachments: " + buildAttachmentString(message.getAttachments()) + ", Editing: " + (originalEditedMessage != null ? originalEditedMessage.getDateSent() : "N/A")); RecipientUtil.shareProfileIfFirstSecureMessage(message.getThreadRecipient()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt index 67a8493d3d..9eeb39351e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt @@ -150,7 +150,13 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base } } - VerifyResponse.from(distributionResponse, null, null) + VerifyResponse.from( + response = distributionResponse, + kbsData = null, + pin = null, + aciPreKeyCollection = null, + pniPreKeyCollection = null + ) }.subscribeOn(Schedulers.single()) .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } } @@ -188,7 +194,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) { pniProtocolStore.loadKyberPreKey(SignalStore.account().pniPreKeys.lastResortKyberPreKeyId) } else { - PreKeyUtil.generateKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) + PreKeyUtil.generateLastRestortKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) } devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt index da32c202da..777681c85b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt @@ -204,7 +204,7 @@ class PreKeysSyncJob private constructor(parameters: Parameters) : BaseJob(param log(serviceIdType, "Rotating last-resort kyber prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") PreKeyUtil.generateAndStoreLastResortKyberPreKey(protocolStore, metadataStore) } else { - log(serviceIdType, "No need to rotate signed prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") + log(serviceIdType, "No need to rotate last-resort kyber prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") null } } 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 a7cc883ab4..e692551ff9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -231,6 +231,7 @@ public void onPushSend() List results = deliver(message, originalEditedMessage, groupRecipient, target); processGroupMessageResults(context, messageId, threadId, groupRecipient, message, results, target, skipped, existingNetworkFailures, existingIdentityMismatches); + ConversationShortcutRankingUpdateJob.enqueueForOutgoingIfNecessary(groupRecipient); Log.i(TAG, JobLogger.format(this, "Finished send.")); } catch (UntrustedIdentityException | UndeliverableMessageException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 27c08e26dd..042abd8a05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -283,6 +283,7 @@ protected static Set enqueueCompressingAndUploadAttachmentsChains(@NonNu width, height, Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.getIncrementalDigest()), Optional.ofNullable(attachment.getFileName()), attachment.isVoiceNote(), attachment.isBorderless(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt index 6ea3850ecd..d05ffe9587 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.InputAwareConstraintLayout import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment @@ -20,7 +21,7 @@ import org.thoughtcrime.securesms.util.fragments.findListener import org.thoughtcrime.securesms.util.visible import kotlin.reflect.KClass -class KeyboardPagerFragment : Fragment() { +class KeyboardPagerFragment : Fragment(), InputAwareConstraintLayout.InputFragment { private lateinit var emojiButton: View private lateinit var stickerButton: View @@ -113,7 +114,8 @@ class KeyboardPagerFragment : Fragment() { transaction.commitAllowingStateLoss() } - fun show() { + override fun show() { + findListener()?.onShown() if (isAdded && view != null) { onHiddenChanged(false) @@ -121,7 +123,8 @@ class KeyboardPagerFragment : Fragment() { } } - fun hide() { + override fun hide() { + findListener()?.onHidden() if (isAdded && view != null) { onHiddenChanged(true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt index cb52c0ab0b..711d008efe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt @@ -4,7 +4,6 @@ import androidx.annotation.MainThread import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import org.signal.core.util.ThreadUtil -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.stickers.StickerSearchRepository import org.thoughtcrime.securesms.util.DefaultValueLiveData @@ -22,7 +21,7 @@ class KeyboardPagerViewModel : ViewModel() { pages = DefaultValueLiveData(startingPages) page = DefaultValueLiveData(startingPages.first()) - StickerSearchRepository(ApplicationDependencies.getApplication()).getStickerFeatureAvailability { available -> + StickerSearchRepository().getStickerFeatureAvailability { available -> if (!available) { val updatedPages = pages.value.toMutableSet().apply { remove(KeyboardPage.STICKER) } pages.postValue(updatedPages) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt index 3f0734669f..25a69a0f13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt @@ -84,6 +84,7 @@ class EmojiSearchFragment : Fragment(), EmojiPageViewGridAdapter.VariationSelect private inner class SearchCallbacks : KeyboardPageSearchView.Callbacks { override fun onNavigationClicked() { ViewUtil.hideKeyboard(requireContext(), requireView()) + callback.closeEmojiSearch() } override fun onQueryChanged(query: String) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 9a2c9e8a55..815a7085db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -12,16 +12,19 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.signal.core.util.Hex; +import org.signal.core.util.Result; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; +import org.signal.ringrtc.CallLinkRootKey; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.calls.links.CallLinks; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.GroupRecord; @@ -42,6 +45,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials; +import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult; import org.thoughtcrime.securesms.stickers.StickerRemoteUri; import org.thoughtcrime.securesms.stickers.StickerUrl; import org.thoughtcrime.securesms.util.AvatarUtil; @@ -63,6 +68,9 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; import okhttp3.CacheControl; import okhttp3.Call; import okhttp3.OkHttpClient; @@ -90,6 +98,28 @@ public LinkPreviewRepository() { .build(); } + public @NonNull Single> getLinkPreview(@NonNull String url) { + return Single.>create(emitter -> { + RequestController controller = getLinkPreview(ApplicationDependencies.getApplication(), + url, + new Callback() { + @Override + public void onSuccess(@NonNull LinkPreview linkPreview) { + emitter.onSuccess(Result.success(linkPreview)); + } + + @Override + public void onError(@NonNull Error error) { + emitter.onSuccess(Result.failure(error)); + } + }); + + if (controller != null) { + emitter.setCancellable(controller::cancel); + } + }).subscribeOn(Schedulers.io()); + } + @Nullable RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback callback) @@ -112,6 +142,8 @@ public LinkPreviewRepository() { metadataController = fetchStickerPackLinkPreview(context, url, callback); } else if (GroupInviteLinkUrl.isGroupLink(url)) { metadataController = fetchGroupLinkPreview(context, url, callback); + } else if (CallLinks.isCallLink(url)) { + metadataController = fetchCallLinkPreview(context, url, callback); } else { metadataController = fetchMetadata(url, metadata -> { if (metadata.isEmpty()) { @@ -274,6 +306,55 @@ private static RequestController fetchStickerPackLinkPreview(@NonNull Context co return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect."); } + private static RequestController fetchCallLinkPreview(@NonNull Context context, + @NonNull String callLinkUrl, + @NonNull Callback callback) { + + CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(callLinkUrl); + if (callLinkRootKey == null) { + callback.onError(Error.PREVIEW_NOT_AVAILABLE); + return () -> { }; + } + + Disposable disposable = ApplicationDependencies.getSignalCallManager() + .getCallLinkManager() + .readCallLink(new CallLinkCredentials(callLinkRootKey.getKeyBytes(), null)) + .observeOn(Schedulers.io()) + .subscribe( + result -> { + if (result instanceof ReadCallLinkResult.Success) { + ReadCallLinkResult.Success success = (ReadCallLinkResult.Success) result; + Log.i(TAG, "Successfully read call link."); + + if (((ReadCallLinkResult.Success) result).getCallLinkState().hasBeenRevoked()) { + Log.i(TAG, "Call link has been revoked."); + callback.onError(Error.PREVIEW_NOT_AVAILABLE); + return; + } + + // Note: thumbnails are generated recv-side using the CallLinkRootKey + callback.onSuccess(new LinkPreview( + callLinkUrl, + success.getCallLinkState().getName(), + "", + 0, + Optional.empty() + )); + } else { + ReadCallLinkResult.Failure failure = (ReadCallLinkResult.Failure) result; + Log.w(TAG, "Failed to read call link: " + failure); + callback.onError(Error.PREVIEW_NOT_AVAILABLE); + } + }, + error -> { + Log.w(TAG, "An error occurred: ", error); + callback.onError(Error.PREVIEW_NOT_AVAILABLE); + } + ); + + return disposable::dispose; + } + private static RequestController fetchGroupLinkPreview(@NonNull Context context, @NonNull String groupUrl, @NonNull Callback callback) diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewState.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewState.java new file mode 100644 index 0000000000..86f7bd0feb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewState.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.linkpreview; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Optional; + +public class LinkPreviewState { + private final String activeUrlForError; + private final boolean isLoading; + private final boolean hasLinks; + private final Optional linkPreview; + private final LinkPreviewRepository.Error error; + + private LinkPreviewState(@Nullable String activeUrlForError, + boolean isLoading, + boolean hasLinks, + Optional linkPreview, + @Nullable LinkPreviewRepository.Error error) + { + this.activeUrlForError = activeUrlForError; + this.isLoading = isLoading; + this.hasLinks = hasLinks; + this.linkPreview = linkPreview; + this.error = error; + } + + public static LinkPreviewState forLoading() { + return new LinkPreviewState(null, true, false, Optional.empty(), null); + } + + public static LinkPreviewState forPreview(@NonNull LinkPreview linkPreview) { + return new LinkPreviewState(null, false, true, Optional.of(linkPreview), null); + } + + public static LinkPreviewState forLinksWithNoPreview(@Nullable String activeUrlForError, @NonNull LinkPreviewRepository.Error error) { + return new LinkPreviewState(activeUrlForError, false, true, Optional.empty(), error); + } + + public static LinkPreviewState forNoLinks() { + return new LinkPreviewState(null, false, false, Optional.empty(), null); + } + + public @Nullable String getActiveUrlForError() { + return activeUrlForError; + } + + public boolean isLoading() { + return isLoading; + } + + public boolean hasLinks() { + return hasLinks; + } + + public Optional getLinkPreview() { + return linkPreview; + } + + public @Nullable LinkPreviewRepository.Error getError() { + return error; + } + + public boolean hasContent() { + return isLoading || hasLinks; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index 9b710102c5..4b8d3c84c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -255,7 +255,7 @@ public void onError(@NonNull LinkPreviewRepository.Error error) { } if (enablePlaceholder) { - return state.linkPreview + return state.getLinkPreview() .map(linkPreview -> LinkPreviewState.forLinksWithNoPreview(linkPreview.getUrl(), LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE)) .orElse(state); } @@ -263,67 +263,6 @@ public void onError(@NonNull LinkPreviewRepository.Error error) { return LinkPreviewState.forNoLinks(); } - public static class LinkPreviewState { - private final String activeUrlForError; - private final boolean isLoading; - private final boolean hasLinks; - private final Optional linkPreview; - private final LinkPreviewRepository.Error error; - - private LinkPreviewState(@Nullable String activeUrlForError, - boolean isLoading, - boolean hasLinks, - Optional linkPreview, - @Nullable LinkPreviewRepository.Error error) - { - this.activeUrlForError = activeUrlForError; - this.isLoading = isLoading; - this.hasLinks = hasLinks; - this.linkPreview = linkPreview; - this.error = error; - } - - private static LinkPreviewState forLoading() { - return new LinkPreviewState(null, true, false, Optional.empty(), null); - } - - private static LinkPreviewState forPreview(@NonNull LinkPreview linkPreview) { - return new LinkPreviewState(null, false, true, Optional.of(linkPreview), null); - } - - private static LinkPreviewState forLinksWithNoPreview(@Nullable String activeUrlForError, @NonNull LinkPreviewRepository.Error error) { - return new LinkPreviewState(activeUrlForError, false, true, Optional.empty(), error); - } - - private static LinkPreviewState forNoLinks() { - return new LinkPreviewState(null, false, false, Optional.empty(), null); - } - - public @Nullable String getActiveUrlForError() { - return activeUrlForError; - } - - public boolean isLoading() { - return isLoading; - } - - public boolean hasLinks() { - return hasLinks; - } - - public Optional getLinkPreview() { - return linkPreview; - } - - public @Nullable LinkPreviewRepository.Error getError() { - return error; - } - - boolean hasContent() { - return isLoading || hasLinks; - } - } - public static class Factory extends ViewModelProvider.NewInstanceFactory { private final LinkPreviewRepository repository; diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModelV2.kt b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModelV2.kt new file mode 100644 index 0000000000..13fca726a4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModelV2.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.linkpreview + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.kotlin.subscribeBy +import org.signal.core.util.Result +import org.signal.core.util.isAbsent +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.Debouncer +import org.thoughtcrime.securesms.util.rx.RxStore +import java.util.Optional + +/** + * Rewrite of [LinkPreviewViewModel] preferring Rx and Kotlin + */ +class LinkPreviewViewModelV2( + private val linkPreviewRepository: LinkPreviewRepository = LinkPreviewRepository(), + private val enablePlaceholder: Boolean +) : ViewModel() { + private var enabled = SignalStore.settings().isLinkPreviewsEnabled + private val linkPreviewStateStore = RxStore(LinkPreviewState.forNoLinks()) + + val linkPreviewState: Flowable = linkPreviewStateStore.stateFlowable + val hasLinkPreview: Boolean = linkPreviewStateStore.state.linkPreview.isPresent + val hasLinkPreviewUi: Boolean = linkPreviewStateStore.state.hasContent() + + private var activeUrl: String? = null + private var activeRequest: Disposable = Disposable.disposed() + private var userCancelled: Boolean = false + private val debouncer: Debouncer = Debouncer(250) + + override fun onCleared() { + activeRequest.dispose() + debouncer.clear() + } + + fun onSend(): List { + val currentState = linkPreviewStateStore.state + + onUserCancel() + + return currentState.linkPreview.map { listOf(it) }.orElse(emptyList()) + } + + fun onTextChanged(text: String, cursorStart: Int, cursorEnd: Int) { + if (!enabled && !enablePlaceholder) { + return + } + + debouncer.publish { + if (text.isEmpty()) { + userCancelled = false + } + + if (userCancelled) { + return@publish + } + + val link: Optional = LinkPreviewUtil.findValidPreviewUrls(text).findFirst() + + activeRequest.dispose() + + if (link.isAbsent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) { + activeUrl = null + setLinkPreviewState(LinkPreviewState.forNoLinks()) + return@publish + } + + setLinkPreviewState(LinkPreviewState.forLoading()) + + val activeUrl = link.get().url + this.activeUrl = activeUrl + activeRequest = if (enabled) { + performRequest(activeUrl) + } else { + createPlaceholder(activeUrl) + } + } + } + + fun onEnabled() { + userCancelled = false + enabled = SignalStore.settings().isLinkPreviewsEnabled + } + + fun onUserCancel() { + activeRequest.dispose() + userCancelled = true + activeUrl = null + debouncer.clear() + setLinkPreviewState(LinkPreviewState.forNoLinks()) + } + + private fun isCursorPositionValid(text: String, link: Link, cursorStart: Int, cursorEnd: Int): Boolean { + if (cursorStart != cursorEnd) { + return true + } + + if (text.endsWith(link.url) && cursorStart == link.position + link.url.length) { + return true + } + + return cursorStart < link.position || cursorStart > link.position + link.url.length + } + + private fun createPlaceholder(url: String): Disposable { + return Single.just(url) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { + if (!userCancelled) { + if (activeUrl != null && activeUrl == url) { + setLinkPreviewState(LinkPreviewState.forLinksWithNoPreview(url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE)) + } else { + setLinkPreviewState(LinkPreviewState.forNoLinks()) + } + } + } + } + + private fun performRequest(url: String): Disposable { + return linkPreviewRepository.getLinkPreview(url) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { result -> + if (!userCancelled) { + val linkPreviewState = when (result) { + is Result.Success -> if (activeUrl == result.success.url) LinkPreviewState.forPreview(result.success) else LinkPreviewState.forNoLinks() + is Result.Failure -> if (activeUrl != null) LinkPreviewState.forLinksWithNoPreview(activeUrl, result.failure) else LinkPreviewState.forNoLinks() + } + + setLinkPreviewState(linkPreviewState) + } + } + } + + private fun setLinkPreviewState(linkPreviewState: LinkPreviewState) { + linkPreviewStateStore.update { cleanseState(linkPreviewState) } + } + + private fun cleanseState(linkPreviewState: LinkPreviewState): LinkPreviewState { + if (enabled) { + return linkPreviewState + } + + if (enablePlaceholder) { + return linkPreviewState + .linkPreview + .map { LinkPreviewState.forLinksWithNoPreview(it.url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE) } + .orElse(linkPreviewState) + } + + return LinkPreviewState.forNoLinks() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java index b3e2585194..0867b5668c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java @@ -14,12 +14,15 @@ import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import com.google.android.material.button.MaterialButton; + import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -36,7 +39,7 @@ public abstract class BaseKbsPinFragment private LearnMoreTextView description; private EditText input; private TextView label; - private TextView keyboardToggle; + private MaterialButton keyboardToggle; private CircularProgressMaterialButton confirm; private ViewModel viewModel; @@ -69,6 +72,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat viewModel.getKeyboard().observe(getViewLifecycleOwner(), keyboardType -> { updateKeyboard(keyboardType); keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + keyboardToggle.setIconResource(keyboardType.getOther().getIconResource()); }); description.setOnLinkClickListener(v -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/PinKeyboardType.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/PinKeyboardType.java index a2f5dd79a9..9a1a7cad28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/PinKeyboardType.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/PinKeyboardType.java @@ -2,6 +2,8 @@ import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.R; + public enum PinKeyboardType { NUMERIC("numeric"), ALPHA_NUMERIC("alphaNumeric"); @@ -30,4 +32,9 @@ public static PinKeyboardType fromCode(@Nullable String code) { return NUMERIC; } + + public int getIconResource() { + if (this == ALPHA_NUMERIC) return R.drawable.ic_keyboard_24; + else return R.drawable.ic_number_pad_conversation_filter_24; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java index 13eb28a94f..c242db4182 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; +import android.provider.Settings; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.WindowManager; @@ -51,43 +52,44 @@ public class LogSectionSystemInfo implements LogSection { boolean locked = KeyCachingService.isLocked(); - builder.append("Time : ").append(System.currentTimeMillis()).append('\n'); - builder.append("Manufacturer : ").append(Build.MANUFACTURER).append("\n"); - builder.append("Model : ").append(Build.MODEL).append("\n"); - builder.append("Product : ").append(Build.PRODUCT).append("\n"); - builder.append("Screen : ").append(getScreenResolution(context)).append(", ") + builder.append("Time : ").append(System.currentTimeMillis()).append('\n'); + builder.append("Manufacturer : ").append(Build.MANUFACTURER).append("\n"); + builder.append("Model : ").append(Build.MODEL).append("\n"); + builder.append("Product : ").append(Build.PRODUCT).append("\n"); + builder.append("Screen : ").append(getScreenResolution(context)).append(", ") .append(ScreenDensity.get(context)).append(", ") .append(getScreenRefreshRate(context)).append("\n"); - builder.append("Font Scale : ").append(context.getResources().getConfiguration().fontScale).append("\n"); - builder.append("Animation Scale: ").append(ContextUtil.getAnimationScale(context)).append("\n"); - builder.append("Android : ").append(Build.VERSION.RELEASE).append(", API ") + builder.append("Font Scale : ").append(context.getResources().getConfiguration().fontScale).append("\n"); + builder.append("Animation Scale : ").append(ContextUtil.getAnimationScale(context)).append("\n"); + builder.append("Android : ").append(Build.VERSION.RELEASE).append(", API ") .append(Build.VERSION.SDK_INT).append(" (") .append(Build.VERSION.INCREMENTAL).append(", ") .append(Build.DISPLAY).append(")\n"); - builder.append("ABIs : ").append(TextUtils.join(", ", getSupportedAbis())).append("\n"); - builder.append("Memory : ").append(getMemoryUsage()).append("\n"); - builder.append("Memclass : ").append(getMemoryClass(context)).append("\n"); - builder.append("MemInfo : ").append(getMemoryInfo(context)).append("\n"); - builder.append("OS Host : ").append(Build.HOST).append("\n"); - builder.append("RecipientId : ").append(locked ? "Unknown" : SignalStore.registrationValues().isRegistrationComplete() ? Recipient.self().getId() : "N/A").append("\n"); - builder.append("ACI : ").append(locked ? "Unknown" : getCensoredAci(context)).append("\n"); - builder.append("Device ID : ").append(locked ? "Unknown" : SignalStore.account().getDeviceId()).append("\n"); - builder.append("Censored : ").append(locked ? "Unknown" : ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()).append("\n"); - builder.append("Network Status : ").append(NetworkUtil.getNetworkStatus(context)).append("\n"); - builder.append("Data Saver : ").append(DeviceProperties.getDataSaverState(context)).append("\n"); - builder.append("Play Services : ").append(getPlayServicesString(context)).append("\n"); - builder.append("FCM : ").append(locked ? "Unknown" : SignalStore.account().isFcmEnabled()).append("\n"); - builder.append("BkgRestricted : ").append(Build.VERSION.SDK_INT >= 28 ? DeviceProperties.isBackgroundRestricted(context) : "N/A").append("\n"); - builder.append("Locale : ").append(Locale.getDefault()).append("\n"); - builder.append("Linked Devices : ").append(locked ? "Unknown" : TextSecurePreferences.isMultiDevice(context)).append("\n"); - builder.append("First Version : ").append(TextSecurePreferences.getFirstInstallVersion(context)).append("\n"); - builder.append("Days Installed : ").append(VersionTracker.getDaysSinceFirstInstalled(context)).append("\n"); - builder.append("Build Variant : ").append(BuildConfig.FLAVOR).append("\n"); - builder.append("Emoji Version : ").append(locked ? "Unknown" : getEmojiVersionString(context)).append("\n"); - builder.append("RenderBigEmoji : ").append(FontUtil.canRenderEmojiAtFontSize(1024)).append("\n"); - builder.append("Telecom : ").append(locked ? "Unknown" : AndroidTelecomUtil.getTelecomSupported()).append("\n"); - builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n"); - builder.append("App : "); + builder.append("ABIs : ").append(TextUtils.join(", ", getSupportedAbis())).append("\n"); + builder.append("Memory : ").append(getMemoryUsage()).append("\n"); + builder.append("Memclass : ").append(getMemoryClass(context)).append("\n"); + builder.append("MemInfo : ").append(getMemoryInfo(context)).append("\n"); + builder.append("OS Host : ").append(Build.HOST).append("\n"); + builder.append("RecipientId : ").append(locked ? "Unknown" : SignalStore.registrationValues().isRegistrationComplete() ? Recipient.self().getId() : "N/A").append("\n"); + builder.append("ACI : ").append(locked ? "Unknown" : getCensoredAci(context)).append("\n"); + builder.append("Device ID : ").append(locked ? "Unknown" : SignalStore.account().getDeviceId()).append("\n"); + builder.append("Censored : ").append(locked ? "Unknown" : ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()).append("\n"); + builder.append("Network Status : ").append(NetworkUtil.getNetworkStatus(context)).append("\n"); + builder.append("Data Saver : ").append(DeviceProperties.getDataSaverState(context)).append("\n"); + builder.append("Play Services : ").append(getPlayServicesString(context)).append("\n"); + builder.append("FCM : ").append(locked ? "Unknown" : SignalStore.account().isFcmEnabled()).append("\n"); + builder.append("BkgRestricted : ").append(Build.VERSION.SDK_INT >= 28 ? DeviceProperties.isBackgroundRestricted(context) : "N/A").append("\n"); + builder.append("Locale : ").append(Locale.getDefault()).append("\n"); + builder.append("Linked Devices : ").append(locked ? "Unknown" : TextSecurePreferences.isMultiDevice(context)).append("\n"); + builder.append("First Version : ").append(TextSecurePreferences.getFirstInstallVersion(context)).append("\n"); + builder.append("Days Installed : ").append(VersionTracker.getDaysSinceFirstInstalled(context)).append("\n"); + builder.append("Build Variant : ").append(BuildConfig.FLAVOR).append("\n"); + builder.append("Emoji Version : ").append(locked ? "Unknown" : getEmojiVersionString(context)).append("\n"); + builder.append("RenderBigEmoji : ").append(FontUtil.canRenderEmojiAtFontSize(1024)).append("\n"); + builder.append("DontKeepActivities: ").append(getDontKeepActivities(context)).append("\n"); + builder.append("Telecom : ").append(locked ? "Unknown" : AndroidTelecomUtil.getTelecomSupported()).append("\n"); + builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n"); + builder.append("App : "); try { builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0))) .append(" ") @@ -101,7 +103,7 @@ public class LogSectionSystemInfo implements LogSection { } catch (PackageManager.NameNotFoundException nnfe) { builder.append("Unknown\n"); } - builder.append("Package : ").append(BuildConfig.APPLICATION_ID).append(" (").append(getSigningString(context)).append(")"); + builder.append("Package : ").append(BuildConfig.APPLICATION_ID).append(" (").append(getSigningString(context)).append(")"); return builder; } @@ -181,4 +183,9 @@ private static String getCensoredAci(@NonNull Context context) { return "N/A"; } } + + private static String getDontKeepActivities(@NonNull Context context) { + int setting = Settings.Global.getInt(context.getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0); + return setting == 0 ? "false" : "true"; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java index 49266f071f..ccfe025c9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendGifFragment.java @@ -41,7 +41,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat super.onViewCreated(view, savedInstanceState); uri = getArguments().getParcelable(KEY_URI); - GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(uri)).into((ImageView) view); + GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(uri)).fitCenter().into((ImageView) view); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt index d241cad7cd..a4dcd8cac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/AddMessageDialogFragment.kt @@ -167,7 +167,6 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a mentionsViewModel = ViewModelProvider(requireActivity(), MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel::class.java) inlineQueryResultsController = InlineQueryResultsController( - requireContext(), inlineQueryViewModel, requireView().findViewById(R.id.background_holder), (requireView() as ViewGroup), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextAlignment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextAlignment.kt index 1094f7964e..7e7975461b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextAlignment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextAlignment.kt @@ -7,5 +7,5 @@ import org.thoughtcrime.securesms.R enum class TextAlignment(val gravity: Int, @DrawableRes val icon: Int) { START(Gravity.START or Gravity.CENTER_VERTICAL, R.drawable.ic_text_start), CENTER(Gravity.CENTER, R.drawable.ic_text_center), - END(Gravity.END or Gravity.CENTER_VERTICAL, R.drawable.ic_text_end); + END(Gravity.END or Gravity.CENTER_VERTICAL, R.drawable.ic_text_end) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextColorStyle.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextColorStyle.kt index 2370522721..5e2f399c02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextColorStyle.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextColorStyle.kt @@ -17,5 +17,5 @@ enum class TextColorStyle(@DrawableRes val icon: Int) { /** * textColor background with white foreground. */ - INVERT(R.drawable.ic_text_effect); + INVERT(R.drawable.ic_text_effect) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 470826f784..34e219483f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor import org.thoughtcrime.securesms.databinding.StoriesTextPostCreationFragmentBinding import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository +import org.thoughtcrime.securesms.linkpreview.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel -import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.CameraDisplay import org.thoughtcrime.securesms.mediasend.v2.HudCommand import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel @@ -71,7 +71,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati _binding = StoriesTextPostCreationFragmentBinding.bind(view) - binding.storyTextPost.showCloseButton() + binding.storyTextPost.enableCreationMode() lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable += sharedViewModel.hudCommands.subscribe { @@ -137,7 +137,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati binding.send.isClickable = false binding.sendInProgressIndicator.visible = true - binding.storyTextPost.hideCloseButton() + binding.storyTextPost.disableCreationMode() val contacts = (sharedViewModel.destination.getRecipientSearchKeyList() + sharedViewModel.destination.getRecipientSearchKey()) .filterIsInstance(ContactSearchKey::class.java) @@ -174,7 +174,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati override fun onResume() { super.onResume() - binding.storyTextPost.showCloseButton() + binding.storyTextPost.enableCreationMode() requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt index 8c139f4567..06112a9880 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageObserver.kt @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.AppForegroundObserver +import org.thoughtcrime.securesms.util.SignalLocalMetrics import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState @@ -86,9 +87,9 @@ class IncomingMessageObserver(private val context: Application) { private val lock: ReentrantLock = ReentrantLock() private val connectionNecessarySemaphore = Semaphore(0) - private val networkConnectionListener = NetworkConnectionListener(context) { isNetworkAvailable -> + private val networkConnectionListener = NetworkConnectionListener(context) { isNetworkUnavailable -> lock.withLock { - if (isNetworkAvailable()) { + if (isNetworkUnavailable()) { Log.w(TAG, "Lost network connection. Shutting down our websocket connections and resetting the drained state.") decryptionDrained = false disconnect() @@ -421,8 +422,8 @@ class IncomingMessageObserver(private val context: Application) { val timePerMessage: Float = duration / batch.size.toFloat() Log.d(TAG, "Decrypted ${batch.size} envelopes in $duration ms (~${round(timePerMessage * 100) / 100} ms per message)") } - attempts = 0 + SignalLocalMetrics.PushWebsocketFetch.onProcessedBatch() if (!hasMore && !decryptionDrained) { Log.i(TAG, "Decryptions newly-drained.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt index 775d2296b3..5878d212d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/NetworkConnectionListener.kt @@ -38,6 +38,12 @@ class NetworkConnectionListener(private val context: Context, private val onNetw onNetworkLost { true } } + override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { + super.onBlockedStatusChanged(network, blocked) + Log.d(TAG, "ConnectivityManager.NetworkCallback onBlockedStatusChanged()") + onNetworkLost { blocked } + } + override fun onAvailable(network: Network) { super.onAvailable(network) Log.d(TAG, "ConnectivityManager.NetworkCallback onAvailable()") diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketStrategy.java b/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketStrategy.java index b2e3b1e28c..82d89db293 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketStrategy.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/WebSocketStrategy.java @@ -4,6 +4,7 @@ import androidx.annotation.WorkerThread; import org.signal.core.util.Stopwatch; +import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; @@ -26,6 +27,15 @@ public class WebSocketStrategy extends MessageRetrievalStrategy { private static final String KEEP_ALIVE_TOKEN = "WebsocketStrategy"; private static final long QUEUE_TIMEOUT = TimeUnit.SECONDS.toMillis(30); + private final long websocketDrainTimeoutMs; + public WebSocketStrategy() { + this(TimeUnit.MINUTES.toMillis(1)); + } + + public WebSocketStrategy(long websocketDrainTimeoutMs) { + this.websocketDrainTimeoutMs = websocketDrainTimeoutMs; + } + @WorkerThread @Override public boolean execute() { @@ -39,7 +49,7 @@ public boolean execute() { jobManager.addListener(job -> job.getParameters().getQueue() != null && job.getParameters().getQueue().startsWith(PushProcessMessageJob.QUEUE_PREFIX), queueListener); - if (!blockUntilWebsocketDrained(observer)) { + if (!blockUntilWebsocketDrained(observer, websocketDrainTimeoutMs)) { return false; } @@ -63,7 +73,7 @@ public boolean execute() { } } - private static boolean blockUntilWebsocketDrained(IncomingMessageObserver observer) { + private static boolean blockUntilWebsocketDrained(IncomingMessageObserver observer, long timeoutMs) { CountDownLatch latch = new CountDownLatch(1); observer.addDecryptionDrainedListener(new Runnable() { @@ -74,7 +84,7 @@ private static boolean blockUntilWebsocketDrained(IncomingMessageObserver observ }); try { - if (latch.await(1, TimeUnit.MINUTES)) { + if (latch.await(timeoutMs, TimeUnit.MILLISECONDS)) { return true; } else { Log.w(TAG, "Hit timeout while waiting for decryptions to drain!"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 36c5a20624..121e131ad4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -120,9 +120,10 @@ static final class Version { static final int PREKEY_SYNC = 87; //static final int DEDUPE_DB_MIGRATION = 88; // MOLLY: Skipped migration static final int DEDUPE_DB_MIGRATION_2 = 89; + static final int EMOJI_VERSION_8 = 90; } - public static final int CURRENT_VERSION = 89; + public static final int CURRENT_VERSION = 90; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -532,6 +533,10 @@ private static LinkedHashMap getMigrationJobs(@NonNull Co jobs.put(Version.DEDUPE_DB_MIGRATION_2, new DatabaseMigrationJob()); } + if (lastSeenVersion < Version.EMOJI_VERSION_8) { + jobs.put(Version.EMOJI_VERSION_8, new EmojiDownloadMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java index 049760c631..4b2dcb6152 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java @@ -22,22 +22,24 @@ class AttachmentStreamLocalUriFetcher implements DataFetcher { private final File attachment; private final byte[] key; private final Optional digest; + private final Optional incrementalDigest; private final long plaintextLength; private InputStream is; - AttachmentStreamLocalUriFetcher(File attachment, long plaintextLength, byte[] key, Optional digest) { - this.attachment = attachment; - this.plaintextLength = plaintextLength; - this.digest = digest; - this.key = key; + AttachmentStreamLocalUriFetcher(File attachment, long plaintextLength, byte[] key, Optional digest, Optional incrementalDigest) { + this.attachment = attachment; + this.plaintextLength = plaintextLength; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.key = key; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { if (!digest.isPresent()) throw new InvalidMessageException("No attachment digest!"); - is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get()); + is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get(), incrementalDigest.get()); callback.onDataReady(is); } catch (IOException | InvalidMessageException e) { callback.onLoadFailed(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java index cdb54cb3ce..a8c3f8772d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java @@ -20,7 +20,7 @@ public class AttachmentStreamUriLoader implements ModelLoader buildLoadData(@NonNull AttachmentModel attachmentModel, int width, int height, @NonNull Options options) { - return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.plaintextLength, attachmentModel.key, attachmentModel.digest)); + return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.plaintextLength, attachmentModel.key, attachmentModel.digest, attachmentModel.incrementalDigest)); } @Override @@ -45,15 +45,20 @@ public static class AttachmentModel implements Key { public @NonNull File attachment; public @NonNull byte[] key; public @NonNull Optional digest; + public @NonNull Optional incrementalDigest; public long plaintextLength; - public AttachmentModel(@NonNull File attachment, @NonNull byte[] key, - long plaintextLength, @NonNull Optional digest) + public AttachmentModel(@NonNull File attachment, + @NonNull byte[] key, + long plaintextLength, + @NonNull Optional digest, + @NonNull Optional incrementalDigest) { - this.attachment = attachment; - this.key = key; - this.digest = digest; - this.plaintextLength = plaintextLength; + this.attachment = attachment; + this.key = key; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.plaintextLength = plaintextLength; } @Override 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 c30b96b287..ad156cc618 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/MobileCoinTestNetConfig.java @@ -71,8 +71,8 @@ public MobileCoinTestNetConfig(@NonNull SignalServiceAccountManager signalServic new ServiceConfig( "5341c6702a3312243c0f049f87259352ff32aa80f0f6426351c3dd063d817d7a", "248356aa0d3431abc45da1773cfd6191a4f2989a4a99da31f450bd7c461e312b", - "ac292a1ad27c0338a5159d5fab2bed3917ea144536cb13b5c1226d09a2fbc648", "b61188a6c946557f32e612eff5615908abd1b72ec11d8b7070595a92d4abbbf1", + "ac292a1ad27c0338a5159d5fab2bed3917ea144536cb13b5c1226d09a2fbc648", new String[] { "INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657" } )); diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java index 6b06d58699..d07db50608 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java @@ -14,12 +14,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.autofill.HintConstants; import androidx.core.view.ViewCompat; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; +import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; @@ -52,7 +52,7 @@ public class PinRestoreEntryFragment extends LoggingFragment { private View skipButton; private CircularProgressMaterialButton pinButton; private TextView errorLabel; - private TextView keyboardToggle; + private MaterialButton keyboardToggle; private PinRestoreViewModel viewModel; @Override @@ -102,12 +102,12 @@ private void initViews(@NonNull View root) { keyboardToggle.setOnClickListener((v) -> { PinKeyboardType keyboardType = getPinEntryKeyboardType(); + keyboardToggle.setIconResource(keyboardType.getIconResource()); + updateKeyboard(keyboardType.getOther()); - keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); }); - PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther(); - keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + keyboardToggle.setIconResource(getPinEntryKeyboardType().getOther().getIconResource()); } private void initViewModel() { @@ -260,14 +260,6 @@ private void updateKeyboard(@NonNull PinKeyboardType keyboard) { pinEntry.getText().clear(); } - private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) { - if (keyboard == PinKeyboardType.ALPHA_NUMERIC) { - return R.string.PinRestoreEntryFragment_enter_alphanumeric_pin; - } else { - return R.string.PinRestoreEntryFragment_enter_numeric_pin; - } - } - private void enableAndFocusPinEntry() { pinEntry.setEnabled(true); pinEntry.setFocusable(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NetworkPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NetworkPreferenceFragment.java index 0df80feb0d..c20f4b24e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NetworkPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NetworkPreferenceFragment.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.preferences; -import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.os.Bundle; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index ba7ba89394..23529d23fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -202,7 +202,7 @@ public static void setSyncAvatar(@NonNull Context context, @NonNull RecipientId } } - private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { + public static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { File profileAvatar = getAvatarFile(context, recipientId, false); boolean profileAvatarExists = profileAvatar.exists() && profileAvatar.length() > 0; File syncAvatar = getAvatarFile(context, recipientId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/AvatarProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/providers/AvatarProvider.kt new file mode 100644 index 0000000000..930b4f5cf3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/AvatarProvider.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.providers + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.UriMatcher +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.MemoryFile +import android.os.ParcelFileDescriptor +import android.os.ProxyFileDescriptorCallback +import androidx.annotation.RequiresApi +import org.signal.core.util.StreamUtil +import org.signal.core.util.ThreadUtil +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.profiles.AvatarHelper +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.service.KeyCachingService +import org.thoughtcrime.securesms.util.AvatarUtil +import org.thoughtcrime.securesms.util.DrawableUtil +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.MemoryFileUtil +import org.thoughtcrime.securesms.util.ServiceUtil +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException + +/** + * Provides user avatar bitmaps to the android system service for use in notifications and shortcuts. + * + * This file heavily borrows from [PartProvider] + */ +class AvatarProvider : BaseContentProvider() { + + companion object { + private val TAG = Log.tag(AvatarProvider::class.java) + private const val CONTENT_AUTHORITY = "${BuildConfig.APPLICATION_ID}.avatar" + private const val CONTENT_URI_STRING = "content://$CONTENT_AUTHORITY/avatar" + private const val AVATAR = 1 + private val CONTENT_URI = Uri.parse(CONTENT_URI_STRING) + private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { + addURI(CONTENT_AUTHORITY, "avatar/#", AVATAR) + } + + @JvmStatic + fun getContentUri(context: Context, recipientId: RecipientId): Uri { + val uri = ContentUris.withAppendedId(CONTENT_URI, recipientId.toLong()) + context.applicationContext.grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + + return uri + } + } + + override fun onCreate(): Boolean { + Log.i(TAG, "onCreate called") + return true + } + + @Throws(FileNotFoundException::class) + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { + Log.i(TAG, "openFile() called!") + + if (KeyCachingService.isLocked()) { + Log.w(TAG, "masterSecret was null, abandoning.") + return null + } + + if (uriMatcher.match(uri) == AVATAR) { + Log.i(TAG, "Loading avatar.") + try { + val recipient = getRecipientId(uri)?.let { Recipient.resolved(it) } ?: return null + return if (Build.VERSION.SDK_INT >= 26) { + getParcelStreamProxyForAvatar(recipient) + } else { + getParcelStreamForAvatar(recipient) + } + } catch (ioe: IOException) { + Log.w(TAG, ioe) + throw FileNotFoundException("Error opening file") + } + } + + Log.w(TAG, "Bad request.") + throw FileNotFoundException("Request for bad avatar.") + } + + override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { + Log.i(TAG, "query() called: $uri") + + if (SignalDatabase.instance == null) { + Log.w(TAG, "SignalDatabase unavailable") + return null + } + + if (uriMatcher.match(uri) == AVATAR) { + val recipientId = getRecipientId(uri) ?: return null + + if (AvatarHelper.hasAvatar(context!!, recipientId)) { + val file: File = AvatarHelper.getAvatarFile(context!!, recipientId) + if (file.exists()) { + return createCursor(projection, file.name, file.length()) + } + } + + return createCursor(projection, "fallback-$recipientId.jpg", 0) + } else { + return null + } + } + + override fun getType(uri: Uri): String? { + Log.i(TAG, "getType() called: $uri") + + if (SignalDatabase.instance == null) { + Log.w(TAG, "SignalDatabase unavailable") + return null + } + + if (uriMatcher.match(uri) == AVATAR) { + getRecipientId(uri) ?: return null + + return MediaUtil.IMAGE_PNG + } + + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + Log.i(TAG, "insert() called") + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + Log.i(TAG, "delete() called") + context?.applicationContext?.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + return 0 + } + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + Log.i(TAG, "update() called") + return 0 + } + + private fun getRecipientId(uri: Uri): RecipientId? { + val rawRecipientId = ContentUris.parseId(uri) + if (rawRecipientId <= 0) { + Log.w(TAG, "Invalid recipient id.") + return null + } + + val recipientId = RecipientId.from(rawRecipientId) + if (!SignalDatabase.recipients.containsId(recipientId)) { + Log.w(TAG, "Recipient does not exist.") + return null + } + + return recipientId + } + + @RequiresApi(26) + private fun getParcelStreamProxyForAvatar(recipient: Recipient): ParcelFileDescriptor { + val storageManager = requireNotNull(ServiceUtil.getStorageManager(context!!)) + val handlerThread = SignalExecutors.getAndStartHandlerThread("avatarservice-proxy", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD) + val handler = Handler(handlerThread.looper) + + val parcelFileDescriptor = storageManager.openProxyFileDescriptor( + ParcelFileDescriptor.MODE_READ_ONLY, + ProxyCallback(context!!.applicationContext, recipient), + handler + ) + + Log.i(TAG, "${recipient.id}:createdProxy") + return parcelFileDescriptor + } + + private fun getParcelStreamForAvatar(recipient: Recipient): ParcelFileDescriptor { + val outputStream = ByteArrayOutputStream() + AvatarUtil.getBitmapForNotification(context!!, recipient, DrawableUtil.SHORTCUT_INFO_WRAPPED_SIZE).apply { + compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + + val memoryFile = MemoryFile("${recipient.id}-imf", outputStream.size()) + StreamUtil.copy(ByteArrayInputStream(outputStream.toByteArray()), memoryFile.outputStream) + StreamUtil.close(memoryFile.outputStream) + + return MemoryFileUtil.getParcelFileDescriptor(memoryFile) + } + + @RequiresApi(26) + private class ProxyCallback( + private val context: Context, + private val recipient: Recipient + ) : ProxyFileDescriptorCallback() { + + private var memoryFile: MemoryFile? = null + + override fun onGetSize(): Long { + Log.i(TAG, "${recipient.id}:onGetSize:${Thread.currentThread().name}:${hashCode()}") + ensureResourceLoaded() + return memoryFile!!.length().toLong() + } + + override fun onRead(offset: Long, size: Int, data: ByteArray?): Int { + ensureResourceLoaded() + + return memoryFile!!.readBytes(data, offset.toInt(), 0, size) + } + + override fun onRelease() { + Log.i(TAG, "${recipient.id}:onRelease") + memoryFile = null + } + + private fun ensureResourceLoaded() { + if (memoryFile != null) { + return + } + + Log.i(TAG, "Reading ${recipient.id} icon into RAM.") + + val outputStream = ByteArrayOutputStream() + val avatarBitmap = AvatarUtil.getBitmapForNotification(context, recipient, DrawableUtil.SHORTCUT_INFO_WRAPPED_SIZE) + avatarBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + + Log.i(TAG, "Writing ${recipient.id} icon to MemoryFile") + + memoryFile = MemoryFile("${recipient.id}-imf", outputStream.size()) + StreamUtil.copy(ByteArrayInputStream(outputStream.toByteArray()), memoryFile!!.outputStream) + StreamUtil.close(memoryFile!!.outputStream) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BaseContentProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BaseContentProvider.java index 5e4c6c287a..64a485d040 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BaseContentProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BaseContentProvider.java @@ -12,7 +12,7 @@ import java.util.ArrayList; -abstract class BaseContentProvider extends ContentProvider { +public abstract class BaseContentProvider extends ContentProvider { private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}; diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index fc1cc35155..b8cf0890af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -86,6 +86,8 @@ open class SignalServiceNetworkAccess(context: Context) { BuildConfig.SIGNAL_SFU_URL.stripProtocol(), BuildConfig.SIGNAL_STAGING_SFU_URL.stripProtocol(), BuildConfig.CONTENT_PROXY_HOST.stripProtocol(), + BuildConfig.SIGNAL_CDSI_URL.stripProtocol(), + BuildConfig.SIGNAL_SVR2_URL.stripProtocol(), G_HOST, F_SERVICE_HOST, F_STORAGE_HOST, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt index 0afef57c98..41a7db86d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.registration import org.signal.libsignal.zkgroup.profiles.ProfileKey -import org.whispersystems.signalservice.api.account.PreKeyCollection data class RegistrationData( val code: String, @@ -9,8 +8,6 @@ data class RegistrationData( val password: String, val registrationId: Int, val profileKey: ProfileKey, - val aciPreKeyCollection: PreKeyCollection, - val pniPreKeyCollection: PreKeyCollection, val fcmToken: String?, val pniRegistrationId: Int, val recoveryPassword: String? diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index 411325ed07..023f398726 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -11,7 +11,6 @@ import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.state.KyberPreKeyRecord; -import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.KeyHelper; import org.signal.libsignal.zkgroup.profiles.ProfileKey; @@ -26,6 +25,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; +import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; import org.thoughtcrime.securesms.jobs.RotateCertificateJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationIds; @@ -36,16 +36,14 @@ import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; -import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.push.BackupAuthCheckProcessor; -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; import java.util.List; @@ -105,7 +103,7 @@ public Single> registerAccount(@NonNull Registra return Single.>fromCallable(() -> { try { String pin = response.getPin(); - registerAccountInternal(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData(), setRegistrationLockEnabled); + registerAccountInternal(registrationData, response, setRegistrationLockEnabled); if (pin != null && !pin.isEmpty()) { PinState.onPinChangedOrCreated(context, pin, SignalStore.pinValues().getKeyboardType()); @@ -127,15 +125,16 @@ public Single> registerAccount(@NonNull Registra @WorkerThread private void registerAccountInternal(@NonNull RegistrationData registrationData, - @NonNull VerifyAccountResponse response, - @Nullable String pin, - @Nullable KbsPinData kbsData, + @NonNull VerifyResponse response, boolean setRegistrationLockEnabled) throws IOException { - ACI aci = ACI.parseOrThrow(response.getUuid()); - PNI pni = PNI.parseOrThrow(response.getPni()); - boolean hasPin = response.isStorageCapable(); + Preconditions.checkNotNull(response.getAciPreKeyCollection(), "Missing ACI prekey collection!"); + Preconditions.checkNotNull(response.getPniPreKeyCollection(), "Missing PNI prekey collection!"); + + ACI aci = ACI.parseOrThrow(response.getVerifyAccountResponse().getUuid()); + PNI pni = PNI.parseOrThrow(response.getVerifyAccountResponse().getPni()); + boolean hasPin = response.getVerifyAccountResponse().isStorageCapable(); SignalStore.account().setAci(aci); SignalStore.account().setPni(pni); @@ -144,17 +143,14 @@ private void registerAccountInternal(@NonNull RegistrationData registrationData, ApplicationDependencies.getProtocolStore().pni().sessions().archiveAllSessions(); SenderKeyUtil.clearAllState(); - SignalServiceAccountDataStoreImpl aciProtocolStore = ApplicationDependencies.getProtocolStore().aci(); - PreKeyCollection aciPreKeyCollection = registrationData.getAciPreKeyCollection(); - PreKeyMetadataStore aciMetadataStore = SignalStore.account().aciPreKeys(); - - SignalServiceAccountDataStoreImpl pniProtocolStore = ApplicationDependencies.getProtocolStore().pni(); - PreKeyCollection pniPreKeyCollection = registrationData.getPniPreKeyCollection(); - PreKeyMetadataStore pniMetadataStore = SignalStore.account().pniPreKeys(); + SignalServiceAccountDataStoreImpl aciProtocolStore = ApplicationDependencies.getProtocolStore().aci(); + PreKeyMetadataStore aciMetadataStore = SignalStore.account().aciPreKeys(); - storePreKeys(aciProtocolStore, aciMetadataStore, aciPreKeyCollection); - storePreKeys(pniProtocolStore, pniMetadataStore, pniPreKeyCollection); + SignalServiceAccountDataStoreImpl pniProtocolStore = ApplicationDependencies.getProtocolStore().pni(); + PreKeyMetadataStore pniMetadataStore = SignalStore.account().pniPreKeys(); + storeSignedAndLastResortPreKeys(aciProtocolStore, aciMetadataStore, response.getAciPreKeyCollection()); + storeSignedAndLastResortPreKeys(pniProtocolStore, pniMetadataStore, response.getPniPreKeyCollection()); RecipientTable recipientTable = SignalDatabase.recipients(); RecipientId selfId = Recipient.trustedPush(aci, pni, registrationData.getE164()).getId(); @@ -180,66 +176,33 @@ private void registerAccountInternal(@NonNull RegistrationData registrationData, TextSecurePreferences.setUnauthorizedReceived(context, false); NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID); - PinState.onRegistration(context, kbsData, pin, hasPin, setRegistrationLockEnabled); + PinState.onRegistration(context, response.getKbsData(), response.getPin(), hasPin, setRegistrationLockEnabled); ApplicationDependencies.closeConnections(); ApplicationDependencies.getIncomingMessageObserver(); + PreKeysSyncJob.enqueue(); } - public static PreKeyCollection generatePreKeysForType(ServiceIdType serviceIdType) { - IdentityKeyPair keyPair; - PreKeyMetadataStore metadataStore; - if (serviceIdType == ServiceIdType.ACI) { - if (!SignalStore.account().hasAciIdentityKey()) { - SignalStore.account().generateAciIdentityKeyIfNecessary(); - } - keyPair = SignalStore.account().getAciIdentityKey(); - metadataStore = SignalStore.account().aciPreKeys(); - } else if (serviceIdType == ServiceIdType.PNI) { - if (!SignalStore.account().hasPniIdentityKey()) { - SignalStore.account().generatePniIdentityKeyIfNecessary(); - } - keyPair = SignalStore.account().getPniIdentityKey(); - metadataStore = SignalStore.account().pniPreKeys(); - } else { - throw new IllegalArgumentException("serviceIdType is not one of {ACI, PNI}"); - } - int nextSignedPreKeyId = metadataStore.getNextSignedPreKeyId(); - SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(nextSignedPreKeyId, keyPair.getPrivateKey()); - metadataStore.setActiveSignedPreKeyId(signedPreKey.getId()); - - int ecOneTimePreKeyIdOffset = metadataStore.getNextEcOneTimePreKeyId(); - List oneTimeEcPreKeys = PreKeyUtil.generateOneTimeEcPreKeys(ecOneTimePreKeyIdOffset); - - - int nextKyberPreKeyId = metadataStore.getNextKyberPreKeyId(); - KyberPreKeyRecord lastResortKyberPreKey = PreKeyUtil.generateKyberPreKey(nextKyberPreKeyId, keyPair.getPrivateKey()); - metadataStore.setLastResortKyberPreKeyId(nextKyberPreKeyId); - - int oneTimeKyberPreKeyIdOffset = metadataStore.getNextKyberPreKeyId(); - List oneTimeKyberPreKeys = PreKeyUtil.generateOneTimeKyberPreKeyRecords(oneTimeKyberPreKeyIdOffset, keyPair.getPrivateKey()); + public static PreKeyCollection generateSignedAndLastResortPreKeys(IdentityKeyPair identity, PreKeyMetadataStore metadataStore) { + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(metadataStore.getNextSignedPreKeyId(), identity.getPrivateKey()); + KyberPreKeyRecord lastResortKyberPreKey = PreKeyUtil.generateLastRestortKyberPreKey(metadataStore.getNextKyberPreKeyId(), identity.getPrivateKey()); return new PreKeyCollection( - keyPair, - nextSignedPreKeyId, - ecOneTimePreKeyIdOffset, - nextKyberPreKeyId, - oneTimeKyberPreKeyIdOffset, - serviceIdType, - keyPair.getPublicKey(), + identity.getPublicKey(), signedPreKey, - oneTimeEcPreKeys, - lastResortKyberPreKey, - oneTimeKyberPreKeys + lastResortKyberPreKey ); } - private static void storePreKeys(SignalServiceAccountDataStoreImpl protocolStore, PreKeyMetadataStore metadataStore, PreKeyCollection preKeyCollection) { - PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.getNextSignedPreKeyId(), preKeyCollection.getSignedPreKey()); - PreKeyUtil.storeOneTimeEcPreKeys(protocolStore, metadataStore, preKeyCollection.getEcOneTimePreKeyIdOffset(), preKeyCollection.getOneTimeEcPreKeys()); - PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.getLastResortKyberPreKeyId(), preKeyCollection.getLastResortKyberPreKey()); - PreKeyUtil.storeOneTimeKyberPreKeys(protocolStore, metadataStore, preKeyCollection.getOneTimeKyberPreKeyIdOffset(), preKeyCollection.getOneTimeKyberPreKeys()); + private static void storeSignedAndLastResortPreKeys(SignalServiceAccountDataStoreImpl protocolStore, PreKeyMetadataStore metadataStore, PreKeyCollection preKeyCollection) { + PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.getSignedPreKey()); metadataStore.setSignedPreKeyRegistered(true); + metadataStore.setActiveSignedPreKeyId(preKeyCollection.getSignedPreKey().getId()); + metadataStore.setLastSignedPreKeyRotationTime(System.currentTimeMillis()); + + PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.getLastResortKyberPreKey()); + metadataStore.setLastResortKyberPreKeyId(preKeyCollection.getLastResortKyberPreKey().getId()); + metadataStore.setLastResortKyberPreKeyRotationTime(System.currentTimeMillis()); } private void saveOwnIdentityKey(@NonNull RecipientId selfId, @NonNull SignalServiceAccountDataStoreImpl protocolStore, long now) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt index 478e9beefe..92b1b7e638 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt @@ -6,6 +6,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.signal.core.util.logging.Log +import org.signal.libsignal.protocol.IdentityKeyPair import org.thoughtcrime.securesms.AppCapabilities import org.thoughtcrime.securesms.gcm.FcmUtil import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -185,9 +186,18 @@ class VerifyAccountRepository(private val context: Application) { recoveryPassword = registrationData.recoveryPassword ) + SignalStore.account().generateAciIdentityKeyIfNecessary() + val aciIdentity: IdentityKeyPair = SignalStore.account().aciIdentityKey + + SignalStore.account().generatePniIdentityKeyIfNecessary() + val pniIdentity: IdentityKeyPair = SignalStore.account().pniIdentityKey + + val aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(aciIdentity, SignalStore.account().aciPreKeys) + val pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(pniIdentity, SignalStore.account().pniPreKeys) + return Single.fromCallable { - val response = accountManager.registerAccount(sessionId, registrationData.recoveryPassword, accountAttributes, registrationData.aciPreKeyCollection, registrationData.pniPreKeyCollection, registrationData.fcmToken, true) - VerifyResponse.from(response, kbsData, pin) + val response = accountManager.registerAccount(sessionId, registrationData.recoveryPassword, accountAttributes, aciPreKeyCollection, pniPreKeyCollection, registrationData.fcmToken, true) + VerifyResponse.from(response, kbsData, pin, aciPreKeyCollection, pniPreKeyCollection) }.subscribeOn(Schedulers.io()) } @@ -205,7 +215,7 @@ class VerifyAccountRepository(private val context: Application) { enum class Mode(val isSmsRetrieverSupported: Boolean) { SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), - PHONE_CALL(false); + PHONE_CALL(false) } private class PushTokenChallengeSubscriber { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponse.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponse.kt index a84f00b5c5..5692784946 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponse.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponse.kt @@ -1,14 +1,27 @@ package org.thoughtcrime.securesms.registration import org.whispersystems.signalservice.api.KbsPinData +import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse -data class VerifyResponse(val verifyAccountResponse: VerifyAccountResponse, val kbsData: KbsPinData?, val pin: String?) { +data class VerifyResponse( + val verifyAccountResponse: VerifyAccountResponse, + val kbsData: KbsPinData?, + val pin: String?, + val aciPreKeyCollection: PreKeyCollection?, + val pniPreKeyCollection: PreKeyCollection? +) { companion object { - fun from(response: ServiceResponse, kbsData: KbsPinData?, pin: String?): ServiceResponse { + fun from( + response: ServiceResponse, + kbsData: KbsPinData?, + pin: String?, + aciPreKeyCollection: PreKeyCollection?, + pniPreKeyCollection: PreKeyCollection? + ): ServiceResponse { return if (response.result.isPresent) { - ServiceResponse.forResult(VerifyResponse(response.result.get(), kbsData, pin), 200, null) + ServiceResponse.forResult(VerifyResponse(response.result.get(), kbsData, pin, aciPreKeyCollection, pniPreKeyCollection), 200, null) } else { ServiceResponse.coerceError(response) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java index 2b208d1dac..4afa97cd87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java @@ -10,10 +10,11 @@ import android.widget.Toast; import androidx.annotation.CallSuper; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; +import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; @@ -49,7 +50,7 @@ public abstract class BaseRegistrationLockFragment extends LoggingFragment { private View forgotPin; protected CircularProgressMaterialButton pinButton; private TextView errorLabel; - private TextView keyboardToggle; + private MaterialButton keyboardToggle; private long timeRemaining; private BaseRegistrationViewModel viewModel; @@ -101,11 +102,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat PinKeyboardType keyboardType = getPinEntryKeyboardType(); updateKeyboard(keyboardType.getOther()); - keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + keyboardToggle.setIconResource(keyboardType.getIconResource()); }); PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther(); - keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + keyboardToggle.setIconResource(keyboardType.getIconResource()); disposables.bindTo(getViewLifecycleOwner().getLifecycle()); viewModel = getViewModel(); @@ -274,14 +275,6 @@ private void updateKeyboard(@NonNull PinKeyboardType keyboard) { pinEntry.getText().clear(); } - private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) { - if (keyboard == PinKeyboardType.ALPHA_NUMERIC) { - return R.string.RegistrationLockFragment__enter_alphanumeric_pin; - } else { - return R.string.RegistrationLockFragment__enter_numeric_pin; - } - } - private void enableAndFocusPinEntry() { pinEntry.setEnabled(true); pinEntry.setFocusable(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt index 3b2b965faf..316fc31c74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt @@ -5,7 +5,6 @@ import android.text.InputType import android.view.View import android.view.inputmethod.EditorInfo import android.widget.Toast -import androidx.annotation.StringRes import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController @@ -74,13 +73,12 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fra } binding.pinRestoreKeyboardToggle.setOnClickListener { - val keyboardType: PinKeyboardType = getPinEntryKeyboardType() - updateKeyboard(keyboardType.other) - binding.pinRestoreKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType)) + val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType() + updateKeyboard(currentKeyboardType.other) + binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource) } - val keyboardType: PinKeyboardType = getPinEntryKeyboardType().other - binding.pinRestoreKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType)) + binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource) reRegisterViewModel.updateTokenData(registrationViewModel.keyBackupCurrentToken) @@ -212,15 +210,6 @@ class ReRegisterWithPinFragment : LoggingFragment(R.layout.pin_restore_entry_fra binding.pinRestorePinInput.text?.clear() } - @StringRes - private fun resolveKeyboardToggleText(keyboard: PinKeyboardType): Int { - return if (keyboard == PinKeyboardType.ALPHA_NUMERIC) { - R.string.RegistrationLockFragment__enter_alphanumeric_pin - } else { - R.string.RegistrationLockFragment__enter_numeric_pin - } - } - private fun onNeedHelpClicked() { val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt index b92f56cc7d..a8e6a18bd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt @@ -58,6 +58,7 @@ object RegistrationViewDelegate { setMessage(message) setPositiveButton(android.R.string.ok) { _, _ -> onConfirmed.run() } setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onEditNumber.run() } + setOnDismissListener { onEditNumber.run() } }.show() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index 397b7a2220..f0e63d06de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -236,8 +236,6 @@ private RegistrationData getRegistrationData() { getRegistrationSecret(), registrationRepository.getRegistrationId(), registrationRepository.getProfileKey(getNumber().getE164Number()), - RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI), - RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI), getFcmToken(), registrationRepository.getPniRegistrationId(), getSessionId() != null ? null : getRecoveryPassword()); @@ -334,7 +332,7 @@ private Single verifyReRegisterWithRecoveryPassword(@No boolean setRegistrationLockEnabled = verifyResponse.getKbsData() != null; if (!setRegistrationLockEnabled) { - verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), pinData, pin); + verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), pinData, pin, verifyResponse.getAciPreKeyCollection(), verifyResponse.getPniPreKeyCollection()); } return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled) diff --git a/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt b/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt index 6449b59040..20ff6b1607 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/releasechannel/ReleaseChannel.kt @@ -44,6 +44,7 @@ object ReleaseChannel { mediaWidth, mediaHeight, Optional.empty(), + Optional.empty(), Optional.of(media), false, false, diff --git a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java index b79d03bdbe..c89d001c37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageActivity.java @@ -143,6 +143,7 @@ private void displayImage(@NonNull Uri uri) { GlideApp.with(this) .load(new DecryptableUri(uri)) + .fitCenter() .into(image); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java index faa2271bd6..8937f412a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java @@ -35,9 +35,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.disposables.Disposable; - /** * Provide a foreground service for {@link SignalCallManager} to leverage to run in the background when necessary. Also * provides devices listeners needed for during a call (i.e., bluetooth, power button). @@ -74,9 +71,6 @@ public final class WebRtcCallService extends Service implements SignalAudioManag private PhoneStateListener hangUpRtcOnDeviceCallAnswered; private SignalAudioManager signalAudioManager; private int lastNotificationId; - private Notification lastNotification; - private boolean isGroup = true; - private Disposable notificationDisposable = Disposable.empty(); private boolean stopping = false; public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId, boolean isVideoCall) { @@ -86,22 +80,22 @@ public static void update(@NonNull Context context, int type, @NonNull Recipient .putExtra(EXTRA_RECIPIENT_ID, recipientId) .putExtra(EXTRA_IS_VIDEO_CALL, isVideoCall); - ForegroundServiceUtil.startWhenCapableOrThrow(context, intent, FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT); } public static void denyCall(@NonNull Context context) { - ForegroundServiceUtil.startWhenCapableOrThrow(context, denyCallIntent(context), FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, denyCallIntent(context), FOREGROUND_SERVICE_TIMEOUT); } public static void hangup(@NonNull Context context) { - ForegroundServiceUtil.startWhenCapableOrThrow(context, hangupIntent(context), FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, hangupIntent(context), FOREGROUND_SERVICE_TIMEOUT); } public static void stop(@NonNull Context context) { Intent intent = new Intent(context, WebRtcCallService.class); intent.setAction(ACTION_STOP); - ForegroundServiceUtil.startWhenCapableOrThrow(context, intent, FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT); } public static @NonNull Intent denyCallIntent(@NonNull Context context) { @@ -116,7 +110,7 @@ public static void sendAudioManagerCommand(@NonNull Context context, @NonNull Au Intent intent = new Intent(context, WebRtcCallService.class); intent.setAction(ACTION_SEND_AUDIO_COMMAND) .putExtra(EXTRA_AUDIO_COMMAND, command); - ForegroundServiceUtil.startWhenCapableOrThrow(context, intent, FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT); } public static void changePowerButtonReceiver(@NonNull Context context, boolean register) { @@ -124,7 +118,7 @@ public static void changePowerButtonReceiver(@NonNull Context context, boolean r intent.setAction(ACTION_CHANGE_POWER_BUTTON) .putExtra(EXTRA_ENABLED, register); - ForegroundServiceUtil.startWhenCapableOrThrow(context, intent, FOREGROUND_SERVICE_TIMEOUT); + ForegroundServiceUtil.tryToStartWhenCapable(context, intent, FOREGROUND_SERVICE_TIMEOUT); } @Override @@ -152,8 +146,6 @@ public void onDestroy() { Log.v(TAG, "onDestroy"); super.onDestroy(); - notificationDisposable.dispose(); - if (uncaughtExceptionHandlerManager != null) { uncaughtExceptionHandlerManager.unregister(); } @@ -175,8 +167,8 @@ public void onDestroy() { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent == null || intent.getAction() == null) { - setCallNotification(); - stop(); + lastNotificationId = INVALID_NOTIFICATION_ID; + stopIfUpdateNotCalledFirst(); return START_NOT_STICKY; } @@ -185,14 +177,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { switch (intent.getAction()) { case ACTION_UPDATE: - RecipientId recipientId = Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)); - isGroup = Recipient.resolved(recipientId).isGroup(); setCallInProgressNotification(intent.getIntExtra(EXTRA_UPDATE_TYPE, 0), Objects.requireNonNull(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)), intent.getBooleanExtra(EXTRA_IS_VIDEO_CALL, false)); return START_STICKY; case ACTION_SEND_AUDIO_COMMAND: - setCallNotification(); + stopIfUpdateNotCalledFirst(); if (signalAudioManager == null) { signalAudioManager = SignalAudioManager.create(this, this); } @@ -201,7 +191,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { signalAudioManager.handleCommand(audioCommand); return START_STICKY; case ACTION_CHANGE_POWER_BUTTON: - setCallNotification(); + stopIfUpdateNotCalledFirst(); if (intent.getBooleanExtra(EXTRA_ENABLED, false)) { registerPowerButtonReceiver(); } else { @@ -209,15 +199,15 @@ public int onStartCommand(Intent intent, int flags, int startId) { } return START_STICKY; case ACTION_STOP: - setCallNotification(); + stopIfUpdateNotCalledFirst(); stop(); return START_NOT_STICKY; case ACTION_DENY_CALL: - setCallNotification(); + stopIfUpdateNotCalledFirst(); callManager.denyCall(); return START_NOT_STICKY; case ACTION_LOCAL_HANGUP: - setCallNotification(); + stopIfUpdateNotCalledFirst(); callManager.localHangup(); return START_NOT_STICKY; default: @@ -225,10 +215,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { } } - private void setCallNotification() { - if (lastNotificationId != INVALID_NOTIFICATION_ID) { - startForegroundCompat(lastNotificationId, lastNotification); - } else { + private void stopIfUpdateNotCalledFirst() { + if (lastNotificationId == INVALID_NOTIFICATION_ID) { Log.w(TAG, "Service running without having called start first, show temp notification and terminate service."); startForegroundCompat(CallNotificationBuilder.getStartingStoppingNotificationId(), CallNotificationBuilder.getStoppingNotification(this)); stop(); @@ -236,24 +224,12 @@ private void setCallNotification() { } public void setCallInProgressNotification(int type, @NonNull RecipientId id, boolean isVideoCall) { - if (lastNotificationId == INVALID_NOTIFICATION_ID) { - lastNotificationId = CallNotificationBuilder.getStartingStoppingNotificationId(); - lastNotification = CallNotificationBuilder.getStartingNotification(this); - startForegroundCompat(lastNotificationId, lastNotification); - } - - notificationDisposable.dispose(); - notificationDisposable = CallNotificationBuilder.getCallInProgressNotification(this, type, Recipient.resolved(id), isVideoCall) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(notification -> { - lastNotificationId = CallNotificationBuilder.getNotificationId(type); - lastNotification = notification; + lastNotificationId = CallNotificationBuilder.getNotificationId(type); - startForegroundCompat(lastNotificationId, lastNotification); - }); + startForegroundCompat(lastNotificationId, CallNotificationBuilder.getCallInProgressNotification(this, type, Recipient.resolved(id), isVideoCall)); } - private synchronized void startForegroundCompat(int notificationId, Notification notification) { + private void startForegroundCompat(int notificationId, Notification notification) { if (stopping) { return; } @@ -265,9 +241,8 @@ private synchronized void startForegroundCompat(int notificationId, Notification } } - private synchronized void stop() { + private void stop() { stopping = true; - notificationDisposable.dispose(); stopForeground(true); stopSelf(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java index c6fcb39678..f0b3aff133 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementAdapter.java @@ -273,6 +273,7 @@ void bind(@NonNull GlideRequests glideRequests, glideRequests.load(new DecryptableUri(stickerPack.getCover().getUri())) .transition(DrawableTransitionOptions.withCrossFade()) + .fitCenter() .set(ApngOptions.ANIMATE, allowApngAnimation) .into(cover); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java index 327ee729d2..4948d040b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewActivity.java @@ -206,6 +206,7 @@ private void presentManifest(@NonNull StickerManifest manifest) { : new StickerRemoteUri(cover.getPackId(), cover.getPackKey(), cover.getId()); GlideApp.with(this).load(model) .transition(DrawableTransitionOptions.withCrossFade()) + .fitCenter() .set(ApngOptions.ANIMATE, DeviceProperties.shouldAllowApngStickerAnimation(this)) .into(coverImage); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewAdapter.java index cda45f78d1..684d8d8037 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewAdapter.java @@ -82,6 +82,7 @@ void bind(@NonNull GlideRequests glideRequests, glideRequests.load(currentGlideModel) .transition(DrawableTransitionOptions.withCrossFade()) .set(ApngOptions.ANIMATE, allowApngAnimation) + .centerInside() .into(image); image.setOnLongClickListener(v -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java index c84eafe8fb..53999f25f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java @@ -40,6 +40,7 @@ void presentSticker(@NonNull Object stickerGlideModel, @Nullable String emoji) { emojiText.setText(emoji); glideRequests.load(stickerGlideModel) .diskCacheStrategy(DiskCacheStrategy.NONE) + .fitCenter() .into(image); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java index 1af51cc49c..4dbef34281 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerSearchRepository.java @@ -1,9 +1,9 @@ package org.thoughtcrime.securesms.stickers; -import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.components.emoji.EmojiUtil; @@ -12,52 +12,80 @@ import org.thoughtcrime.securesms.database.StickerTable; import org.thoughtcrime.securesms.database.StickerTable.StickerRecordReader; import org.thoughtcrime.securesms.database.model.StickerRecord; +import org.thoughtcrime.securesms.emoji.EmojiSource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + public final class StickerSearchRepository { private final StickerTable stickerDatabase; private final AttachmentTable attachmentDatabase; - public StickerSearchRepository(@NonNull Context context) { + public StickerSearchRepository() { this.stickerDatabase = SignalDatabase.stickers(); this.attachmentDatabase = SignalDatabase.attachments(); } + public @NonNull Single> searchByEmoji(@NonNull String emoji) { + if (emoji.isEmpty() || emoji.length() > EmojiSource.getLatest().getMaxEmojiLength()) { + return Single.just(Collections.emptyList()); + } + + return Single.fromCallable(() -> searchByEmojiSync(emoji)); + } + public void searchByEmoji(@NonNull String emoji, @NonNull Callback> callback) { SignalExecutors.BOUNDED.execute(() -> { - String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji); - List out = new ArrayList<>(); - Set possible = EmojiUtil.getAllRepresentations(searchEmoji); - - for (String candidate : possible) { - try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) { - StickerRecord record = null; - while ((record = reader.getNext()) != null) { - out.add(record); - } + callback.onResult(searchByEmojiSync(emoji)); + }); + } + + @WorkerThread + private List searchByEmojiSync(@NonNull String emoji) { + String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji); + List out = new ArrayList<>(); + Set possible = EmojiUtil.getAllRepresentations(searchEmoji); + + for (String candidate : possible) { + try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) { + StickerRecord record = null; + while ((record = reader.getNext()) != null) { + out.add(record); } } + } - callback.onResult(out); - }); + return out; + } + + public @NonNull Single getStickerFeatureAvailability() { + return Single.fromCallable(this::getStickerFeatureAvailabilitySync) + .observeOn(Schedulers.io()); } public void getStickerFeatureAvailability(@NonNull Callback callback) { SignalExecutors.BOUNDED.execute(() -> { - try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) { - if (cursor != null && cursor.moveToFirst()) { - callback.onResult(true); - } else { - callback.onResult(attachmentDatabase.hasStickerAttachments()); - } - } + callback.onResult(getStickerFeatureAvailabilitySync()); }); } + @WorkerThread + private Boolean getStickerFeatureAvailabilitySync() { + try (Cursor cursor = stickerDatabase.getAllStickerPacks("1")) { + if (cursor != null && cursor.moveToFirst()) { + return true; + } else { + return attachmentDatabase.hasStickerAttachments(); + } + } + } + public interface Callback { void onResult(T result); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt index eaddcd2645..aea93c29eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt @@ -12,8 +12,8 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ThumbnailView import org.thoughtcrime.securesms.databinding.StoriesTextPostLinkPreviewBinding import org.thoughtcrime.securesms.linkpreview.LinkPreview +import org.thoughtcrime.securesms.linkpreview.LinkPreviewState import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil -import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.Slide @@ -73,7 +73,7 @@ class StoryLinkPreviewView @JvmOverloads constructor( return future ?: SettableFuture(false) } - fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean) { + fun bind(linkPreviewState: LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean) { val linkPreview: LinkPreview? = linkPreviewState.linkPreview.orElseGet { linkPreviewState.activeUrlForError?.let { LinkPreview(it, LinkPreviewUtil.getTopLevelDomain(it) ?: it, null, -1L, null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt index 5839084a7b..4c59544e70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost import org.thoughtcrime.securesms.fonts.TextFont import org.thoughtcrime.securesms.linkpreview.LinkPreview -import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel +import org.thoughtcrime.securesms.linkpreview.LinkPreviewState import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryScale import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryTextWatcher @@ -48,7 +48,7 @@ class StoryTextPostView @JvmOverloads constructor( init { TextStoryTextWatcher.install(textView) - textView.movementMethod = LongClickMovementMethod.getInstance(context) + disableCreationMode() } fun getLinkPreviewThumbnailWidth(useLargeThumbnail: Boolean): Int { @@ -59,12 +59,14 @@ class StoryTextPostView @JvmOverloads constructor( return linkPreviewView.getThumbnailViewHeight(useLargeThumbnail) } - fun showCloseButton() { + fun enableCreationMode() { linkPreviewView.setCanClose(true) + textView.movementMethod = null } - fun hideCloseButton() { + fun disableCreationMode() { linkPreviewView.setCanClose(false) + textView.movementMethod = LongClickMovementMethod.getInstance(context) } fun setTypeface(typeface: Typeface) { @@ -144,7 +146,7 @@ class StoryTextPostView @JvmOverloads constructor( setTextColor(storyTextPost.textForegroundColor, false) setTextBackgroundColor(storyTextPost.textBackgroundColor) - hideCloseButton() + disableCreationMode() postAdjustLinkPreviewTranslationY() } @@ -157,7 +159,7 @@ class StoryTextPostView @JvmOverloads constructor( linkPreviewView.setThumbnailDrawable(drawable, useLargeThumbnail) } - fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int, useLargeThumbnail: Boolean) { + fun bindLinkPreviewState(linkPreviewState: LinkPreviewState, hiddenVisibility: Int, useLargeThumbnail: Boolean) { linkPreviewView.bind(linkPreviewState, hiddenVisibility, useLargeThumbnail) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt index de5b03d275..ba74835877 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt @@ -31,7 +31,7 @@ class PrivateStorySettingsFragment : DSLSettingsFragment( menuId = R.menu.story_private_menu ) { - private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() } + private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment.create() } private val viewModel: PrivateStorySettingsViewModel by viewModels( factoryProducer = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt index aa5921f3f2..a7b2271dec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt @@ -50,7 +50,7 @@ class ChooseInitialMyStoryMembershipViewModel @JvmOverloads constructor( return Single.fromCallable { SignalStore.storyValues().userHasBeenNotifiedAboutStories = true Stories.onStorySettingsChanged(Recipient.self().id) - store.state.recipientId + store.state.recipientId!! }.observeOn(AndroidSchedulers.mainThread()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt index df48314827..37d49820fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt @@ -45,7 +45,7 @@ class StoriesPrivacySettingsFragment : }) private val lifecycleDisposable = LifecycleDisposable() - private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() } + private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment.create() } override fun createAdapters(): Array { return arrayOf(DSLSettingsAdapter(), PagingMappingAdapter(), DSLSettingsAdapter()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index 4e6aac656d..bfa8a208b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -451,7 +451,6 @@ class StoryGroupReplyFragment : private fun initializeMentions() { inlineQueryResultsController = InlineQueryResultsController( - requireContext(), inlineQueryViewModel, composer, (requireView() as ViewGroup), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java index 7a6111ec9f..5b6d76d7ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -14,6 +14,9 @@ import androidx.core.graphics.drawable.IconCompat; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.transition.Transition; @@ -24,7 +27,9 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequest; +import org.thoughtcrime.securesms.providers.AvatarProvider; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -55,7 +60,7 @@ public static void loadBlurredIconIntoImageView(@NonNull Recipient recipient, @N GlideApp.with(target) .load(photo) - .transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS)) + .transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS), new CenterCrop()) .into(new CustomViewTarget(target) { @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { @@ -99,12 +104,8 @@ public static Bitmap loadIconBitmapSquareNoCache(@NonNull Context context, } @WorkerThread - public static IconCompat getIconForNotification(@NonNull Context context, @NonNull Recipient recipient) { - try { - return IconCompat.createWithBitmap(requestCircle(GlideApp.with(context).asBitmap(), context, recipient, UNDEFINED_SIZE).submit().get()); - } catch (ExecutionException | InterruptedException e) { - return null; - } + public static IconCompat getIconWithUriForNotification(@NonNull Context context, @NonNull RecipientId recipientId) { + return IconCompat.createWithContentUri(AvatarProvider.getContentUri(context, recipientId)); } @WorkerThread @@ -128,26 +129,31 @@ public static IconCompat getIconForNotification(@NonNull Context context, @NonNu @WorkerThread public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient) { + return getBitmapForNotification(context, recipient, UNDEFINED_SIZE); + } + + @WorkerThread + public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient, int size) { try { - return requestCircle(GlideApp.with(context).asBitmap(), context, recipient, UNDEFINED_SIZE).submit().get(); + return requestCircle(GlideApp.with(context).asBitmap(), context, recipient, size).submit().get(); } catch (ExecutionException | InterruptedException e) { return null; } } private static GlideRequest requestCircle(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, int targetSize) { - return request(glideRequest, context, recipient, targetSize).circleCrop(); + return request(glideRequest, context, recipient, targetSize, new CircleCrop()); } private static GlideRequest requestSquare(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient) { - return request(glideRequest, context, recipient, UNDEFINED_SIZE).centerCrop(); + return request(glideRequest, context, recipient, UNDEFINED_SIZE, new CenterCrop()); } - private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, int targetSize) { - return request(glideRequest, context, recipient, true, targetSize); + private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, int targetSize, @Nullable BitmapTransformation transformation) { + return request(glideRequest, context, recipient, true, targetSize, transformation); } - private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf, int targetSize) { + private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf, int targetSize, @Nullable BitmapTransformation transformation) { final ContactPhoto photo; if (Recipient.self().equals(recipient) && loadSelf) { photo = new ProfileContactPhoto(recipient); @@ -160,7 +166,14 @@ private static GlideRequest request(@NonNull GlideRequest glideRequest .diskCacheStrategy(DiskCacheStrategy.ALL); if (recipient.shouldBlurAvatar()) { - return request.transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS)); + BlurTransformation blur = new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS); + if (transformation != null) { + return request.transform(blur, transformation); + } else { + return request.transform(blur); + } + } else if (transformation != null) { + return request.transform(transformation); } else { return request; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java b/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java index 4292af93b4..485b6346ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java @@ -20,6 +20,7 @@ public final class BlurTransformation extends BitmapTransformation { + private static final int VERSION = 1; public static final float MAX_RADIUS = 25f; private final RenderScript rs; @@ -58,6 +59,6 @@ protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, in @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(String.format(Locale.US, "blur-%f-%f", bitmapScaleFactor, blurRadius).getBytes()); + messageDigest.update(String.format(Locale.US, "blur-%f-%f-%d", bitmapScaleFactor, blurRadius, VERSION).getBytes()); } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java index 6058c7c314..b696ac4c5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java @@ -68,34 +68,25 @@ public static boolean canBubble(@NonNull Context context, @NonNull RecipientId r NotificationChannel conversationChannel = notificationManager.getNotificationChannel(ConversationUtil.getChannelId(context, recipient), ConversationUtil.getShortcutId(recipientId)); - if (conversationChannel == null) { - Log.d(TAG, "Conversation channel was null, therefore no bubbles."); - return false; - } - - if (!conversationChannel.canBubble()) { - Log.d(TAG, "Conversation channel does not allow bubbles."); - return false; + final StringBuilder bubbleLoggingMessage = new StringBuilder("Bubble State:"); + if (Build.VERSION.SDK_INT < 31) { + bubbleLoggingMessage.append("\nisBelowApi31 = true"); + } else { + bubbleLoggingMessage.append("\nnotificationManager.areBubblesEnabled() = ").append(notificationManager.areBubblesEnabled()); + bubbleLoggingMessage.append("\nnotificationManager.getBubblePreference() = ").append(notificationManager.getBubblePreference()); } - if (Build.VERSION.SDK_INT < 31) { - if (!notificationManager.areBubblesAllowed()) { - Log.d(TAG, "Notification Manager does not allow bubbles."); - return false; - } + bubbleLoggingMessage.append("\nnotificationManager.areBubblesAllowed() = ").append(notificationManager.areBubblesAllowed()); + if (conversationChannel != null) { + bubbleLoggingMessage.append("\nconversationChannel.canBubble() = ").append(conversationChannel.canBubble()); } else { - if (!notificationManager.areBubblesEnabled()) { - Log.d(TAG, "Notification Manager disabled bubbles."); - return false; - } - - if (notificationManager.getBubblePreference() == NotificationManager.BUBBLE_PREFERENCE_NONE) { - Log.d(TAG, "Bubble preference in Notification Manager was none, therefore no bubbles."); - return false; - } + bubbleLoggingMessage.append("\nconversationChannel = null"); } - - return true; + + Log.d(TAG, bubbleLoggingMessage.toString()); + + return (Build.VERSION.SDK_INT < 31 || (notificationManager.areBubblesEnabled() && notificationManager.getBubblePreference() != NotificationManager.BUBBLE_PREFERENCE_NONE)) && + (notificationManager.areBubblesAllowed() || (conversationChannel != null && conversationChannel.canBubble())); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java index 4f7f5dc0e6..879f0fa597 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java @@ -286,12 +286,11 @@ private static boolean pushShortcutForRecipientInternal(@NonNull Context context /** * @return A Compat Library Person object representing the given Recipient */ - @WorkerThread public static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) { return new Person.Builder() .setKey(getShortcutId(recipient.getId())) .setName(recipient.getDisplayName(context)) - .setIcon(AvatarUtil.getIconForNotification(context, recipient)) + .setIcon(AvatarUtil.getIconWithUriForNotification(context, recipient.getId())) .setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 1a2eede984..213b7688ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -104,7 +104,7 @@ public final class FeatureFlags { private static final String PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.3"; private static final String ANY_ADDRESS_PORTS_KILL_SWITCH = "android.calling.fieldTrial.anyAddressPortsKillSwitch"; private static final String AD_HOC_CALLING = "android.calling.ad.hoc.2"; - private static final String EDIT_MESSAGE_SEND = "android.editMessage.send.2"; + private static final String EDIT_MESSAGE_SEND = "android.editMessage.send.3"; private static final String MAX_ATTACHMENT_COUNT = "android.attachments.maxCount"; private static final String MAX_ATTACHMENT_SIZE_MB = "android.attachments.maxSize"; 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 37c97fd1fa..f524885587 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt @@ -77,6 +77,24 @@ object LocalMetrics { } } + /** + * Marks a split for an event. Updates the last time, so future splits will have duration relative to this event. + * + * If an event with the provided ID does not exist, this is effectively a no-op. + */ + fun splitWithDuration(id: String, split: String, duration: Long) { + val time = System.currentTimeMillis() + + executor.execute { + val lastTime: Long? = lastSplitTimeById[id] + val splitDoesNotExist: Boolean = eventsById[id]?.splits?.none { it.name == split } ?: true + if (lastTime != null && splitDoesNotExist) { + eventsById[id]?.splits?.add(LocalMetricsSplit(split, duration)) + lastSplitTimeById[id] = time + } + } + } + /** * Stop tracking an event you were previously tracking. All future calls to [split] and [end] will do nothing for this id. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java index 1cfe455bd7..9a8f8cd4f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java @@ -12,6 +12,7 @@ import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import org.thoughtcrime.securesms.R; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt index 3428903e71..6802182837 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt @@ -155,3 +155,10 @@ fun MessageRecord.getRecordQuoteType(): QuoteModel.Type { fun MessageRecord.isEditMessage(): Boolean { return this is MediaMmsMessageRecord && isEditMessage } + +/** + * Returns whether or not the given message record can be reacted to. + */ +fun MessageRecord.isValidReactionTarget(): Boolean { + return isSecure && !isPending && !isFailed && !isRemoteDelete && !isUpdate +} 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 efdbe9b8b5..930271d9d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; /** * A nice interface for {@link LocalMetrics} that gives us a place to define string constants and nicer method names. @@ -189,6 +190,54 @@ private static void end(long messageId) { } } + public static final class PushWebsocketFetch { + private static final String SUCCESS_EVENT = "push-websocket-fetch"; + private static final String TIMEOUT_EVENT = "timed-out-fetch"; + + private static final String SPLIT_BATCH_PROCESSED = "batches-processed"; + private static final String SPLIT_PROCESS_TIME = "fetch-time"; + private static final String SPLIT_TIMED_OUT = "timeout"; + + private static final AtomicInteger processedBatches = new AtomicInteger(0); + + public static @NonNull String startFetch() { + String baseId = System.currentTimeMillis() + ""; + + String timeoutId = TIMEOUT_EVENT + baseId; + String successId = SUCCESS_EVENT + baseId; + + LocalMetrics.getInstance().start(successId, SUCCESS_EVENT); + LocalMetrics.getInstance().start(timeoutId, TIMEOUT_EVENT); + processedBatches.set(0); + + return baseId; + } + + public static void onProcessedBatch() { + processedBatches.incrementAndGet(); + } + + public static void onTimedOut(String metricId) { + LocalMetrics.getInstance().cancel(SUCCESS_EVENT + metricId); + + String timeoutId = TIMEOUT_EVENT + metricId; + + LocalMetrics.getInstance().split(timeoutId, SPLIT_TIMED_OUT); + LocalMetrics.getInstance().end(timeoutId); + } + + public static void onDrained(String metricId) { + LocalMetrics.getInstance().cancel(TIMEOUT_EVENT + metricId); + + String successId = SUCCESS_EVENT + metricId; + + LocalMetrics.getInstance().split(successId, SPLIT_PROCESS_TIME); + LocalMetrics.getInstance().splitWithDuration(successId, SPLIT_BATCH_PROCESSED, processedBatches.get()); + LocalMetrics.getInstance().end(successId); + } + + } + public static final class GroupMessageSend { private static final String NAME = "group-message-send"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewModelFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewModelFactory.kt index 07344cd7d5..19e5e65590 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewModelFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewModelFactory.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util import androidx.annotation.MainThread import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -35,3 +36,12 @@ inline fun Fragment.viewModel( factoryProducer = ViewModelFactory.factoryProducer(create) ) } + +@MainThread +inline fun Fragment.activityViewModel( + noinline create: () -> VM +): Lazy { + return activityViewModels( + factoryProducer = ViewModelFactory.factoryProducer(create) + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/SimpleProgressDialog.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/SimpleProgressDialog.java index 792bcde260..261e64fa08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/views/SimpleProgressDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/SimpleProgressDialog.java @@ -7,8 +7,6 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; @@ -27,7 +25,7 @@ private SimpleProgressDialog() {} @MainThread public static @NonNull AlertDialog show(@NonNull Context context) { - AlertDialog dialog = new MaterialAlertDialogBuilder(context) + AlertDialog dialog = new AlertDialog.Builder(context) .setView(R.layout.progress_dialog) .setCancelable(false) .create(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/InMemoryTranscoder.java b/app/src/main/java/org/thoughtcrime/securesms/video/InMemoryTranscoder.java index 4d0931f4bb..1f952b4ded 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/InMemoryTranscoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/InMemoryTranscoder.java @@ -9,18 +9,24 @@ import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.io.ByteStreams; import org.signal.core.util.logging.Log; +import org.signal.libsignal.media.Mp4Sanitizer; +import org.signal.libsignal.media.ParseException; +import org.signal.libsignal.media.SanitizedMetadata; import org.thoughtcrime.securesms.media.MediaInput; import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; import org.thoughtcrime.securesms.video.videoconverter.EncodingException; import org.thoughtcrime.securesms.video.videoconverter.MediaConverter; +import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; +import java.io.SequenceInputStream; import java.text.NumberFormat; import java.util.Locale; @@ -76,7 +82,7 @@ public InMemoryTranscoder(@NonNull Context context, @NonNull MediaDataSource dat public @NonNull MediaStream transcode(@NonNull Progress progress, @Nullable TranscoderCancelationSignal cancelationSignal) - throws IOException, EncodingException, VideoSizeException + throws IOException, EncodingException { if (memoryFile != null) throw new AssertionError("Not expecting to reuse transcoder"); @@ -138,6 +144,15 @@ public InMemoryTranscoder(@NonNull Context context, @NonNull MediaDataSource dat converter.convert(); + memoryFile.seek(0); + + SanitizedMetadata metadata = null; + try { + metadata = Mp4Sanitizer.sanitize(new FileInputStream(memoryFileFileDescriptor), memoryFile.size()); + } catch (ParseException e) { + Log.e(TAG, "Could not parse MP4 file.", e); + } + // output details of the transcoding long outSize = memoryFile.size(); float encodeDurationSec = (System.currentTimeMillis() - startTime) / 1000f; @@ -162,9 +177,14 @@ public InMemoryTranscoder(@NonNull Context context, @NonNull MediaDataSource dat throw new VideoSizeException("Size constraints could not be met!"); } - memoryFile.seek(0); - return new MediaStream(new FileInputStream(memoryFileFileDescriptor), MimeTypes.VIDEO_MP4, 0, 0); + if (metadata != null && metadata.getSanitizedMetadata() != null) { + memoryFile.seek(metadata.getDataOffset()); + return new MediaStream(new SequenceInputStream(new ByteArrayInputStream(metadata.getSanitizedMetadata()), ByteStreams.limit(new FileInputStream(memoryFileFileDescriptor), metadata.getDataLength())), MimeTypes.VIDEO_MP4, 0, 0); + } else { + memoryFile.seek(0); + return new MediaStream(new FileInputStream(memoryFileFileDescriptor), MimeTypes.VIDEO_MP4, 0, 0); + } } public boolean isTranscodeRequired() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java index 751c57ccef..231fb95b92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java @@ -13,6 +13,7 @@ import androidx.annotation.Nullable; import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; @@ -64,14 +65,16 @@ public boolean isPhoto() { @Override public void loadInto(@NonNull ImageView imageView) { Bitmap cached = CACHE.get(uri); - if (cached != null) { + if (cached != null && !cached.isRecycled()) { Log.d(TAG, "Using cached value."); - imageView.setImageBitmap(CACHE.get(uri)); + imageView.setImageBitmap(cached); } else { - Log.d(TAG, "Not in cache. Fetching using Glide."); - GlideApp.with(imageView) + Log.d(TAG, "Not in cache or recycled. Fetching using Glide."); + GlideApp.with(imageView.getContext().getApplicationContext()) .asBitmap() .load(new DecryptableStreamUriLoader.DecryptableUri(uri)) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) .addListener(new RequestListener<>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { @@ -94,16 +97,18 @@ public boolean onResourceReady(Bitmap resource, Object model, Target tar @Override public boolean prefetch(@NonNull Context context, long maxWaitTime) { Bitmap cached = CACHE.get(uri); - if (cached != null) { + if (cached != null && !cached.isRecycled()) { Log.d(TAG, "Already cached, skipping prefetch."); return true; } long startTime = System.currentTimeMillis(); try { - Bitmap bitmap = GlideApp.with(context) + Bitmap bitmap = GlideApp.with(context.getApplicationContext()) .asBitmap() .load(new DecryptableStreamUriLoader.DecryptableUri(uri)) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) .submit() .get(maxWaitTime, TimeUnit.MILLISECONDS); diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java index 3f4667c27f..ab488c6b0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java @@ -7,10 +7,11 @@ import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; import org.signal.core.util.PendingIntentFlags; -import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.WebRtcCallActivity; @@ -19,10 +20,6 @@ import org.thoughtcrime.securesms.service.webrtc.WebRtcCallService; import org.thoughtcrime.securesms.util.ConversationUtil; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - /** * Manages the state of the WebRtc items in the Android notification bar. * @@ -39,6 +36,20 @@ public class CallNotificationBuilder { public static final int TYPE_ESTABLISHED = 3; public static final int TYPE_INCOMING_CONNECTING = 4; + private enum LaunchCallScreenIntentState { + CONTENT(null, 0), + AUDIO(WebRtcCallActivity.ANSWER_ACTION, 1), + VIDEO(WebRtcCallActivity.ANSWER_VIDEO_ACTION, 2); + + final @Nullable String action; + final int requestCode; + + LaunchCallScreenIntentState(@Nullable String action, int requestCode) { + this.action = action; + this.requestCode = requestCode; + } + } + /** * This is the API level at which call style notifications will * properly pop over the screen and allow a user to answer a call. @@ -50,16 +61,9 @@ public class CallNotificationBuilder { */ public static final int API_LEVEL_CALL_STYLE = 29; - public static Single getCallInProgressNotification(Context context, int type, Recipient recipient, boolean isVideoCall) { - Intent contentIntent = new Intent(context, WebRtcCallActivity.class); - contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - contentIntent.putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_FULLSCREEN, true); - // MOLLY: Set a non-empty action to clear any pending action held by the screen lock - contentIntent.setAction(WebRtcCallActivity.NO_ACTION); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentFlags.mutable()); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(type)) + public static Notification getCallInProgressNotification(Context context, int type, Recipient recipient, boolean isVideoCall) { + PendingIntent pendingIntent = getActivityPendingIntent(context, LaunchCallScreenIntentState.CONTENT); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(type)) .setSmallIcon(R.drawable.ic_call_secure_white_24dp) .setContentIntent(pendingIntent) .setOngoing(true) @@ -69,33 +73,27 @@ public static Single getCallInProgressNotification(Context context builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting)); builder.setPriority(NotificationCompat.PRIORITY_MIN); builder.setContentIntent(null); - return Single.just(builder.build()); + return builder.build(); } else if (type == TYPE_INCOMING_RINGING) { builder.setContentText(getIncomingCallContentText(context, recipient, isVideoCall)); builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setFullScreenIntent(pendingIntent, true); - if (deviceVersionSupportsIncomingCallStyle()) - { - return Single.fromCallable(() -> ConversationUtil.buildPerson(context, recipient)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .map(person -> { - builder.setStyle(NotificationCompat.CallStyle.forIncomingCall( - person, - getServicePendingIntent(context, WebRtcCallService.denyCallIntent(context)), - getActivityPendingIntent(context, isVideoCall ? WebRtcCallActivity.ANSWER_VIDEO_ACTION : WebRtcCallActivity.ANSWER_ACTION) - ).setIsVideo(isVideoCall)); - return builder.build(); - }); - } else { - return Single.just(builder.build()); + if (deviceVersionSupportsIncomingCallStyle()) { + Person person = ConversationUtil.buildPerson(context, recipient); + builder.setStyle(NotificationCompat.CallStyle.forIncomingCall( + person, + getServicePendingIntent(context, WebRtcCallService.denyCallIntent(context)), + getActivityPendingIntent(context, isVideoCall ? LaunchCallScreenIntentState.VIDEO : LaunchCallScreenIntentState.AUDIO) + ).setIsVideo(isVideoCall)); } + + return builder.build(); } else if (type == TYPE_OUTGOING_RINGING) { builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call)); builder.addAction(getServiceNotificationAction(context, WebRtcCallService.hangupIntent(context), R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call)); - return Single.just(builder.build()); + return builder.build(); } else { builder.setContentText(getOngoingCallContentText(context, recipient, isVideoCall)); builder.setOnlyAlertOnce(true); @@ -103,19 +101,14 @@ public static Single getCallInProgressNotification(Context context builder.setCategory(NotificationCompat.CATEGORY_CALL); if (deviceVersionSupportsIncomingCallStyle()) { - return Single.fromCallable(() -> ConversationUtil.buildPerson(context, recipient)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .map(person -> { - builder.setStyle(NotificationCompat.CallStyle.forOngoingCall( - person, - getServicePendingIntent(context, WebRtcCallService.hangupIntent(context)) - ).setIsVideo(isVideoCall)); - return builder.build(); - }); - } else { - return Single.just(builder.build()); + Person person = ConversationUtil.buildPerson(context, recipient); + builder.setStyle(NotificationCompat.CallStyle.forOngoingCall( + person, + getServicePendingIntent(context, WebRtcCallService.hangupIntent(context)) + ).setIsVideo(isVideoCall)); } + + return builder.build(); } } @@ -196,11 +189,21 @@ private static NotificationCompat.Action getServiceNotificationAction(Context co return new NotificationCompat.Action(iconResId, context.getString(titleResId), getServicePendingIntent(context, intent)); } - private static PendingIntent getActivityPendingIntent(@NonNull Context context, @NonNull String action) { + private static PendingIntent getActivityPendingIntent(@NonNull Context context, @NonNull LaunchCallScreenIntentState launchCallScreenIntentState) { Intent intent = new Intent(context, WebRtcCallActivity.class); - intent.setAction(action); + intent.setAction(launchCallScreenIntentState.action); + + if (launchCallScreenIntentState == LaunchCallScreenIntentState.CONTENT) { + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + intent.putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_FULLSCREEN, launchCallScreenIntentState == LaunchCallScreenIntentState.CONTENT); + intent.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false); + + // MOLLY: Set a non-empty action to clear any pending action held by the screen lock + intent.setAction(WebRtcCallActivity.NO_ACTION); - return PendingIntent.getActivity(context, 0, intent, PendingIntentFlags.mutable()); + return PendingIntent.getActivity(context, launchCallScreenIntentState.requestCode, intent, PendingIntentFlags.updateCurrent()); } private static boolean deviceVersionSupportsIncomingCallStyle() { diff --git a/app/src/main/res/drawable/ic_keyboard_24.xml b/app/src/main/res/drawable/ic_keyboard_24.xml index 142edd1d8a..7781422e8c 100644 --- a/app/src/main/res/drawable/ic_keyboard_24.xml +++ b/app/src/main/res/drawable/ic_keyboard_24.xml @@ -1,5 +1,43 @@ - - + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_number_pad_conversation_filter_24.xml b/app/src/main/res/drawable/ic_number_pad_conversation_filter_24.xml index b9d9ef2bbe..0658693275 100644 --- a/app/src/main/res/drawable/ic_number_pad_conversation_filter_24.xml +++ b/app/src/main/res/drawable/ic_number_pad_conversation_filter_24.xml @@ -1,5 +1,36 @@ - - + + + + + + + + + + + diff --git a/app/src/main/res/layout/backup_enable_dialog.xml b/app/src/main/res/layout/backup_enable_dialog.xml index e25b0541d0..c3e3b8020f 100644 --- a/app/src/main/res/layout/backup_enable_dialog.xml +++ b/app/src/main/res/layout/backup_enable_dialog.xml @@ -1,95 +1,114 @@ - + - + - + - + - + - + - - + - + + - + - + - - - + - + + + - + - - + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/backup_enable_dialog_v29.xml b/app/src/main/res/layout/backup_enable_dialog_v29.xml index 5fb8e63792..3fb073f9ad 100644 --- a/app/src/main/res/layout/backup_enable_dialog_v29.xml +++ b/app/src/main/res/layout/backup_enable_dialog_v29.xml @@ -1,130 +1,136 @@ - + tools:viewBindingIgnore="true"> - - - - - - - + android:orientation="vertical" + android:paddingStart="23dp" + android:paddingTop="12dp" + android:paddingEnd="23dp"> - - - + - + - + - - - - + - + + + + + + + + + + + + + + + + + + + + + + - + android:layout_marginEnd="10dp" /> - - + android:text="@string/backup_enable_dialog__i_have_written_down_this_passphrase" + android:textSize="12sp" /> + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/base_kbs_pin_fragment.xml b/app/src/main/res/layout/base_kbs_pin_fragment.xml index fd6d07709e..10b6ba8271 100644 --- a/app/src/main/res/layout/base_kbs_pin_fragment.xml +++ b/app/src/main/res/layout/base_kbs_pin_fragment.xml @@ -102,6 +102,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" + app:icon="@drawable/ic_keyboard_24" + app:iconGravity="textStart" + app:iconPadding="8dp" app:layout_constraintBottom_toTopOf="@id/edit_kbs_pin_confirm" app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_input_label" app:layout_constraintVertical_bias="0.0" diff --git a/app/src/main/res/layout/conversation_test_fragment.xml b/app/src/main/res/layout/conversation_test_fragment.xml new file mode 100644 index 0000000000..aad7d8e65f --- /dev/null +++ b/app/src/main/res/layout/conversation_test_fragment.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/delivery_status_view.xml b/app/src/main/res/layout/delivery_status_view.xml deleted file mode 100644 index 3fba6e5f1c..0000000000 --- a/app/src/main/res/layout/delivery_status_view.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_lock.xml b/app/src/main/res/layout/fragment_registration_lock.xml index e3be5f4bb7..e546806942 100644 --- a/app/src/main/res/layout/fragment_registration_lock.xml +++ b/app/src/main/res/layout/fragment_registration_lock.xml @@ -78,11 +78,14 @@ style="@style/Signal.Widget.Button.Large.Secondary" android:layout_width="match_parent" android:layout_height="wrap_content" + android:text="@string/RegistrationLockFragment__switch_keyboard" + app:icon="@drawable/ic_keyboard_24" + app:iconGravity="textStart" + app:iconPadding="8dp" app:layout_constraintBottom_toTopOf="@id/kbs_lock_pin_confirm" app:layout_constraintTop_toBottomOf="@id/kbs_lock_forgot_pin" app:layout_constraintVertical_bias="0.0" - tools:layout_editor_absoluteX="32dp" - tools:text="Create Alphanumeric Pin" /> + tools:layout_editor_absoluteX="32dp" /> diff --git a/app/src/main/res/layout/pin_restore_entry_fragment.xml b/app/src/main/res/layout/pin_restore_entry_fragment.xml index 7f7f139131..27e23e9e8c 100644 --- a/app/src/main/res/layout/pin_restore_entry_fragment.xml +++ b/app/src/main/res/layout/pin_restore_entry_fragment.xml @@ -97,10 +97,13 @@ style="@style/Signal.Widget.Button.Large.Secondary" android:layout_width="match_parent" android:layout_height="wrap_content" + android:text="@string/RegistrationLockFragment__switch_keyboard" + app:icon="@drawable/ic_keyboard_24" + app:iconGravity="textStart" + app:iconPadding="8dp" app:layout_constraintTop_toBottomOf="@id/pin_restore_forgot_pin" app:layout_constraintVertical_bias="0.0" - tools:layout_editor_absoluteX="32dp" - tools:text="Create Alphanumeric Pin" /> + tools:layout_editor_absoluteX="32dp" /> diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index 56ea30ac2f..340d09ca62 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -6,15 +6,6 @@ android:layout_height="match_parent" app:animateKeyboardChanges="true"> - - + + - - + + + + + + + + + app:argType="boolean" /> + + + @@ -724,7 +731,7 @@ + android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsFragment"> Kan nie video aflaai nie. Jy sal dit weer moet stuur. - gewysig %1$s + Gewysig %1$s Sluit aan by oproep @@ -883,6 +883,8 @@ Geen skakelvoorskou beskikbaar nie. Hierdie groepskakel is nie aktief nie. %1$s · %2$s + + Gebruik hierdie skakel om by \'n Signal-oproep aan te sluit @@ -1626,8 +1628,8 @@ Jy het %1$d pogings oor. As jou pogings opgebruik is, kan jy ’n nuwe PIN skep. Jy kan registreer en jou rekening gebruik, maar jy sal sekere gestoorde instellings soos jou profielinligting verloor. Signal-registrasie – Hulp nodig met PIN vir Android - Voer alfanumeriese PIN in - Voer numeriese PIN in + + Verander sleutelbord Skep jou PIN @@ -2314,7 +2316,7 @@ Om die oproep te beantwoord, gee Molly toegang tot jou mikrofoon. - To answer the video call, give Molly access to your microphone and camera. + Om die video-oproep te beantwoord, gee Molly toegang tot jou mikrofoon en kamera. Molly het Mikrofoon- en Kameratoestemmings nodig om oproepe te maak en te ontvang, maar dit is permanent geweier. Gaan asseblief na die toepassinginstellings, kies \"Toestemmings\" en aktiveer \"Mikrofoon\" en \"Kamera\". Op \'n gekoppelde toestel beantwoord. Op \'n gekoppelde toestel geweier. @@ -3443,7 +3445,9 @@ Volgende + Skep alfanumeriese PIN + Skep numeriese PIN @@ -3505,8 +3509,8 @@ Voer die PIN in wat jy vir jou rekening geskep het. Dit is anders as jou SMS-verifikasiekode. Sleutel die PIN in wat jy vir jou rekening geskep het. - Voer alfanumeriese PIN in - Voer numeriese PIN in + + Verander sleutelbord PIN verkeerd. Probeer weer. PIN vergeet? PIN verkeerd @@ -6018,14 +6022,12 @@ Gefiltreer volgens gemis Kies alles - + Skrap Skrap %1$d oproep? Skrap %1$d oproepe? - - Skrap vir my %1$d oproep geskrap diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 940deabc5a..762b4284d8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -995,6 +995,8 @@ استعراض الرابط عير متاح رابط هذه المجموعة غير مُفعَّل %1$s . %2$s + + اِستخدم هذا الرابط للانضمام إلى مكالمة Signal @@ -1862,8 +1864,8 @@ بقيت لديك %1$d محاولة. إذا انتهت محاولاتك، تستطيع إنشاء رقم تعريفي شخصي جديد. والتسجيل واستخدام حسابك لكنك ستفقد الإعدادات المحفوظة مثل معلومات حسابك الشخصي. التسجيل في سيجنال - تحتاج مساعدة بالرقم التعريفي الشخصي في أندرويد - أدخل الرقم التعريفي الشخصي بالأحرف والأرقام - أدخل الرقم التعريفي الشخصي بالأرقام + + تغيير لوحة المفاتيح إنشاء رقمك التعريفي الشخصي @@ -2594,7 +2596,7 @@ للإجابة على المكالمة، يجب عليك منح ترخيص الوصول إلى ميكروفونك. - To answer the video call, give Molly access to your microphone and camera. + للرّد على مكالمة الفيديو، يُرجى منح Molly ترخيص الوصول إلى الميكروفون و الكاميرا على هاتفك. يحتاج سيجنال إلى أذونات الميكروفون والكاميرا من أجل استقبال أو تلقّي المكالمات، ولكن الإذن لم يُمنح بشكل دائم. الرجاء زيارة إعدادات التطبيق، واختيار \"الأذونات\"، ثم تفعيل \"الميكروفون\" و\"الكاميرا\". أجاب عبر جهاز مرتبط. رُفض عبر جهاز مرتبط. @@ -3779,7 +3781,9 @@ التالي + إنشاء الرقم التعريفي الشخصي بالأحرف والأرقام + إنشاء الرقم التعريفي الشخصي بالأرقام @@ -3849,8 +3853,8 @@ عليك بإدخال الرقم التعريفي الشخصي الذي قمت بإنشائه لحسابك. إنه مختلف عن رمز التحقق الذي وصلك عبر رسالة قصيرة. أدخل رقم التعريف الشخصي الذي أنشأته لحسابك. - أدخل الرقم التعريفي الشخصي بالأحرف والأرقام - أدخل الرقم التعريفي الشخصي بالأرقام + + تغيير لوحة المفاتيح الرقم التعريفي الشخصي غير صحيح. يرجى إعادة المحاولة. هل نسيت الرقم التعريفي الشخصي؟ الرقم التعريفي الشخصي غير صحيح @@ -6514,7 +6518,7 @@ تمت التصفية حسب الفائتة تحديد الكل - + حذف حذف %1$d مكالمات؟ @@ -6524,8 +6528,6 @@ حذف %1$d مكالمة؟ حذف %1$d مكالمة؟ - - احذف بالنسبة لي %1$d مكالمات محذوفة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index de5fad9fcb..92d32504dc 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -883,6 +883,8 @@ Heç bir bağlantı önbaxışı yoxdur Qrup bağlantısı aktiv deyil %1$s · %2$s + + Signal Zənginə qoşulmaq üçün bu keçiddən istifadə edin @@ -1626,8 +1628,8 @@ %1$d cəhdiniz qaldı. Cəhdləriniz uğursuz olsa yeni PİN yarada bilərsiniz. Hesabı qeydiyyatdan keçirib istifadə edə bilərsiniz, lakin yadda saxlanılan tənzimləmələr və hesab məlumatı silinəcək. Signal Qeydiyyatı - Android üçün PIN ilə bağlı kömək lazımdır - Alfa nömrəli PIN daxil edin - Rəqəmli PIN daxil edin + + Klaviaturanı dəyiş PIN yaradın @@ -2314,7 +2316,7 @@ Zəngi cavablandırmaq üçün Molly-ın mikrofonunuza müraciətinə icazə verin. - To answer the video call, give Molly access to your microphone and camera. + Video zəngə cavab vermək üçün Molly-ın cihazınızın mikrofon və kamerasına girişinə icazə verin. Molly, zəng etmək və ya almaq üçün Mikrofon və Kamera icazələrini tələb edir, ancaq bu icazələr birdəfəlik rədd edilib. Zəhmət olmasa tətbiq tənzimləmələrində \"İcazələr\"i seçib \"Mikrofon\" və \"Kamera\"nı fəallaşdırın. Əlaqə yaradılmış cihazda cavablandı. Əlaqə yaradılmış cihazda rədd edildi. @@ -3443,7 +3445,9 @@ Növbəti + Alfa nömrəli PIN yaradın + Nömrəli PIN yaradın @@ -3505,8 +3509,8 @@ Hesabınız üçün yaratdığınız PIN-i daxil edin. Bu, SMS təsdiqləmə kodundan fərqlidir. Hesabınız üçün yaratdığınız PIN kodu daxil edin. - Alfa nömrəli PIN daxil edin - Rəqəmli PIN yazın + + Klaviaturanı dəyiş Yanlış PIN. Yenidən sınayın. PIN-i unutdunuz? Yanlış PIN @@ -6018,14 +6022,12 @@ Cavabsızlara görə filtrlənib Hamısını seç - + Sil %1$d zəng silinsin? %1$d zəng silinsin? - - Mənim üçün sil %1$d zəng silindi diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 74efe61ced..3fcba56be2 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -317,7 +317,7 @@ Неуспешно изтегляне на видеото. Ще трябва да го изпратите отново. - редактирано %1$s + Редактирано %1$s Присъединете се към разговор @@ -883,6 +883,8 @@ Няма наличен преглед на връзката Тази групова връзка не е активна %1$s · %2$s + + Използвайте този линк, за да се присъедините към повикване в Signal @@ -1626,8 +1628,8 @@ Остават %1$d опити. Ако свършите опитите си, можете да създадете нов ПИН. Можете да се регистрирате и да използвате акаунта си, но ще загубите някои запазени настройки, като информация за вашия профил. Регистрация за Signal - Необходима помощ за ПИН за Android - Въведете буквено-цифрен ПИН - Въведете ПИН от цифри + + Превключване на клавиатурата Създаване на ПИН @@ -2314,7 +2316,7 @@ За да отговорите на повикването, дайте на Molly достъп до микрофона си. - To answer the video call, give Molly access to your microphone and camera. + За да отговорите на видео повикването, дайте достъп на Molly до микрофона и камерата ви. Molly се нуждае от достъп до микрофона и камерта Ви, за да може да получава обаждания, но той му е отказан. Моля, отидете на настройки в менюто и изберете \"Разрешения\", \"Микрофон\" и \"Камера\". Отговорено на свързано устройство. Отказано на свързано устройство. @@ -3443,7 +3445,9 @@ Следващ + Създайте буквено-цифров ПИН + Създайте цифров ПИН @@ -3505,8 +3509,8 @@ Въведете ПИН кода, който сте създали за своя акаунт. Той е различен от кода за потвърждение през SMS. Въведете ПИН кода, който сте създали за акаунта си. - Въведете ПИН от букви и цифри - Въведете ПИН от цифри + + Превключване на клавиатурата Грешен ПИН. Опитайте отново. Забравен ПИН? Грешен ПИН @@ -6018,14 +6022,12 @@ Филтриране по пропуснати Избиране на всичко - + Изтриване Изтриване на %1$d обаждане? Изтриване на %1$d обаждания? - - Изтриване за мен %1$d изтрито повикване diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index e0cb0bf4f8..d7e8326029 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -883,6 +883,8 @@ লিংকের প্রিভিউ উপলব্ধ নেই এই গ্রুপ লিংকটি সচল নেই %1$s · %2$s + + Signal কলে যোগ দিতে এই লিংকটি ব্যবহার করুন @@ -1626,8 +1628,8 @@ আপনার %1$d টি প্রচেষ্টা বাকী আছে। আপনার প্রচেষ্টার সীমা শেষ হয়ে গেলে, আপনি একটি নতুন পিন তৈরি করতে পারবেন। আপনি আপনার অ্যাকাউন্ট রেজিস্টার করে ব্যবহার করতে পারবেন কিন্তু আপনি আপনার প্রোফাইলের তথ্যের মতো কিছু সেভ করা সেটিংস হারাবেন। Signal নিবন্ধন - অ্যানড্রয়েড এর জন্য PIN এর ব্যাপারে সাহায্য প্রয়োজন - বর্ণানুক্রমিক পিন প্রবেশ করান - সংখ্যার পিন প্রবেশ করান + + কীবোর্ড পাল্টান আপনার পিন তৈরি করুন @@ -2314,7 +2316,7 @@ কলের উত্তর দিতে, Molly-কে আপনার মাইক্রোফোন অ্যাক্সেস দিন। - To answer the video call, give Molly access to your microphone and camera. + ভিডিও কলের উত্তর দিতে, Molly-কে আপনার মাইক্রোফোন এবং ক্যামেরায় অ্যাক্সেস দিন। কল সমূহ করতে বা গ্রহণের জন্য Molly এর মাইক্রোফোন এবং ক্যামেরা ব্যবহারের অনুমতি প্রয়োজন, তবে সেগুলি স্থায়ীভাবে অস্বীকার করা হয়েছে। দয়া করে অ্যাপ্লিকেশন সেটিংসে যান এবং \"অনুমতিগুলি\" নির্বাচন করুন এবং \"মাইক্রোফোন\" এবং \"ক্যামেরা\" সক্ষম করুন | সংযুক্ত ডিভাইসে উত্তর দেওয়া হয়েছে। সংযুক্ত ডিভাইসে অস্বীকার করা হয়েছে। @@ -3443,7 +3445,9 @@ পরবর্তী + বর্ণানুক্রমিক পিন তৈরি করুন + সংখ্যার পিন তৈরি করুন @@ -3505,8 +3509,8 @@ আপনি নিজের অ্যাকাউন্ট এর জন্য তৈরি করা পিনটি প্রবেশ করুন। এটি আপনার এসএমএস যাচাইকরণ কোড থেকে ভিন্ন। আপনার অ্যাকাউন্টের জন্য তৈরি করা পিনটি লিখুন। - বর্ণানুক্রমিক পিন প্রবেশ করান - সংখ্যার পিন প্রবেশ করান + + কীবোর্ড পাল্টান ভুল পিন। আবার চেষ্টা করুন| পিন ভুলে গেছেন? ভুল পিন @@ -6018,14 +6022,12 @@ \'মিসড কল\' অনুযায়ী ফিল্টার করা হয়েছে সবগুলো নির্বাচন করুন - + মুছে ফেলুন %1$d কল মুছবেন? %1$d কল মুছবেন? - - আমার জন্য মুছে ফেলুন %1$dটি কল মুছে ফেলা হয়েছে diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index f9cdc423d3..8f8e099b6c 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -319,7 +319,7 @@ Nije moguće preuzeti videozapis. Morat ćete ga poslati ponovo. - uređeno %1$s + Uređeno %1$s Pridruži se pozivu @@ -939,6 +939,8 @@ Pregled linka nije dostupan Ovaj link za grupu nije aktivan %1$s · %2$s + + Koristite ovu poveznicu da se pridružite pozivu na Signalu @@ -1744,8 +1746,8 @@ Preostalo vam je %1$d pokušaja. Ako svi vaši pokušaji budu neuspješni, možete kreirati novi PIN. Moći ćete se registrovati i koristiti svoj račun, ali će neke postavke računa, poput informacija o profilu, biti poništene. Signal registracija – trebate pomoć u vezi s PIN-om za Android - Unesite alfanumerički PIN - Unesite numerički PIN + + Promijeni tastaturu Kreirajte svoj PIN @@ -2454,7 +2456,7 @@ Da biste odgovorili na poziv, dozvolite Mollyu pristup Vašem mikrofonu. - To answer the video call, give Molly access to your microphone and camera. + Da odgovorite na video poziv, dozvolite Mollyu pristup mikrofonu i kameri. Mollyu je potrebno dopuštenje da pristupi mikrofonu i kameri kako bi mogao slati i primati pozive, ali je ono trajno uskraćeno. Molimo nastavite do postavki aplikacije, odaberite \"Dozvole\" i aktivirajte stavke \"Mikrofon\" i \"Kamera\". Odgovoreno na povezanom uređaju. Odbijeno na povezanom uređaju. @@ -3611,7 +3613,9 @@ Dalje + Kreiraj alfanumerički PIN + Kreiraj numerički PIN @@ -3677,8 +3681,8 @@ Unesite PIN koji ste kreirali za svoj račun. PIN nije isto što i SMS kōd za verifikaciju. Unesite PIN koji ste kreirali za svoj račun. - Unesite alfanumerički PIN - Unesite numerički PIN + + Promijeni tastaturu Netačan PIN. Pokušajte ponovo. Zaboravili ste PIN? Netačan PIN @@ -6266,7 +6270,7 @@ Filtrirano prema propuštenim Odaberi sve - + Izbriši Izbrisati %1$d poziv? @@ -6274,8 +6278,6 @@ Izbrisati %1$d poziva? Izbrisati %1$d poziva? - - Izbriši za mene %1$d poziv je izbrisan diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 9b76d2990c..afae5618b7 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -317,7 +317,7 @@ No es pot descarregar el vídeo. Hauràs de tornar-lo a enviar. - editat %1$s + Editat fa %1$s Unir-me a la trucada @@ -883,6 +883,8 @@ No hi ha previsualització d\'enllaç disponible. Aquest enllaç de grup no està actiu. %1$s · %2$s + + Utilitzar aquest enllaç per a unir-te a la trucada de Signal @@ -1626,8 +1628,8 @@ Us resten %1$d intents. Si us quedeu sense intents, podeu crear un PIN nou. Podeu registrar-vos i usar el compte, però perdreu algunes configuracions desades, com ara la informació del perfil. Registre del Signal - Cal ajuda amb el PIN per a Android - Escriviu un PIN alfanumèric - Marqueu un PIN numèric + + Canviar teclat Creeu el PIN @@ -2314,7 +2316,7 @@ Per respondre la trucada, permeteu que el Molly accedeixi al micròfon. - To answer the video call, give Molly access to your microphone and camera. + Per respondre la videotrucada, permet que Molly accedeixi al micròfon i a la càmera. El Molly necessita el permís del micròfon i de la càmera per tal de fer o rebre trucades, però s\'han denegat permanentment. Si us plau, continueu cap al menú de configuració de l\'aplicació, seleccioneu Permisos i activeu-hi el micròfon i la càmera. S\'ha respost en un dispositiu enllaçat. S\'ha rebutjat en un dispositiu enllaçat. @@ -3443,7 +3445,9 @@ Següent + Creeu un PIN alfanumèric + Creeu un PIN numèric @@ -3505,8 +3509,8 @@ Marqueu el PIN que heu creat per al compte. Això és diferent del codi de verificació d\'SMS. Introdueix el PIN que has creat per al teu compte. - Escriviu un PIN alfanumèric - Marqueu un PIN numèric + + Canviar teclat PIN incorrecte. Torneu-ho a provar. Heu oblidat el PIN? PIN incorrecte @@ -6018,14 +6022,12 @@ Filtrat per perdudes Seleccionar-ho tot - + Eliminar Eliminar %1$d trucada? Eliminar %1$d trucades? - - Eliminar per a mi %1$d trucada eliminada diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8bb9a1a7ee..136c784c17 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -319,7 +319,7 @@ Video nelze stáhnout. Budete ho muset poslat znovu. - upraveno %1$s + Upraveno %1$s Připojit se k hovoru @@ -939,6 +939,8 @@ Není dostupný žádný náhled odkazu Tento odkaz na skupinu není aktivní %1$s . %2$s + + Pomocí tohoto odkazu se můžete připojit k hovoru přes Signal @@ -1744,8 +1746,8 @@ Zbývá vám %1$d pokusů. Pokud vám pokusy dojdou, můžete si vytvořit nový PIN. Můžete se zaregistrovat a používat svůj účet, ale ztratíte některá uložená nastavení, jako jsou vaše profilové informace. Registrace Signal - Potřebuji pomoc s PIN pro Android - Zadejte alfanumerický PIN - Zadejte číselný PIN + + Přepnout klávesnici Vytvořit váš PIN @@ -2454,7 +2456,7 @@ Chcete-li přijmout hovor, umožněte aplikaci Molly přístup k mikrofonu. - To answer the video call, give Molly access to your microphone and camera. + Chcete-li přijmout videohovor, umožněte aplikaci Molly přístup k mikrofonu a fotoaparátu. Molly potřebuje oprávnění pro přístup k mikrofonu a fotoaparátu, abyste mohli volat nebo přijímat hovory, ale tato oprávnění jsou nyní zakázána. Prosím pokračujte do menu nastavení aplikací, vyberte \"Oprávnění\" a povolte \"Mikrofon\" a \"Fotoaparát\". Odpovězeno na propojeném zařízení. Odmítnuto na propojeném zařízení. @@ -3611,7 +3613,9 @@ Další + Vytvořit alfanumerický PIN + Vytvořit číselný PIN @@ -3677,8 +3681,8 @@ Zadejte PIN, který jste vytvořili pro váš účet. Je to jiný kód, než ten v ověřovací SMS. Zadejte PIN, který jste si vytvořili pro svůj účet. - Zadejte alfanumerický PIN - Zadejte číselný PIN + + Přepnout klávesnici Nesprávný PIN. Zkuste to znovu. Zapomněli jste PIN? Nesprávný PIN @@ -6266,7 +6270,7 @@ Filtrovat podle zmeškaných hovorů Vybrat vše - + Smazat Odstranit %1$d hovor? @@ -6274,8 +6278,6 @@ Odstranit %1$d hovorů? Odstranit %1$d hovorů? - - Smazat u mě %1$d smazaný hovor diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2abd06d4b7..83a0aa0389 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -317,7 +317,7 @@ Videoen kan ikke downloades. Du skal sende den igen. - redigeret %1$s + Redigeret %1$s Deltag i opkald @@ -883,6 +883,8 @@ Ingen forhåndsvisning af link tilgængelig Dette gruppelink er ikke aktivt %1$s - %2$s + + Brug dette link til at deltage i et Signal-opkald @@ -1626,8 +1628,8 @@ Du har %1$d forsøg tilbage. Hvis du løber tør for forsøg, kan du oprette en ny pinkode. Du kan registrere og bruge din konto, men du mister nogle gemte indstillinger som dine profiloplysninger. Signal registrering - Brug for hjælp med pinkode til Android - Indtast alfanumerisk pinkode - Indtast numerisk pinkode + + Skift tastatur Opret din pinkode @@ -2314,7 +2316,7 @@ Hvis du vil besvare opkaldet, skal du give Molly adgang til din mikrofon. - To answer the video call, give Molly access to your microphone and camera. + For at besvare videoopkaldet skal du give Molly adgang til din mikrofon og dit kamera. Molly beder om tilladelse til at tilgå mikrofon og kamera, for at kunne modtage og foretage opkald, hvilket er blevet nægtet. Gå venligst til appindstillinger, vælg \"Tilladelser\" og tilvælg \"Mikrofon\" og \"Kamera\". Besvaret på en forbundet enhed. Afvist på en forbundet enhed. @@ -3443,7 +3445,9 @@ Næste + Generér alfanumerisk pinkode + Generér numerisk pinkode @@ -3505,8 +3509,8 @@ Indtast den pinkode du oprettede til din konto. Den er forskellig fra din SMS-verifikationskode. Angiv den pinkode, du har oprettet til din konto. - Indtast alfanumerisk pinkode - Indtast numerisk pinkode + + Skift tastatur Forkert pinkode. Prøv igen. Glemt pinkode? Forkert pinkode @@ -6018,14 +6022,12 @@ Filtreret efter ubesvaret Vælg alle - + Slet Slet %1$d opkald? Slet %1$d opkald? - - Slet for mig %1$d opkald slettet diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9d72e014e5..1d8b198c13 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -883,6 +883,8 @@ Keine Link-Vorschau verfügbar Dieser Gruppen-Link ist nicht aktiv %1$s · %2$s + + Über diesen Link kannst du an einem Signal-Anruf teilnehmen @@ -1626,8 +1628,8 @@ Du hast %1$d verbleibende Versuche. Falls du all deine Versuche aufbrauchst, kannst du eine neue PIN erstellen. Du kannst dich registrieren und dein Nutzerkonto verwenden, wirst aber einige gespeicherte Einstellungen, wie z. B. deine Profilinformationen verlieren. Signal-Registrierung – Benötige Hilfe zur PIN für Android - Alphanumerische PIN eingeben - Numerische PIN eingeben + + Tastatur umschalten Erstelle deine PIN @@ -2314,7 +2316,7 @@ Zum Annehmen des Anrufs benötigt Molly Zugriff auf dein Mikrofon. - To answer the video call, give Molly access to your microphone and camera. + Um den Videoanruf zu beantworten, musst du Molly Zugriff auf dein Mikrofon und deine Kamera geben. Molly benötigt für Anrufe die Berechtigungen »Mikrofon« und »Kamera«, diese wurden jedoch dauerhaft abgelehnt. Bitte öffne die App-Einstellungen, wähle »Berechtigungen« und aktiviere »Mikrofon« und »Kamera«. Auf einem gekoppelten Gerät angenommen. Auf einem gekoppelten Gerät abgelehnt. @@ -3443,7 +3445,9 @@ Weiter + Alphanumerische PIN erstellen + Numerische PIN erstellen @@ -3505,8 +3509,8 @@ Gib die PIN ein, die du für dein Konto erstellt hast. Sie unterscheidet sich von deinem SMS-Verifikationscode. Gib die PIN ein, die du für dein Konto festgelegt hast. - Alphanumerische PIN eingeben - Numerische PIN eingeben + + Tastatur umschalten Falsche PIN. Bitte versuche es erneut. PIN vergessen? Falsche PIN @@ -6018,14 +6022,12 @@ Gefiltert nach Entgangen Alle auswählen - + Löschen %1$d Anruf löschen? %1$d Anrufe löschen? - - Für mich löschen %1$d Anruf gelöscht diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e308aa0b48..ff0ed55545 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -317,7 +317,7 @@ Η λήψη του βίντεο απέτυχε. Πρέπει να το στείλεις ξανά. - επεξεργασία %1$s + Έγινε επεξεργασία %1$s Είσοδος στην κλήση @@ -883,6 +883,8 @@ Δεν υπάρχει διαθέσιμη προεπισκόπιση συνδέσμου Ο σύνδεσμος της ομάδας δεν είναι ενεργός %1$s · %2$s + + Χρησιμοποίησε αυτόν τον σύνδεσμο για να μπεις σε μία Κλήση Signal @@ -1626,8 +1628,8 @@ Έχεις ακόμα %1$d προσπάθειες. Αν τις εξαντλήσεις, θα πρέπει να δημιουργήσεις ένα νέο PIN. Θα έχεις τη δυνατότητα να εγγραφτείς και να χρησιμοποιήσεις τον λογαριασμό σου, αλλά θα χάσεις κάποιες αποθηκευμένες ρυθμίσεις όπως οι πληροφορίες προφίλ σου. Εγγραφή στο Signal - Βοήθεια με το PIN σε Android - Εισαγωγή αλφαριθμητικού PIN - Εισαγωγή αριθμητικού PIN + + Εναλλαγή πληκτρολογίου Δημιούργησε το PIN σου @@ -2314,7 +2316,7 @@ Για να απαντήσεις στη κλήση, δώσε στο Molly πρόσβαση στο μικρόφωνο. - To answer the video call, give Molly access to your microphone and camera. + Για να απαντήσεις στη βιντεοκλήση, δώσε στο Molly πρόσβαση στο μικρόφωνο και στην κάμερα. Το Molly χρειάζεται τα δικαιώματα Μικροφώνου και Κάμερας για την πραγματοποίηση κλήσεων, αλλά αυτά δεν έχουν δοθεί μόνιμα. Παρακαλώ πήγαινε στις ρυθμίσεις εφαρμογών, επέλεξε τα \"Δικαιώματα\", και ενεργοποίησε το \"Μικρόφωνο\" και \"Κάμερα\". Απαντήθηκε από συνδεμένη συσκευή. Απορρίφθηκε απο συνδεμένη συσκευή. @@ -3443,7 +3445,9 @@ Επόμενο + Δημιουργία αλφαριθμητικού PIN + Δημιουργία αριθμητικού PIN @@ -3505,8 +3509,8 @@ Γράψε το PIN που δημιούργησες για το λογαριασμό σου. Αυτό είναι διαφορετικό από τον κωδικό επαλήθευσης που έλαβες μέσω SMS. Εισήγαγε το PIN που δημιούργησες για τον λογαριασμό σου. - Εισαγωγή αλφαριθμητικού PIN - Εισαγωγή αριθμητικού PIN + + Εναλλαγή πληκτρολογίου Λάθος PIN. Ξαναδοκίμασε. Ξέχασες το PIN; Λάθος ΡΙΝ @@ -6018,14 +6022,12 @@ Φιλτράρισμα ανά αναπάντητες Επιλογή όλων - + Διαγραφή Διαγραφή %1$d κλήσης; Διαγραφή %1$d κλήσεων; - - Διαγραφή για μένα %1$d κλήση διαγράφηκε diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8d128cb7b4..a579e74e32 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -883,6 +883,8 @@ Vista previa no disponible El enlace al grupo no está disponible %1$s · %2$s + + Usar este enlace para unirse a llamada de Signal @@ -1626,8 +1628,8 @@ Te quedan %1$d intentos. Cuando los agotes, podrás crear un nuevo PIN. Podrás registrar y usar tu cuenta pero perderás tu perfil, contactos, grupos y personas bloqueadas. Registro en Signal - Ayuda con el PIN en Android - Introducir PIN alfanumérico - Introducir PIN numérico + + Cambiar teclado Crea tu PIN @@ -2314,7 +2316,7 @@ Para atender la llamada, permite a Molly el acceso al micrófono. - To answer the video call, give Molly access to your microphone and camera. + Para atender la videollamada, permite a Molly el acceso al micrófono y a la cámara. Molly necesita acceso al micrófono y cámara para hacer o atender llamadas. Por favor, ve a la aplicación «Ajustes», selecciona Molly en el menú «Aplicaciones y notificaciones» y en «Permisos» activa «Micrófono» y «Cámara». Atendida en dispositivo enlazado. Rechazada en dispositivo enlazado. @@ -3443,7 +3445,9 @@ Siguiente + Crear PIN alfanumérico + Crear PIN numérico @@ -3505,8 +3509,8 @@ Introduce el PIN que has seleccionado al crear tu cuenta de Signal. El PIN es diferente al del SMS de verificación. Introduce el PIN que creaste para tu cuenta. - Introduce PIN alfanumérico - Introduce PIN numérico + + Cambiar teclado PIN incorrecto. Inténtalo de nuevo. ¿No recuerdas el PIN? PIN incorrecto @@ -6018,14 +6022,12 @@ Filtrar por perdidas Seleccionar todo - + Eliminar ¿Eliminar %1$d llamada? ¿Eliminar %1$d llamadas? - - Eliminar solo para mí %1$d llamada eliminada diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 2782f3bff6..bb73e34ffd 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -317,7 +317,7 @@ Videot ei saa alla laadida. Pead selle uuesti saatma. - muudetud %1$s eest + Muudetud %1$s eest Liitu kõnega @@ -883,6 +883,8 @@ Lingieelvaade puudub See grupilink ei ole aktiivne %1$s · %2$s + + Kasuta Signali kõnega liitumiseks seda linki @@ -1626,8 +1628,8 @@ Sul on jäänud %1$d katsetust. Kui katsed otsa saavad, saad luua uue PIN-koodi. Saad oma kontot registreerida ja kasutada, kuid kaotad mõned salvestatud seaded, näiteks oma profiiliteabe. Signali registreerimine - vajad abi Androidi PIN-koodiga - Sisesta tähtnumbriline PIN-kood - Sisesta numbriline PIN-kood + + Vaheta klaviatuuri Loo enda PIN-kood @@ -2314,7 +2316,7 @@ Kõnele vastamiseks anna Mollyile juurdepääs mikrofonile. - To answer the video call, give Molly access to your microphone and camera. + Videokõnele vastamiseks anna Mollyile juurdepääs oma mikrofonile ja kaamerale. Molly vajab mikrofoni ja kaamera lubasid, et teha ja vastu võtta kõnesid, ent need on püsivalt keelatud. Palun jätka rakenduse seadetes, vali \"Load\" ja luba \"Mikrofon\" ning \"Kaamera\". Vastatud ühendatud seadmes. Keeldutud ühendatud seadmes. @@ -3344,7 +3346,7 @@ Lisa avakuvale Loo mull - Format text + Vorminda teksti Laienda hüpikut @@ -3443,7 +3445,9 @@ Edasi + Loo tähtnumbriline PIN-kood + Loo numbriline PIN-kood @@ -3505,8 +3509,8 @@ Sisesta PIN-kood, mille tegid enda konto jaoks. See erineb SMS-kontrollkoodist. Sisesta PIN-kood, mille oled oma konto jaoks loonud. - Sisesta tähtnumbriline PIN-kood - Sisesta numbriline PIN-kood + + Vaheta klaviatuuri Lubamatu PIN-kood. Proovi uuesti. Unustasid PIN-koodi? Sobimatu PIN-kood @@ -5920,7 +5924,7 @@ Spoiler - Clear formatting + Eemalda vormindus @@ -6018,14 +6022,12 @@ Filtreeritud vastamata kõnede alusel Vali kõik - + Kustuta Kas kustutada %1$d kõne? Kas kustutada %1$d kõnet? - - Kustuta minu jaoks %1$d kõne kustutatud diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e0105cdc50..dfc695c46d 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -317,7 +317,7 @@ Ezin da deskargatu bideoa. Berriro bidali beharko duzu. - editatuta (%1$s) + Editatuta (%1$s) Deian sartu @@ -883,6 +883,8 @@ Ez dago estekaren aurrebistarik Talde esteka hau ez dago indarrean %1$s · %2$s + + Erabili esteka hau Signal-eko dei batean sartzeko @@ -1626,8 +1628,8 @@ %1$d saiakera falta zaizkizu. Saiakerarik gabe gelditzen bazara, PIN berri bat sortu dezakezu. Zure kontua erregistratu eta erabili dezakezu, baina gordetako ezarpen batzuk galduko dituzu; esate baterako, zure profilaren informazioa. Signal Erregistratzea - Laguntza Android PIN-erako. - Idatzi PIN alfanumerikoa - Idatzi zenbakizko PINa + + Aldatu teklatua Sortu zure PINa @@ -2314,7 +2316,7 @@ Deiari erantzuteko, eman Molly-i mikrofonoa atzitzeko baimena. - To answer the video call, give Molly access to your microphone and camera. + Bideo-deiari erantzuteko, eman Molly-i mikrofonorako eta kamerarako sarbidea. Mollyek Mikronofoa eta Kamera baimenak behar ditu deiak egin edo jasotzeko baina ukatu egin dizkiozu. Joan aplikazioaren ezarpenetara, aukeratu \"Baimenak\" eta aktibatu \"Mikrofonoa\" eta \"Kamera\" Erantzunda lotutako gailu batean. Ukatuta lotutako gailu batean. @@ -3344,7 +3346,7 @@ Gehitu hasiera pantailara Sortu burbuila - Format text + Formateatu testua Zabaldu leihoa @@ -3443,7 +3445,9 @@ Hurrengoa + PIN alfanumerikoa sortu + Zenbakizko PINa sortu @@ -3505,8 +3509,8 @@ Idatzi konturako sortu duzun PIN-a. PIN hau ez da zure SMS berifikazio gakoa. Idatzi zure konturako sortu duzun PINa. - Idatzi PIN alfanumerikoa - Idatzi zenbakizko PINa + + Aldatu teklatua PIN okerra. Saia zaitez berriro. PINa ahaztu al duzu? PIN okerra @@ -5920,7 +5924,7 @@ Spoiler-a - Clear formatting + Garbitu formatua @@ -6018,14 +6022,12 @@ Galduen arabera iragazita Dena hautatu - + Ezabatu %1$d dei ezabatu nahi duzu? %1$d dei ezabatu nahi dituzu? - - Ezabatu niretzat Dei %1$d ezabatu da diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2bbf05b1b7..b919cdc9d8 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -883,6 +883,8 @@ هیچ پیش‌نمایش پیوند‌ی در دسترس نیست این پیوند گروه فعال نیست %1$s · %2$s + + از این پیوند برای پیوستن به یک تماس سیگنال استفاده کنید @@ -1626,8 +1628,8 @@ شما %1$d فرصت دیگر باقی دارید. اگر فرصت‌های شما تمام شوند، می‌توانید یک پین جدید بسازید. شما می‌توانید ثبت‌نام و از حساب کاربری خود استفاده کنید ولی برخی تنظیمات ذخیره‌شده مانند اطلاعات پروفایل خود را از دست می‌دهید. ثبت‌نام سیگنال - نیاز به راهنمایی در مورد پین اندروید - وارد کردن پین حرفی‌عددی - پین عددی را وارد کنید + + تغییر صفحه‌کلید پین خود را ایجاد کنید @@ -2314,7 +2316,7 @@ برای پاسخ به تماس، به سیگنال دسترسی به میکروفون خود را بدهید. - To answer the video call, give Molly access to your microphone and camera. + برای پاسخ به تماس تصویری، دسترسی به میکروفون و دوربین را به سیگنال بدهید. سیگنال برای برقراری و دریافت تماس نیاز مجوزها‌ی میکروفون و دوربین دارد، اما دسترسی به آن‌ها به طور دائم رد شده است. لطفاً به تنظیمات برنامه رفته، در قسمت «مجوزها»، «میکروفون» و «دوربین» را فعال کنید. برروی دستگاه پیوند داده شده پاسخ داده شد. بر روی دستگاه پیوند داده شده رد شد. @@ -3443,7 +3445,9 @@ بعدی + ایجاد پین حرفی‌عددی + ایجاد پین عددی @@ -3505,8 +3509,8 @@ پین ایجاد شده برای حساب کاربری خود را وارد کنید. این کد با کد وارسی پیامکی شما تفاوت دارد. پینی که برای حسابتان ساخته‌اید را وارد کنید. - وارد کردن پین حرفی‌عددی - پین عددی را وارد کنید + + تغییر صفحه‌کلید پین نادرست. دوباره تلاش کنید. پین را فراموش کرده‌اید؟ پین نادرست @@ -6018,14 +6022,12 @@ فیلترشده بر اساس ازدست‌رفته انتخاب همه - + پاک کردن %1$d تماس پاک شود؟ %1$d تماس پاک شود؟ - - پاک کردن برای من %1$d تماس پاک شد diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 25081bb06d..33712a614d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -317,7 +317,7 @@ Videota ei voi ladata. Sinun on lähetettävä se uudelleen. - muokattu %1$s + Muokattu %1$s Liity puheluun @@ -883,6 +883,8 @@ Linkin esikatselu ei ole saatavilla Tämä ryhmälinkki ei ole aktiivinen %1$s · %2$s + + Liity Signal-puheluun tämän linkin avulla @@ -1626,8 +1628,8 @@ Sinulla on %1$d yritystä jäljellä. Jos yritykset loppuvat, voit luoda uuden tunnusluvun. Voit rekisteröidä ja käyttää tiliäsi, mutta menetät tallennetut asetukset, kuten profiilitietosi. Signalin rekisteröinti - Tarvitsen apua tunnusluvun kanssa Androidilla - Syötä aakkosnumeerinen tunnusluku - Syötä numeerinen tunnusluku + + Vaihda näppäimistöä Valitse tunnusluku @@ -2314,7 +2316,7 @@ Jotta voit vastata puheluun, anna Mollyille lupa käyttää mikrofonia. - To answer the video call, give Molly access to your microphone and camera. + Anna Mollyille mikrofonin ja kameran käyttöoikeus, jotta voit vastata videopuheluun. Molly tarvitsee luvan käyttää mikrofonia ja kameraa puheluiden soittamista ja vastaanottamista varten, mutta nämä käyttöoikeudet ovat pysyvästi evätty Mollyilta. Voit muuttaa tätä menemällä sovellusten asetuksiin, valitsemalla \"Sovelluksen käyttöoikeudet\" ja laittamalla päälle \"Mikrofoni\" ja \"Kamera\". Vastattu yhdistetyllä laitteella. Hylätty yhdistetyllä laitteella. @@ -3344,7 +3346,7 @@ Lisätty kotinäytölle Luo kupla - Format text + Muotoile tekstiä Suurenna ponnahdusikkuna @@ -3443,7 +3445,9 @@ Seuraava + Luo aakkosnumeerinen tunnusluku + Luo numeerinen tunnusluku @@ -3505,8 +3509,8 @@ Syötä tilille valitsemasi tunnusluku. Se ei ole sama kuin saamasi tekstiviestivahvistuskoodi. Kirjoita PIN-koodi, jonka loit tiliäsi varten. - Syötä aakkosnumeerinen tunnusluku - Syötä numeerinen tunnusluku + + Vaihda näppäimistöä Väärä tunnusluku, yritä uudelleen. Unohditko tunnusluvun? Väärä tunnusluku @@ -5920,7 +5924,7 @@ Sumennus - Clear formatting + Poista muotoilu @@ -6018,14 +6022,12 @@ Suodatusperuste: Vastaamattomat Valitse kaikki - + Poista Poistetaanko %1$d puhelu? Poistetaanko %1$d puhelua? - - Poista minulta %1$d puhelu poistettu diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0adaa7c54f..58300c5e5c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -883,6 +883,8 @@ Aucun aperçu du lien n’est proposé Ce lien de groupe est inactif %1$s · %2$s + + Utiliser ce lien pour rejoindre un appel Signal @@ -1626,8 +1628,8 @@ Il vous reste %1$d essais. Si vous épuisez le nombre limite d’essais, vous pouvez créer un nouveau PIN. Vous pouvez vous inscrire et utiliser votre compte, mais vous perdrez certains paramètres enregistrés tels que les informations de votre profil. Inscription à Signal – Besoin d’aide avec le NIP sur Android - Saisir un NIP alphanumérique - Saisir un NIP numérique + + Changer de clavier Créer votre NIP @@ -2314,7 +2316,7 @@ Pour répondre à l’appel, accordez à Molly l’accès à votre microphone. - To answer the video call, give Molly access to your microphone and camera. + Pour répondre à l’appel vidéo, veuillez autoriser Molly à accéder à votre micro et votre appareil photo. Molly exige les autorisations Microphone et Appareil photo afin d’effectuer et de recevoir des appels, mais elles ont été refusées définitivement. Veuillez accéder au menu des paramètres des applis, sélectionner « Autorisations » et activer « Microphone » et « Appareil photo ». L’appel a été pris sur un appareil relié. L’appel a été refusé sur un appareil relié. @@ -3443,7 +3445,9 @@ Suivant + Créer un NIP alphanumérique + Créer un NIP numérique @@ -3505,8 +3509,8 @@ Saisissez le NIP que vous avez créé pour votre compte. Il est différent de votre code de confirmation reçu par texto. Saisissez le PIN choisi pour votre compte. - Saisissez un NIP alphanumérique - Saisissez un NIP numérique + + Changer de clavier Le NIP est erroné. Veuillez réessayer. Avez-vous oublié votre NIP ? Le NIP est erroné @@ -6018,14 +6022,12 @@ Appel manqué filtré Tout sélectionner - + Supprimer Supprimer %1$d appel ? Supprimer %1$d appels ? - - Supprimer pour moi %1$d appel supprimé diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index d20f82606d..17075cd383 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -320,7 +320,7 @@ Ní féidir an físeán a íoslódáil. Beidh ort é a sheoladh arís. - curtha in eagar %1$s + Curtha in eagar %1$s Téigh isteach sa ghlao @@ -967,6 +967,8 @@ Níl réamhamharc ar an nasc ar fáil Níl an nasc chuig an mbaicle sin gníomhach %1$s · %2$s + + Úsáid an nasc seo le dul isteach i nGlao Signal @@ -1803,8 +1805,8 @@ Tá %1$d iarracht fágtha agat. Má úsáideann tú gach iarracht, is féidir leat UAP nua a chruthú. Is féidir leat do chuntas a chlárú agus a úsáid ach caillfidh tú roinnt socruithe sábháilte amhail faisnéis faoi do phróifíl. Clárúchán Signal — UAP ar Android - Cuir isteach UAP alfa-uimhriúil - Cuir isteach UAP uimhriúil + + Aistrigh eochairchlár Cruthaigh d\'UAP @@ -2524,7 +2526,7 @@ Chun an glao a fhreagairt, tabhair rochtain do Molly ar do mhicreafón. - To answer the video call, give Molly access to your microphone and camera. + Tabhair rochtain do Molly ar do mhicreafón agus ceamara leis an bhfísghlao a fhreagairt. Tá gá ag Molly le ceadanna micreafóin agus cheamara chun glaonna a chuir nó a fháil, ach ní ceadaítear iad go deo. Lean ar aghaidh, le do thoil, go socruithe aipe, roghnaigh \"Ceadanna\", agus cumasaigh \"Micreafón\" agus \"Ceamara\". Freagartha ar ghléas nasctha. Diúltaithe dó ar ghléas nasctha. @@ -3695,7 +3697,9 @@ Ar Aghaidh + Cruthaigh UAP alfa-uimhriúil + Cruthaigh UAP uimhriúil @@ -3763,8 +3767,8 @@ Cuir isteach an UAP a chruthaigh tú don chuntas seo. Ní ionann é seo agus an cód deimhniúcháin SMS. Cuir isteach an UAP a chruthaigh tú do do chuntas. - Cuir isteach UAP alfa-uimhriúil - Cuir isteach UAP uimhriúil + + Aistrigh eochairchlár UAP mhícheart. Bain triail eile as. Ar dhearmad tú do PIN? UAP mhícheart @@ -6390,7 +6394,7 @@ Scagtha de réir caillte Roghnaigh gach - + Scrios Scrios %1$d ghlao? @@ -6399,8 +6403,6 @@ Scrios %1$d nglao? Scrios %1$d glao? - - Scrios domsa %1$d ghlao scriosta diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index fdf2aab2d7..6af49e1a9b 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -317,7 +317,7 @@ Non se pode descargar o vídeo. Terás que reenvialo. - editada hai %1$s + Editada hai %1$s Unirse á chamada @@ -883,6 +883,8 @@ Vista previa non dispoñible A ligazón deste grupo non está activa %1$s · %2$s + + Emprega esta ligazón para unirte a unha chamada de Signal @@ -1626,8 +1628,8 @@ Quédache %1$d intentos. Se os esgotas, podes crear un novo PIN. Podes rexistrar e usar a túa conta pero perderás algúns axustes gardados como a información do perfil. Rexistro en Signal - Preciso Axuda co PIN para Android - Escribe PIN alfanumérico - Escribe PIN numérico + + Cambiar teclado Crea o teu PIN @@ -2314,7 +2316,7 @@ Para responder a chamada, permite que Molly acceda ao teu micrófono. - To answer the video call, give Molly access to your microphone and camera. + Para responder a videochamada, permite que Molly acceda ao teu micrófono e cámara. Molly require permiso para poder facer e recibir chamadas, pero este foi denegado de forma permanente. Vai aos axustes da aplicación, selecciona \"Permisos\" e activa \"Micrófono\" e \"Cámara\". Respondido nun dispositivo ligado. Rexeitado nun dispositivo ligado. @@ -3443,7 +3445,9 @@ Seguinte + Crear PIN alfanumérico + Crear PIN numérico @@ -3505,8 +3509,8 @@ Escribe o PIN creado para a túa conta. Non é o mesmo que o código de verificación por SMS. Introduce o PIN que creaches para a túa conta. - Escribe PIN alfanumérico - Escribe PIN numérico + + Cambiar teclado PIN incorrecto. Inténtao outra vez. Esqueciches o PIN? PIN incorrecto @@ -6018,14 +6022,12 @@ Filtro por perdidas Seleccionar todo - + Eliminar Borrar %1$d chamada? Borrar %1$d chamadas? - - Eliminar para min %1$d chamada eliminada diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index b47060123c..44714adf26 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -883,6 +883,8 @@ કોઈ લિંકનુ પ્રિવ્યુ ઉપલબ્ધ નથી આ ગ્રુપ લિંક સક્રિય નથી %1$s . %2$s + + Signal કૉલમાં જોડાવા માટે આ લિંક વાપરો @@ -1626,8 +1628,8 @@ તમારી પાસે %1$d પ્રયાસ બાકી છે. જો તમે પ્રયત્નોનો અંત લાવો છો, તો તમે નવો PIN બનાવી શકો છો. તમે તમારા એકાઉન્ટ રજીસ્ટર અને ઉપયોગ કરી શકો છો પરંતુ તમે તમારી પ્રોફાઇલ માહિતી જેવી કેટલીક સાચવેલી સેટિંગ્સ ગુમાવશો. Signal  રજીસ્ટ્રેશન- Android માટે પિન સાથે સહાયની જરૂર છે - આલ્ફાન્યુમેરિક PIN દાખલ કરો - સંખ્યાત્મક PIN દાખલ કરો + + કીબોર્ડ સ્વિચ કરો તમારો PIN બનાવો @@ -2314,7 +2316,7 @@ કૉલનો જવાબ આપવા માટે, Mollyને તમારા માઇક્રોફોન પર ઍક્સેસ આપો. - To answer the video call, give Molly access to your microphone and camera. + વીડિયો કૉલનો જવાબ આપવા માટે, Mollyને તમારા માઇક્રોફોન અને કૅમેરાનો ઍક્સેસ આપો. કૉલ કરવા અથવા પ્રાપ્ત કરવા માટે Molly ને માઇક્રોફોન અને કેમેરાની પરવાનગીની જરૂર હોય છે, પરંતુ તેઓને કાયમી નામંજૂર કરવામાં આવ્યા છે. કૃપા કરીને એપ્લિકેશન સેટિંગ્સ ચાલુ રાખો, \"પરવાનગી\" પસંદ કરો અને \"માઇક્રોફોન\" અને \"કેમેરો\" સક્ષમ કરો. લિંક્ડ ડિવાઇસ પર જવાબ આપ્યો. લિંક્ડ ડિવાઇસ પર નકારી. @@ -3443,7 +3445,9 @@ આગળ + આલ્ફાન્યુમેરિક પિન બનાવો + સંખ્યાત્મક PIN બનાવો @@ -3505,8 +3509,8 @@ તમારા એકાઉન્ટ માટે તમે બનાવેલો PIN દાખલ કરો. આ તમારા SMS ચકાસણી કોડથી અલગ છે. તમે તમારા એકાઉન્ટ માટે બનાવેલ પિન દાખલ કરો. - આલ્ફાન્યુમેરિક PIN દાખલ કરો - સંખ્યાત્મક PIN દાખલ કરો + + કીબોર્ડ સ્વિચ કરો ખોટો પિન. ફરીથી પ્રયત્ન કરો. PIN ભૂલી ગયા? ખોટો PIN @@ -6018,14 +6022,12 @@ મિસ્ડ કૉલથી ફિલ્ટર કરો બધા પસંદ કરો - + ડિલીટ કરો %1$d કૉલ ડિલીટ કરવો છે? %1$d કૉલ ડિલીટ કરવા છે? - - મારા માટે ડિલીટ કરો %1$d કૉલ ડિલીટ કર્યો diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8798e91c60..4009cf7657 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -883,6 +883,8 @@ कोई लिंक प्रिव्यु उपलब्ध नहीं है यह समूह लिंक सक्रिय नहीं है %1$s · %2$s + + Signal कॉल से जुड़ने के लिए यह लिंक इस्तेमाल करें @@ -1626,8 +1628,8 @@ आपके पास %1$d मौके बचे हैं। अगर आपके मौके खत्म हो गए तो आप नया पिन बना सकते हैं। आप रजिस्टर करके अपना खाता इस्तेमाल कर सकते हैं पर आप कुछ सेटिंग्स खो देंगे जैसे प्रोफ़ाइल की जानकारी। Signal रजिस्ट्रेशन - Android के लिए पिन को लेकर मदद चाहिए - अल्फ़ान्यूमेरिक पिन दर्ज करें - संख्यात्मक पिन दर्ज करें + + कीबोर्ड बदलें अपना पिन बनाएँ @@ -2314,7 +2316,7 @@ कॉल का जवाब देने के लिए Molly को अपने माइक्रोफ़ोन का ऐक्सेस दें। - To answer the video call, give Molly access to your microphone and camera. + वीडियो कॉल का उत्तर देने के लिए, Molly को अपने माइक्रोफ़ोन और कैमरे तक पहुंच प्रदान करें। कॉल करने या प्राप्त करने के लिए Molly को माइक्रोफ़ोन और कैमरा अनुमतियों की आवश्यकता होती है, लेकिन उन्हें स्थायी रूप से अस्वीकार कर दिया गया है। कृपया ऐप सेटिंग्स जारी रखें, \"अनुमतियां\" चुनें, और \"माइक्रोफ़ोन\" और \"कैमरा\" सक्षम करें। एक लिंक किए हुए डिवाइस पर जवाब दिया गया। एक लिंक किए हुए डिवाइस पर रद्द किया गया। @@ -3443,7 +3445,9 @@ अगला + अल्फ़ान्यूमेरिक पिन बनाएं + न्यूमेरिक पिन बनाएं @@ -3505,8 +3509,8 @@ अपने ख़ाते के लिये बनाया गया पिन डालें। ये पिन आपके SMS सत्यापण कोड से अलग है। आपके द्वारा अपने अकाउंट के लिए बनाया गया पिन दर्ज करें। - अल्फ़ान्यूमेरिक पिन दर्ज करें - संख्यात्मक पिन दर्ज करें + + कीबोर्ड बदलें गलत पिन। पुनः प्रयास करें। पिन भूल गए? ग़लत पिन @@ -6018,14 +6022,12 @@ मिस्ड द्वारा फ़िल्टर किया गया सभी को चुन लो - + डिलीट करें %1$d कॉल हटाएं? %1$d कॉल हटाएं? - - मेरे लिये डिलीट करें %1$d कॉल डिलीट की गई diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index fec41ac03e..cf248414ca 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -319,7 +319,7 @@ Nije moguće preuzeti videozapis. Morat ćete ga poslati ponovno. - uređeno %1$s + Uređeno %1$s Pridruži se pozivu @@ -939,6 +939,8 @@ Nema dostupnog pregleda poveznice Poveznica grupe nije aktivna %1$s · %2$s + + Koristite ovu poveznicu da biste se pridružili Signal pozivu @@ -1744,8 +1746,8 @@ Preostalo vam je %1$d pokušaja. Ako vam ponestane pokušaja, možete stvoriti novi PIN. Možete registrirati i koristiti svoj račun, ali ćete izgubiti neke spremljene postavke poput podataka o vašem profilu. Registracija Signala - Trebate pomoć s PIN-om za Android - Unesite alfanumerički PIN - Unesite numerički PIN + + Promjena tipkovnice Stvorite svoj PIN @@ -2454,7 +2456,7 @@ Da biste odgovorili na poziv, omogućite Mollyu pristup vašem mikrofonu. - To answer the video call, give Molly access to your microphone and camera. + Da biste odgovorili na videopoziv, omogućite Mollyu pristup vašem mikrofonu i kameri. Molly zahtijeva dopuštenja za mikrofon i kameru za upućivanje ili primanje poziva, ali ona su trajno odbijena. Otvorite postavke aplikacije, odaberite \"Dozvole\" i omogućite \"Mikrofon\" i \"Kamera\". Odgovoreno na povezanom uređaju. Odbijeno na povezanom uređaju. @@ -3611,7 +3613,9 @@ Sljedeće + Stvorite alfanumerički PIN + Stvorite numerički PIN @@ -3677,8 +3681,8 @@ Unesite PIN koji ste stvorili za svoj račun. To se razlikuje od vašeg SMS kôda za provjeru. Unesite PIN koji ste stvorili za svoj račun. - Unesite alfanumerički PIN - Unesite numerički PIN + + Promjena tipkovnice Nevažeći PIN. Pokušajte ponovno. Zaboravili ste PIN? Nevažeći PIN @@ -6266,7 +6270,7 @@ Filtar: propušteni pozivi Odaberi sve - + Izbriši Izbrisati %1$d poziv? @@ -6274,8 +6278,6 @@ Izbrisati %1$d poziva? Izbrisati %1$d poziva? - - Izbriši za mene %1$d izbrisan poziv diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 6790348662..9bb2a48dc9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -317,7 +317,7 @@ A videó nem tölthető le. Újra el kell küldened. - szerkesztve: %1$s + Szerkesztve: %1$s Belépés a hívásba @@ -883,6 +883,8 @@ Nem érhető el előnézeti kép Ez a csoporthivatkozás nem aktív %1$s · %2$s + + Ezzel a hivatkozással csatlakozhatsz egy Signal-híváshoz @@ -1626,8 +1628,8 @@ Még %1$d próbálkozásod maradt. Ha kifogysz belőlük, lehetőséged lesz új PIN létrehozására. Regisztrálhatsz újra, majd használatba veheted a fiókodat, de mentett beállításaid egy része, mint például a profil-információid elvesznek. Signal regisztráció - Segítségkérés a PIN kód használatához Androidon - Add meg az alfanumerikus PIN kódot - Add meg a számjegyekből álló PIN kódot + + Billentyűzet váltása PIN létrehozása @@ -2314,7 +2316,7 @@ A hívás fogadásához engedélyezd, hogy a Molly hozzáférhessen a mikrofonhoz! - To answer the video call, give Molly access to your microphone and camera. + A videohívás fogadásához engedélyezd, hogy a Molly hozzáférhessen a mikrofonhoz és a kamerához! A Mollynak szüksége van a Mikrofon és Kamera engedélyekre, hogy képes legyen hívások indítására és fogadására, de ez jelenleg nincs megadva. Kérlek menj az alkalmazásbeállításokhoz, válaszd az \"Engedélyek\"-et és engedélyezd a \"Mikrofon\"-t és a \"Kamera\"-t. Fogadva egy társított eszközön. Elutasítva egy társított eszközön. @@ -3443,7 +3445,9 @@ Tovább + Alfanumerikus PIN kód létrehozása + Számjegyekből álló PIN kód létrehozása @@ -3505,8 +3509,8 @@ Gépeld be a fiókodhoz létrehozott PIN kódodat. Ez nem azonos az SMS-ben regisztrációkor kapott ellenőrző kóddal. Add meg a fiókodhoz létrehozott PIN-kódot. - Add meg az alfanumerikus PIN kódot - Add meg a számjegyekből álló PIN kódot + + Billentyűzet váltása Hibás PIN kód. Próbáld újra! Elfelejtetted PIN kódodat? Hibás PIN @@ -6018,14 +6022,12 @@ Szűrve nem fogadott hívás szerint Összes kiválasztása - + Törlés %1$d hívás törlése? %1$d hívás törlése? - - Törlés számomra %1$d hívás törölve diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 430f9cc011..843c6fe722 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -316,7 +316,7 @@ Tidak dapat mengunduh video. Anda harus mengirimnya lagi. - diedit %1$s + Diedit %1$s Gabung ke panggilan @@ -855,6 +855,8 @@ Pratinjau tautan tidak tersedia Tautan grup ini tidak aktif %1$s . %2$s + + Gunakan tautan ini untuk gabung di Panggilan Signal @@ -1567,8 +1569,8 @@ Anda memiliki %1$d percobaan tersisa. Jika kehabisan, Anda dapat membuat PIN baru. Anda dapat melakukan pendaftaran dan menggunakan akun Anda namun Anda akan kehilangan pengaturan yang disimpan seperti informasi profil. Pendaftaran Signal - Perlu Bantuan mengenai PIN untuk Android - Masukan PIN alfanumerik - Masukkan PIN numerik + + Ganti papan ketik Buat PIN Anda @@ -2244,7 +2246,7 @@ Untuk menjawab panggilan, berikan Molly akses ke mikrofon Anda. - To answer the video call, give Molly access to your microphone and camera. + Untuk menjawab panggilan video, beri Molly akses ke mikrofon dan kamera. Molly memerlukan izin Mikrofon dan Kamera untuk menerima atau melakukan panggilan, tetapi saat ini izin ditolak secara permanen. Mohon lanjutkan ke pengatuaran aplikasi, pilih \"Izin\" lalu aktifkan \"Mikrofon\" serta \"Kamera\". Dijawab dari perangkat terhubung. Ditolak dari perangkat terhubung. @@ -3359,7 +3361,9 @@ Berikutnya + Buat PIN alfanumerik + Buat PIN numerik @@ -3419,8 +3423,8 @@ Masukkan PIN yang Anda buat untuk akun Anda. Ini berbeda dari kode verifikasi SMS. Masukkan PIN yang Anda buat untuk akun Anda. - Masukan PIN alfanumerik - Masukkan PIN numerik + + Ganti papan ketik PIN Salah. Coba lagi. Lupa PIN? PIN Salah @@ -5894,13 +5898,11 @@ Difilter berdasarkan yang tidak terjawab Pilih semua - + Hapus Hapus %1$d panggilan? - - Hapus untuk saya %1$d panggilan dihapus diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ce4dca42d4..ff2dff77b9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -317,7 +317,7 @@ Impossibile scaricare il video. Dovrai inviarlo di nuovo. - modificato %1$s + Modificato %1$s Unisciti alla chiamata @@ -883,6 +883,8 @@ Nessuna anteprima del link disponibile Questo link del gruppo non è attivo %1$s · %2$s + + Usa questo link per unirti a una chiamata Signal @@ -1626,8 +1628,8 @@ Hai %1$d tentativi rimanenti. Se esaurisci i tentativi, puoi creare un nuovo PIN. Puoi registrarti e usare il tuo account ma perderai alcune impostazioni salvate, per esempio le informazioni del tuo profilo. Registrazione Signal - Ho bisogno di aiuto con il PIN per Android - Inserisci PIN alfanumerico - Inserisci PIN numerico + + Cambia tastiera Crea il tuo PIN @@ -2314,7 +2316,7 @@ Per rispondere alla chiamata, dai a Molly l\'autorizzazione ad accedere al tuo microfono. - To answer the video call, give Molly access to your microphone and camera. + Per rispondere alla videochiamata, dai a Molly l\'autorizzazione ad accedere al tuo microfono e alla fotocamera. Molly richiede le autorizzazioni all\'uso del microfono e della fotocamera per fare e ricevere chiamate, ma questo sono state negate in modo permanente. Si prega di aprire il menu delle impostazioni dell\'app, selezionare \"Autorizzazioni\" e abilitare \"Microfono\" e \"Fotocamera\". Hai risposto su un dispositivo collegato. Hai rifiutato su un dispositivo collegato. @@ -3443,7 +3445,9 @@ Avanti + Crea PIN alfanumerico + Crea PIN numerico @@ -3505,8 +3509,8 @@ Inserisci il PIN che hai creato per il tuo account. Questo è diverso dal tuo codice di verifica SMS. Inserisci il PIN che hai creato per il tuo account. - Inserisci PIN alfanumerico - Inserisci PIN numerico + + Cambia tastiera PIN errato. Riprova. PIN dimenticato? PIN errato @@ -6018,14 +6022,12 @@ Solo chiamate perse Seleziona tutto - + Elimina Vuoi eliminare %1$d chiamata? Vuoi eliminare %1$d chiamate? - - Elimina per me %1$d chiamata eliminata diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 68eb0bb444..21c5e23127 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -939,6 +939,8 @@ תצוגה מקדימה של קישור אינה זמינה קישור קבוצה זה אינו פעיל %1$s · %2$s + + אפשר להשתמש בלינק הזה כדי להצטרף לשיחת Signal @@ -1744,8 +1746,8 @@ יש לך %1$d ניסינות נותרים. אם הניסיונות יאזלו לך, אתה יכול ליצור PIN חדש. אתה יכול להירשם ולהשתמש בחשבון שלך אבל תאבד מספר הגדרות מסוימות כמו מידע הפרופיל שלך. הרשמה אל Signal - עזרה עם PIN עבור Android - הכנס PIN אלפאנומרי - הכנס PIN מספרי + + החלפת מקלדת צור את ה־PIN שלך @@ -2454,7 +2456,7 @@ כדי לענות אל השיחה, תן אל Molly גישה אל המיקרופון שלך. - To answer the video call, give Molly access to your microphone and camera. + כדי לענות לשיחת וידאו, Molly צריכה גישה למיקרופון ולמצלמה שלך. Molly דורש את ההרשאות של המיקרופון והמצלמה על מנת לבצע שיחות ולענות לשיחות, אבל הן נדחו לצמיתות. אנא המשך אל הגדרות היישום, בחר \"הרשאות\" ואפשר את \"מיקרופון\" ואת \"מצלמה\". נענתה על מכשיר מקושר. נדחתה על מכשיר מקושר. @@ -3611,7 +3613,9 @@ הבא + צור PIN אלפאנומרי + צור PIN מספרי @@ -3677,8 +3681,8 @@ הכנס את ה־PIN שיצרת עבור החשבון שלך. הוא שונה מקוד הווידוא שלך במסרון. צריך להזין את ה–PIN שיצרת עבור החשבון שלך. - הכנס PIN אלפאנומרי - הכנס PIN מספרי + + החלפת מקלדת PIN שגוי. נסה שוב. שכחת PIN? PIN שגוי @@ -6266,7 +6270,7 @@ סינון לפי שיחות שלא נענו בחירת הכל - + מחיקה למחוק שיחה %1$d? @@ -6274,8 +6278,6 @@ למחוק %1$d שיחות? למחוק %1$d שיחות? - - מחיקה עבורי שיחה %1$d נמחקה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1ee59b6114..c2dbf74c84 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -855,6 +855,8 @@ リンクプレビューは使用できません このグループリンクは利用できません %1$s · %2$s + + リンクを使用して Signal 通話に参加する @@ -1567,8 +1569,8 @@ あと%1$d回試行できます。残りの試行回数がなくなった場合、新しいPINを作成できます。アカウントを登録して使用することもできますが、プロフィールなどの保存されている設定情報は失われます。 Signal登録 - AndroidのPINに関するサポートが必要 - 英数字のPINを入力 - 数字のPINを入力 + + キーボードを切り替える PINを作成 @@ -2244,7 +2246,7 @@ 着信に出るには、Mollyにマイクへのアクセスを許可してください。 - To answer the video call, give Molly access to your microphone and camera. + ビデオ通話に出るには、Molly にマイクとカメラへのアクセスを許可してください。 通話するには、Mollyにマイクとカメラへのアクセス許可が必要ですが、無効になっています。アプリ設定メニューの「アプリの権限」で「マイク」と「カメラ」を有効にしてください。 リンクした端末で応答しました。 リンクした端末で拒否しました。 @@ -3359,7 +3361,9 @@ 次へ + 英数字のPINを作成 + 数字のPINを作成 @@ -3419,8 +3423,8 @@ あなたのアカウント用に作成したPINを入力してください。これはSMSの認証コードとは違います。 アカウント用に作成した PIN を入力してください。 - 英数字のPINを入力 - 数字のPINを入力 + + キーボードを切り替える PINが違います。再度試してください。 PINを忘れましたか? PINが違います @@ -5894,13 +5898,11 @@ 未確認のものを検索する すべて選択する - + 消去 %1$d 件の通話を消去しますか? - - 自分の分だけ消去 %1$d の通話が消去されました diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 294714a3df..97e51ef27a 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -883,6 +883,8 @@ ბმულის წინასწარი ნახვა არაა ხელმისაწვდომი ჯგუფის ეს ბმული არ არის აქტიური %1$s · %2$s + + გამოიყენე ეს ბმული Signal-ის ზარზე შესასვლელად @@ -1626,8 +1628,8 @@ დაგრჩა %1$d მცდელობა. თუ მცდელობები ამოგეწურება, შეგიძლია ახალი პინ-კოდი შექმნა. შეგიძლია დარეგისტრირდე და გამოიყენო შენი ანგარიში, მაგრამ დაკარგავ ზოგიერთ შენახულ პარამეტრს, მაგალითად, შენი პროფილის ინფორმაციას. Signal-ის რეგისტრაცია - საჭიროა დახმარება Android-ის პინ-კოდთან დაკავშირებით - შეიყვანე ანბანურ-ციფრული პინ-კოდი - შეიყვანე ციფრული პინ-კოდი + + კლავიატურის გადართვა შექმენი შენი პინ-კოდი @@ -2314,7 +2316,7 @@ ზარზე პასუხისთვის მიეცი Molly-ს წვდომა შენს მიკროფონზე. - To answer the video call, give Molly access to your microphone and camera. + მიეცი Molly-ს წვდომა შენს მიკროფონსა და კამერაზე, რათა ვიდეო ზარს უპასუხო. ზარების განსახორციელებლად ან მისაღებად Molly-ს მიკროფონსა და კამერაზე წვდომის ნებართვა სჭირდება, მაგრამ ისინი სამუდამოდ იქნა უარყოფილია. გთხოვთ, შეხვიდე აპის პარამეტრებში, აირჩიო „ნებართვები“ და ჩართო „მიკროფონი“ და „კამერა“. ზარი მიღებულია დაკავშირებულ მოწყობილობაზე. ზარი უარყოფილია დაკავშირებულ მოწყობილობაზე. @@ -3344,7 +3346,7 @@ მთავარ გვერდზე დამატება ბაბლის შექმნა - Format text + ტექსტის ფორმატირება ფანჯრის გაფართოება @@ -3443,7 +3445,9 @@ შემდეგი + შექმენი ანბანურ-ციფრული პინ-კოდი + შექმენი ციფრული პინ-კოდი @@ -3505,8 +3509,8 @@ შეიყვანე შენი მონაცემებისთვის შექმნილი პინ-კოდი. ის განსხვავდება შენი SMS ვერიფიკაციის კოდისგან. შეიყვანე შენი ანგარიშისთვის შექმნილი პინ-კოდი. - შეიყვანე ანბანურ-ციფრული პინ-კოდი - შეიყვანე ციფრული პინ-კოდი + + კლავიატურის გადართვა არასწორი პინ-კოდი. ხელახლა სცადე. დაგავიწყდა პინ-კოდი? არასწორი პინ-კოდი @@ -5920,7 +5924,7 @@ სპოილერი - Clear formatting + ფორმატირების გასუფთავება @@ -6018,14 +6022,12 @@ გაფილტრულია გამოტოვებულის მიხედვით ყველას მონიშვნა - + წაშლა წავშალოთ %1$d ზარი? წავშალოთ %1$d ზარი? - - ჩემთვის წაშლა წაშლილია %1$d ზარი diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 91ae7aa925..b93c123399 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -883,6 +883,8 @@ Сілтеменің шағын көрінісі жоқ Бұл топ сілтемесі істемейді %1$s · %2$s + + Signal қоңырауына қосылу үшін осы сілтемені пайдаланыңыз @@ -1626,8 +1628,8 @@ %1$d мүмкіндігіңіз қалды. Егер мүмкіндіктердің барлығын пайдаланып қойсаңыз, жаңа PIN-код жасауыңызға болады. Тіркеліп, аккаунтыңызды қолдана аласыз, бірақ профиль ақпараты сияқты кейбір сақталған параметрлер жоғалады. Signal қолданбасында тіркелу - Android жүйесіне арналған PIN кодына қатысты көмек керек - Әріпті-санды PIN енгізу - Санды PIN енгізу + + Пернетақтаны ауыстырыңыз PIN код жасау @@ -2314,7 +2316,7 @@ Қоңырауға жауап беру үшін Molly қолданбасының микрофонды пайдалануына рұқсат етіңіз. - To answer the video call, give Molly access to your microphone and camera. + Видеоқоңырауға жауап беру үшін Molly қолданбасының микрофоныңызды және камераңызды пайдалануына рұқсат етіңіз. Қоңырау шалу немесе қабылдау үшін Molly қолданбасына микрофон мен камераны пайдалануға рұқсат керек, бірақ параметрлерде оларды пайдалануға рұқсат берілмеген. Қолданба параметрлеріне кіріп, \"Рұқсаттар\" бөлімін таңдаңыз да, \"Микрофон\" және \"Камера\" параметрлерін қосыңыз. Байланыстырылған құрылғыда жауап берілді. Байланыстырылған құрылғыда қабылданбады. @@ -3443,7 +3445,9 @@ Келесі + Әріптік-сандық PIN-код жасау + Сандық PIN-код жасау @@ -3505,8 +3509,8 @@ Тіркелгіңіз үшін жасаған PIN кодты енгізіңіз. Ол сіздің SMS арқылы жіберілген тексеру кодыңыздан басқаша. Аккаунт үшін ойлап тапқан PIN кодыңызды енгізіңіз. - Әріпті-санды PIN енгізу - Санды PIN енгізу + + Пернетақтаны ауыстырыңыз Дұрыс емес PIN. Қайтадан байқап көріңіз. PIN кодын ұмытып қалдыңыз ба? Дұрыс емес PIN @@ -6018,14 +6022,12 @@ Қабылданбаған қоңыраулар ғана көрсетілді Барлығын таңдау - + Жою %1$d қоңырауды жою керек пе? %1$d қоңырауды жою керек пе? - - Меннен ғана жою %1$d қоңырау жойылды diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index d65c23b6da..44fe3bc42c 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -855,6 +855,8 @@ មិនមានតំណភ្ជាប់បង្ហាញទេ តំណភ្ជាប់ក្រុមនេះមិនដំណើរការទេ %1$s · %2$s + + ប្រើតំណនេះដើម្បីចូលរួមការហៅតាម Signal @@ -1567,8 +1569,8 @@ អ្នកនៅមានការព្យាយាម %1$d ដង។ បើការព្យាយាមអស់ អ្នកអាចបង្កើតលេខកូដ PIN ថ្មី។ អ្នកអាចចុះឈ្មោះ និងប្រើប្រាស់គណនីរបស់អ្នក តែអ្នកនឹងត្រូវបាត់បង់ការកំណត់ដែលបានរក្សាទុក ដូចជាព័ត៌មានប្រវត្តិរូបរបស់អ្នក។ ការចុះឈ្មោះ Signal - ត្រូវការជំនួយជាមួយ PIN សម្រាប់ Android - បញ្ចូលលេខ PIN អក្សរក្រមលេខ - បញ្ចូលលេខកូដ PIN + + ប្តូរក្តារចុច បង្កើតលេខ PIN របស់អ្នក @@ -2244,7 +2246,7 @@ ដើម្បីឆ្លើយតបការហៅ សូមឲ្យ Molly ប្រើប្រាស់ម៉ៃក្រូហ្វូនរបស់អ្នក។ - To answer the video call, give Molly access to your microphone and camera. + ដើម្បីលើកទទួលការហៅជាវីដេអូ សូមឲ្យ Molly ប្រើប្រាស់មីក្រូហ្វូន និងកាមេរ៉ារបស់អ្នក។ Mollyត្រូវការសិទ្ធិប្រើប្រាស់ម៉ៃក្រូហ្វូន និងកាមេរ៉ា ដើម្បីហៅចេញ ឬទទួលការហៅចូល ប៉ុន្តែពួកវាត្រូវបានបដិសេធរហូត។ សូមបន្តទៅកាន់ ការកំណត់ ជ្រើសរើស \"ការអនុញ្ញាត\" និងបើក \"ម៉ៃក្រូហ្វូន\" និង \"កាមេរ៉ា\"។ បានឆ្លើយលើឧបករណ៍ដែលបានតភ្ជាប់។ បានបដិសេធលើឧបករណ៍ដែលបានតភ្ជាប់។ @@ -3359,7 +3361,9 @@ បន្ទាប់ + បង្កើតអក្សរក្រមលេខ PIN + បង្កើតលេខ PIN @@ -3419,8 +3423,8 @@ បញ្ចូលលេខ PIN ដែលអ្នកបានបង្កើតសម្រាប់ គណនីរបស់អ្នក។ នេះខុសពីលេខកូដផ្ទៀងផ្ទាត់ពីសារ SMS ។ បញ្ចូលលេខកូដសម្ងាត់ដែលអ្នកបានបង្កើតសម្រាប់គណនីរបស់អ្នក។ - បញ្ចូលលេខ PIN អក្សរក្រមលេខ - បញ្ចូលលេខកូដ PIN + + ប្តូរក្តារចុច លេខកូដ PIN មិនត្រឹមត្រូវ។ សាកល្បងម្តងទៀត។ ភ្លេចលេខ PIN? លេខកូដ PIN មិនត្រឹមត្រូវ @@ -5894,13 +5898,11 @@ បានត្រងតាមការខកមិនបានទទួល ជ្រើសរើសទាំងអស់ - + លុប លុបការហៅ %1$d ឬ? - - លុបសម្រាប់ខ្ញុំ បានលុបការហៅ %1$d diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 06fd0f45fc..b3ea02186e 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -883,6 +883,8 @@ ಪೂರ್ವವೀಕ್ಷಣೆ ಲಭ್ಯವಿಲ್ಲ ಗುಂಪಿನ ಲಿಂಕ್ ಸಕ್ರಿಯವಾಗಿಲ್ಲ %1$s · %2$s + + Signal ಕರೆಗೆ ಸೇರಲು ಈ ಲಿಂಕ್ ಬಳಸಿ @@ -1626,8 +1628,8 @@ ನಿಮ್ಮಲ್ಲಿ %1$d ಪ್ರಯತ್ನಗಳು ಉಳಿದಿವೆ. ನೀವು ಪ್ರಯತ್ನಗಳ ಮಿತಿಯನ್ನು ಮೀರಿದರೆ, ನೀವು ಹೊಸ ಪಿನ್ ರಚಿಸಬಹುದು. ನಿಮ್ಮ ಖಾತೆಯನ್ನು ನೀವು ನೋಂದಾಯಿಸಬಹುದು ಮತ್ತು ಬಳಸಬಹುದು. ಆದರೆ ನಿಮ್ಮ ಪ್ರೊಫೈಲ್ ಮಾಹಿತಿಯಂತಹ ಕೆಲವು ಉಳಿಸಿದ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನೀವು ಕಳೆದುಕೊಳ್ಳುತ್ತೀರಿ. Signal ನೋಂದಣಿ - Android ಗಾಗಿ ಪಿನ್‌ ಕುರಿತ ಸಹಾಯ ಬೇಕಿದೆ - ಆಲ್ಫಾನ್ಯೂಮರಿಕ್ ಪಿನ್ ನಮೂದಿಸಿ - ಸಂಖ್ಯಾ ಪಿನ್ ನಮೂದಿಸಿ + + ಕೀಬೋರ್ಡ್ ಬದಲಿಸಿ ನಿಮ್ಮ ಪಿನ್ ರಚಿಸಿ @@ -2314,7 +2316,7 @@ ಕರೆಗೆ ಉತ್ತರಿಸಲು, ನಿಮ್ಮ ಮೈಕ್ರೋಫೋನ್‌ಗೆ Molly ಪ್ರವೇಶ ನೀಡಿ. - To answer the video call, give Molly access to your microphone and camera. + ವೀಡಿಯೊ ಕರೆಗೆ ಉತ್ತರಿಸಲು, ನಿಮ್ಮ ಮೈಕ್ರೋಫೋನ್ ಮತ್ತು ಕ್ಯಾಮರಾಗೆ Molly ಆ್ಯಕ್ಸೆಸ್ ನೀಡಿ. ಕರೆಗಳನ್ನು ಮಾಡಲು ಅಥವಾ ಪಡೆಯಲು Molly ಗೆ ಮೈಕ್ರೊಫೋನ್ ಹಾಗೂ ಕ್ಯಾಮರಾ ಅನುಮತಿಗಳು ಅಗತ್ಯವಿರುತ್ತವೆ, ಆದರೆ ಅವುಗಳನ್ನು ಶಾಶ್ವತವಾಗಿ ನಿರಾಕರಿಸಲಾಗಿದೆ. ದಯವಿಟ್ಟು ಆ್ಯಪ್ ಸೆಟ್ಟಿಂಗ್‌ ಗಳಿಗೆ ಮುಂದುವರಿಯಿರಿ, \"ಅನುಮತಿಗಳು\" ಆಯ್ಕೆ ಮಾಡಿ, ಮತ್ತು \"ಮೈಕ್ರೊಫೋನ್\" ಮತ್ತು \"ಕ್ಯಾಮೆರಾ\" ಸಕ್ರಿಯಗೊಳಿಸಿ. ಲಿಂಕ್ ಮಾಡಿದ ಸಾಧನದಲ್ಲಿ ಉತ್ತರಿಸಲಾಗಿದೆ. ಲಿಂಕ್ ಮಾಡಿದ ಸಾಧನದಲ್ಲಿ ತಿರಸ್ಕರಿಸಲಾಗಿದೆ. @@ -3443,7 +3445,9 @@ ಮುಂದೆ + ಆಲ್ಫಾನ್ಯೂಮರಿಕ್ ಪಿನ್ ರಚಿಸಿ + ಸಂಖ್ಯಾ ಪಿನ್ ರಚಿಸಿ @@ -3505,8 +3509,8 @@ ನೀವು ಖಾತೆಗಾಗಿ ರಚಿಸಿದ ಪಿನ್ ಅನ್ನು ನಮೂದಿಸಿ. ಇದು ಎಸ್‌ಎಂಎಸ್‌ ದೃಢೀಕರಣ ಕೋಡ್ ಗಿಂತ ಭಿನ್ನವಾಗಿರುತ್ತದೆ. ನಿಮ್ಮ ಖಾತೆಗಾಗಿ ನೀವು ರಚಿಸಿದ PIN ನಮೂದಿಸಿ. - ಆಲ್ಫಾನ್ಯೂಮರಿಕ್ ಪಿನ್ ನಮೂದಿಸಿ - ಸಂಖ್ಯಾ ಪಿನ್ ನಮೂದಿಸಿ + + ಕೀಬೋರ್ಡ್ ಬದಲಿಸಿ ತಪ್ಪಾದ ಪಿನ್. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ. ಪಿನ್ ಮರೆತಿದ್ದೀರಾ? ತಪ್ಪಾದ ಪಿನ್ @@ -6018,14 +6022,12 @@ ತಪ್ಪಿಹೋದವುಗಳು ಎಂದು ಫಿಲ್ಟರ್ ಮಾಡಲಾಗಿದೆ ಎಲ್ಲಾ ಆಯ್ಕೆಮಾಡಿ - + ಅಳಿಸಿ ಕರೆಯನ್ನು %1$d ಅಳಿಸಬೇಕೇ? ಕರೆಗಳನ್ನು %1$d ಅಳಿಸಬೇಕೇ? - - ನನಗೆ ಮಾತ್ರ ಅಳಿಸಿ %1$d ಕರೆಯನ್ನು ಅಳಿಸಲಾಗಿದೆ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a11260508e..82fac5786b 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -855,6 +855,8 @@ 사용 가능한 링크 미리 보기가 없습니다. 이 그룹 링크가 비활성화 되었습니다. %1$s, %2$s + + 이 링크를 사용하여 Signal 통화에 참여하세요. @@ -1567,8 +1569,8 @@ %1$d번의 시도가 남아 있습니다. 시도가 부족하면 새 PIN을 만들 수 있습니다. 계정을 등록하고 사용할 수 있지만 프로필 정보와 같은 일부 저장된 설정이 손실됩니다. Signal 등록 - Android용 PIN에 대한 도움이 필요합니다. - 영숫자 번호 입력 - 숫자 번호 입력 + + 키보드 전환 번호 생성 @@ -2244,7 +2246,7 @@ 통화를 수락하려면 Molly에서 마이크를 사용하도록 허용하세요. - To answer the video call, give Molly access to your microphone and camera. + 영상 통화를 수락하려면 Molly에서 마이크와 카메라를 사용하도록 허용하세요. Molly에서 전화 기능을 사용하려면 마이크와 카메라 권한이 필요하지만 현재 거부되어 있습니다. 앱 설정 메뉴에서 \'권한\'을 선택한 후 \'마이크\'와 \'카메라\' 항목을 허용해 주세요. 연결된 기기에서 응답했습니다. 연결된 기기에서 거부했습니다. @@ -3359,7 +3361,9 @@ 다음 + 영숫자 번호 생성 + 숫자 번호 생성 @@ -3419,8 +3423,8 @@ 계정용으로 생성한 번호를 입력하세요. SMS 인증 코드와는 다릅니다. 계정에 대해 만든 PIN을 입력하세요. - 영숫자 번호 입력 - 숫자 번호 입력 + + 키보드 전환 잘못된 번호입니다. 다시 시도해 주세요. 번호를 잊으셨나요? 잘못된 번호 @@ -5894,13 +5898,11 @@ 부재중 전화로 필터링함 모두 선택 - + 삭제 통화 %1$d개를 삭제할까요? - - 나에게서 삭제 %1$d 통화를 삭제함 diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index e0ee20c86f..fc54c95b0a 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -316,7 +316,7 @@ Видео жүктөлүп алынган жок. {0} кайра жөнөтүшү керек. - качан оңдолду: %1$s + Качан оңдолду: %1$s Чалууга кошулуу @@ -855,6 +855,8 @@ Шилтемени алдын ала көрө албайсыз Бул топтун шилтемеси иштебейт %1$s · %2$s + + Signal чалуусуна ушул шилтеме менен кошулуңуз @@ -1567,8 +1569,8 @@ %1$d мүмкүнчүлүк гана калды. Эгер баарын колдонуп салсаңыз, жаңы PIN код түзүшүңүз керек. Аккаунт түзүп, аны колдоно алганыңыз менен, профилиңиздеги маалымат сыяктуу айрым сакталган нерселерди жоготуп алышыңыз мүмкүн. Signal-га Катталуу - Android-ге PIN код үчүн жардам керек - Тамгалык-цифралык PIN кодду киргизиңиз - Сандык PIN кодду киргизиңиз + + Башка баскычтопко которулуу PIN код түзүңүз @@ -2244,7 +2246,7 @@ Чалууга жооп берүү үчүн Molly колдонмосуна микрофонуңуз керек. - To answer the video call, give Molly access to your microphone and camera. + Видео чалууга жооп берүү үчүн Molly колдонмосуна микрофонуңуз менен камераңызды колдонгонго уруксат бериңиз. Чалуу же чалууларды кабыл алуу үчүн Molly колдонмосуна микрофон менен камераны колдонууга уруксат беришиңиз керек, бирок сиз андан баш тарткансыз. Колдонмонун параметрлерине кирүү үчүн \"Улантуу\" дегенди басып, \"Уруксаттар\" дегенди тандап, \"Микрофон\" жана \"Камера\" дегенди иштетиңиз. Байланышкан түзмөктө жооп берилди. Байланышкан түзмөктө четке кагылды. @@ -3359,7 +3361,9 @@ Кийинки + Тамгалык-цифралык PIN код түзүү + Цифралык PIN код түзүү @@ -3419,8 +3423,8 @@ Жеке кабинетиңиз үчүн түзгөн PIN кодду киргизиңиз. Бул код SMS аркылуу келген текшерүү кодуңуздан айырмаланып турат. Аккаунтуңуздун PIN кодун киргизиниз. - Тамгалык-цифралык PIN-кодду киргизиңиз - Цифралык PIN-кодду киргизиңиз + + Башка баскычтопко которулуу Туура эмес PIN-код. Кайра кайталаңыз. PIN-кодду унуттуңузбу? Туура эмес PIN-код @@ -5894,13 +5898,11 @@ Жооп берилбегендер боюнча иргелди Баарын тандоо - + Өчүрүү %1$d чалууну өчүрөсүзбү? - - Менде эле өчсүн %1$d чалуу өчүрүлдү diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 09e049c23f..23849f2aa8 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -319,7 +319,7 @@ Nepavyksta atsisiųsti vaizdo įrašo. Turėsi dar kartą ją išsiųsti. - redaguota %1$s + Redaguota %1$s Prisijungti prie skambučio @@ -939,6 +939,8 @@ Nuorodos peržiūra neprieinama Šios grupės nuoroda nėra aktyvi %1$s · %2$s + + Naudokis šia nuoroda, kad prisijungtum prie „Signal“ skambučio @@ -1744,8 +1746,8 @@ Jums liko %1$d bandymas. Jei išnaudosite visus bandymus, galėsite susikurti naują PIN kodą. Galėsite registruotis ir naudotis savo paskyra, bet prarasite kai kuriuos įrašytus nustatymus, kaip pavyzdžiui, profilio informaciją. Signal Registracija - Reikia pagalbos su PIN kodu „Android“ įrenginyje - Įvesti tekstinį PIN kodą - Įvesti skaitinį PIN kodą + + Perjungti klaviatūrą Susikurti PIN kodą @@ -2454,7 +2456,7 @@ Norėdami atsiliepti, suteikite Molly prieigą prie mikrofono. - To answer the video call, give Molly access to your microphone and camera. + Kad atsilieptum į vaizdo skambutį, suteik „Molly“ prieigą prie mikrofono ir kameros. Norint skambinti ar gauti skambučius, Molly reikia mikrofono ir kameros leidimo, tačiau jis buvo visam laikui uždraustas. Pereikite į programėlės nustatymus, pasirinkite „Leidimai“ ir įjunkite „Mikrofoną“ ir „Kamerą“. Atsiliepta susietame įrenginyje. Atmesta susietame įrenginyje. @@ -3611,7 +3613,9 @@ Kitas + Sukurti tekstinį PIN kodą + Sukurti skaitinį PIN kodą @@ -3677,8 +3681,8 @@ Įveskite savo paskyrai sukurtą PIN kodą. Tai yra kas kita, nei jūsų SMS patvirtinimo kodas. Įvesk PIN kodą, kurį susikūrei savo paskyrai. - Įvesti tekstinį PIN kodą - Įvesti skaitinį PIN kodą + + Perjungti klaviatūrą Neteisingas PIN kodas. Bandykite dar kartą. Užmiršote PIN kodą? Neteisingas PIN @@ -6266,7 +6270,7 @@ Filtravimas pagal praleistus Žymėti visus - + Ištrinti Ištrinti %1$d skambutį? @@ -6274,8 +6278,6 @@ Ištrinti %1$d skambučio? Ištrinti %1$d skambučių? - - Ištrinti man Ištrintas %1$d skambutis diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 7ba4fa6be2..4e0fd8d9ec 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -318,7 +318,7 @@ Nevar lejupielādēt video. Jums tas jānosūta vēlreiz. - rediģēta %1$s + Rediģēta %1$s Pievienoties sarunai @@ -911,6 +911,8 @@ Saites priekšskatījums nav pieejams Šī grupas saite nav aktīva. %1$s · %2$s + + Izmantojiet šo saiti, lai pievienotos Signal zvanam @@ -1685,8 +1687,8 @@ Jums atlikuši %1$d mēģinājumi. Ja jums beidzas mēģinājumi, varat izveidot jaunu PIN. Varat reģistrēties un izmantot savu kontu, taču zaudēsiet saglabātos iestatījumus, piemēram, profila informāciju. Signal reģistrācija - Nepieciešama palīdziba ar PIN priekš Android - Ievadiet no burtiem un cipariem sastāvošu PIN - Izveido ciparu PIN + + Pārslēgt tastatūru Izveidojiet savu PIN @@ -2384,7 +2386,7 @@ Lai atbildētu uz zvanu, atļaujiet Molly piekļūt jūsu mikrofonam. - To answer the video call, give Molly access to your microphone and camera. + Lai atbildētu uz video zvanu, atļaujiet Molly piekļūt jūsu mikrofonam un kamerai. Molly nepieciešama atļauja piekļuvei mikrofonam un kamerai, lai zvanītu un saņemtu zvanus, bet tā nav dota. Dodieties uz lietotnes iestatījumiem, izvēlieties \"Atļaujas\" un iespējojiet \"Mikrofons\" un \"Kamera\". Atbildēts no savienotās ierīces Atteikts no savienotās ierīces. @@ -3427,7 +3429,7 @@ Pievienot sākuma ekrānam Izveidot burbuli - Format text + Formatēt tekstu Izvērst uznirstošo logu @@ -3527,7 +3529,9 @@ Tālāk + Izveidojiet burtu/ciparu PIN + Izveido ciparisko PIN @@ -3591,8 +3595,8 @@ Ievadiet sava konta PIN kodu. Tas atšķiras no jūsu SMS verifikācijas koda. Ievadiet savam kontam izveidoto PIN. - Ievadiet no burtiem un cipariem sastāvošu PIN - Izveido ciparisku PIN + + Pārslēgt tastatūru Nepareizs PIN. Mēģiniet vēlreiz. Aizmirsāt PIN? Nepareizs PIN @@ -6043,7 +6047,7 @@ Spoileris - Clear formatting + Notīrīt formatējumu @@ -6142,15 +6146,13 @@ Filtrēts pēc neatbildētajiem Atzīmēt visus - + Dzēst Vai dzēst %1$d zvanus? Vai dzēst %1$d zvanu? Vai dzēst %1$d zvanus? - - Dzēst man Izdzēsti %1$d zvani diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 3d8084de59..26cd2b4db9 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -317,7 +317,7 @@ Не може да се преземе видеото. Ќе мора да ја испратите одново. - изменето %1$s + Изменето %1$s Приклучи се на повикот @@ -883,6 +883,8 @@ Не е достапен преглед за линкот Групниот линк не е активен %1$s · %2$s + + Користете го овој линк за да се приклучите на Signal повик @@ -1626,8 +1628,8 @@ Ви преостануваат уште %1$d обиди. Ако ги искористите сите обиди, можете да создадете нов PIN. Можете да ја регистрирате и користите Вашата сметка, но ќе ги изгубите некои од зачуваните поставувања како што се Вашите информации за профилот. Signal регистрација - Потребна Ви е помош со PIN кодот за Android - Внесете алфанумерички PIN - Внесете нумерички PIN + + Смени тастатура Направете го вашиот PIN @@ -2314,7 +2316,7 @@ За да одговорите на повикот, дозволете пристап на Molly до Вашиот микрофон. - To answer the video call, give Molly access to your microphone and camera. + За да одговорите на видео повикот, дозволете Molly да има пристап до микрофонот и камерата. Molly има потреба од дозвола до микрофонот и камерата за да може да воспоставува повици. Оваа дозвола е трајно одбиена. Ве молиме продолжете до менито за поставувањата, изберете „Дозволи“ и вклучете „Микрофон“ и „Камера“. Одговорено на поврзаниот уред. Одбиено на поврзаниот уред. @@ -3443,7 +3445,9 @@ Следно + Создај алфанумерички PIN + Создај нумерички PIN @@ -3505,8 +3509,8 @@ Го внесете PIN кодот што го создадовте за Вашата сметка. Овој код е различен од Вашиот SMS код за проверка. Внесете го ПИН-от што го создадовте за вашата сметка. - Внесете алфанумерички PIN - Внесете нумерички PIN + + Смени тастатура Погрешен PIN. Обидете се повторно. Го заборавивте PIN-от? Погрешен PIN @@ -6018,14 +6022,12 @@ Филтрирани се пропуштените Избери сѐ - + Избриши Да се избрише %1$d повик? Да се избришат %1$d повици? - - Избриши за мене %1$d повик избришан diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d4b68d56a8..bd91b21a3a 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -883,6 +883,8 @@ ലിങ്ക് പ്രിവ്യൂ ലഭ്യമല്ല ഈ ഗ്രൂപ്പ് ലിങ്ക് സജീവമല്ല %1$s · %2$s + + Signal കോളിൽ ചേരാൻ ഈ ലിങ്ക് ഉപയോഗിക്കുക @@ -1626,8 +1628,8 @@ നിങ്ങൾക്ക് %1$d ശ്രമങ്ങൾ ശേഷിക്കുന്നു. നിങ്ങൾക്ക് ശ്രമങ്ങൾ തീർന്നുപോയാൽ, നിങ്ങൾക്ക് ഒരു പുതിയ പിൻ സൃഷ്ടിക്കാൻ കഴിയും. നിങ്ങൾക്ക് രജിസ്റ്റർ ചെയ്യാനും അക്കൗണ്ട് ഉപയോഗിക്കാനും കഴിയും, പക്ഷേ നിങ്ങളുടെ പ്രൊഫൈൽ വിവരങ്ങൾ പോലുള്ള ചില സംരക്ഷിച്ച ക്രമീകരണങ്ങൾ നഷ്‌ടപ്പെടും. Signal രജിസ്ട്രേഷൻ - Android-നായുള്ള PIN സഹായം ആവശ്യമുണ്ടോ - ആൽഫാന്യൂമെറിക് PIN നൽകുക - സംഖ്യാ PIN നൽകുക + + കീബോർഡ് സ്വിച്ച് ചെയ്യുക നിങ്ങളുടെ PIN സൃഷ്ടിക്കുക @@ -2314,7 +2316,7 @@ ഈ കോളിന് മറുപടി നൽകാൻ, നിങ്ങളുടെ മൈക്രോഫോണ്‍ ഉപയോഗിക്കാന്‍ Molly-നെ അനുവദിക്കുക. - To answer the video call, give Molly access to your microphone and camera. + വീഡിയോ കോളിന് മറുപടി നൽകാൻ, Molly-ന് നിങ്ങളുടെ മൈക്രോഫോണും ക്യാമറയും ആക്‌സസ് ചെയ്യാനുള്ള അനുമതി നൽകുക. കോളുകൾ വിളിക്കുന്നതിനോ സ്വീകരിക്കുന്നതിനോ Molly-ന് മൈക്രോഫോൺ, ക്യാമറ അനുമതികൾ ആവശ്യമാണ്, പക്ഷേ അവ ശാശ്വതമായി നിരസിക്കപ്പെട്ടു. അപ്ലിക്കേഷൻ ക്രമീകരണങ്ങളിൽ തുടരുക, \"അനുമതികൾ\" തിരഞ്ഞെടുത്ത് \"മൈക്രോഫോൺ\", \"ക്യാമറ\" എന്നിവ പ്രവർത്തനക്ഷമമാക്കുക. ഒരു ബന്ധിപ്പിച്ച ഉപകരണത്തിൽ ഉത്തരം നൽകി. ബന്ധിപ്പിച്ച ഉപകരണത്തിൽ നിരസിച്ചു. @@ -3443,7 +3445,9 @@ അടുത്തത് + ആൽഫാന്യൂമെറിക് PIN സൃഷ്ടിക്കുക + സംഖ്യാ PIN സൃഷ്ടിക്കുക @@ -3505,8 +3509,8 @@ നിങ്ങളുടെ അക്കൗണ്ടിനായി നിങ്ങൾ സൃഷ്‌ടിച്ച പിൻ നൽകുക. ഇത് നിങ്ങളുടെ SMS പരിശോധന കോഡിൽ നിന്ന് വ്യത്യസ്തമാണ്. നിങ്ങളുടെ അക്കൗണ്ടിനായി സൃഷ്ടിച്ച PIN നൽകുക. - ആൽഫാന്യൂമെറിക് പിൻ നൽകുക - സംഖ്യാ പിൻ നൽകുക + + കീബോർഡ് സ്വിച്ച് ചെയ്യുക PIN തെറ്റാണ്. വീണ്ടും ശ്രമിക്കുക. PIN മറന്നോ? PIN തെറ്റാണ് @@ -6018,14 +6022,12 @@ മിസ്‌ഡ് അനുസരിച്ച് ഫിൽറ്റർ ചെയ്യൂ എല്ലാം തിരഞ്ഞെടുക്കുക - + ഇല്ലാതാക്കൂ %1$d കോൾ ഇല്ലാതാക്കണോ? %1$d കോളുകൾ ഇല്ലാതാക്കണോ? - - എനിക്കായി ഇല്ലാതാക്കൂ %1$d കോൾ ഇല്ലാതാക്കി diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 7948966bab..207af8c9e6 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -883,6 +883,8 @@ कुठलेही लिंक पुनरावलोकन उपलब्ध नाही ही गट लिंक सक्रिय नाही %1$s · %2$s + + एका Signal कॉलमध्ये सामील होण्यासाठी या लिंकचा वापर करा @@ -1626,8 +1628,8 @@ आपल्याकडे %1$d प्रयत्न उर्वरित आहे. जर आपले प्रयत्न संपले, तर आपण एक नवीन PIN तयार करू शकता. आपण नोंदणी करू शकता आणि आपले खाते वापरू शकता पण काही जतन केलेल्या सेटिंग आपण गमवाल जसे की आपली प्रोफाईल माहिती. Signal नोंदणी - Android करिता PIN साठी मदत हवी - अल्फान्यूमेरिक PIN प्रविष्ट करा - न्यूमेरिक PIN प्रविष्ट करा + + किबोर्ड स्वीच करा आपला PIN तयार करा @@ -2314,7 +2316,7 @@ कॉलला उत्तर देण्यासाठी, Molly ला तुमच्या मायक्रोफोनचा अॅक्सेस द्या. - To answer the video call, give Molly access to your microphone and camera. + व्हिडिओ कॉलचे उत्तर देण्यास, आपल्या मायक्रोफोन आणि कॅमेऱ्याला Molly अ‍ॅक्सेस द्या. कॉल करण्यासाठी किंवा प्राप्त करण्यासाठी Molly ला मायक्रोफोन आणि कॅमेरा परवानग्यांची आवश्यकता असते, पण ती कायमची नाकारली गेली आहे. कृपया अॅप सेटिंग मेनू मध्ये सुरू ठेवा, \"परवानग्या\" निवडा, आणि \"मायक्रोफोन\" आणि \"कॅमेरा\" सक्षम करा. लिंक केलेल्या डिव्हाईसवर उत्तर दिले. लिंक केलेल्या डिव्हाईसवर नकार दिला. @@ -3443,7 +3445,9 @@ पुढे + अल्फान्यूमेरिक PIN तयार करा + न्यूमेरिक PIN तयार करा @@ -3505,8 +3509,8 @@ आपल्या खात्यासाठी आपण तयार केलेला PIN प्रविष्ट करा. हे आपल्या SMS सत्यापन कोडपेक्षा वेगळे आहे. आपल्या अकाऊंटसाठी आपण तयार केलेला पिन प्रविष्ट करा. - अल्फान्यूमेरिक PIN प्रविष्ट करा - न्यूमेरिक PIN प्रविष्ट करा + + किबोर्ड स्वीच करा चुकीचा PIN.पुन्हा प्रयत्न करा. PIN विसरलात? चुकीचा PIN @@ -6018,14 +6022,12 @@ मिस्ड कॉलनुसार फिल्टर केले सर्व निवडा - + हटवा %1$d कॉल हटवायचा? %1$d कॉल्स हटवायचे? - - माझ्यासाठी हटवा %1$d कॉल हटवला diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 64b06abc4f..5444dcee55 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -316,7 +316,7 @@ Tidak dapat memuat turun video. Anda perlu menghantarnya semula. - diedit %1$s + Diedit %1$s Sertai panggilan @@ -855,6 +855,8 @@ Pratonton pautan tidak tersedia Pautan kumpulan ini tidak aktif %1$s · %2$s + + Gunakan pautan ini untuk menyertai Panggilan Signal @@ -1567,8 +1569,8 @@ Anda mempunyai %1$d percubaan yang tinggal. Jika anda kehabisan peluang percubaan, anda boleh mencipta PIN baharu. Anda boleh mendaftar dan menggunakan akaun anda tetapi anda akan kehilangan sesetengah tetapan yang disimpan seperti maklumat profil anda. Pendaftaran Signal - Perlukan Bantuan dengan PIN untuk Android - Masukkan PIN abjad angka - Masukkan PIN angka + + Tukar papan kekunci Cipta PIN anda @@ -2244,7 +2246,7 @@ Untuk menjawab panggilan, berikan Molly akses kepada mikrofon anda. - To answer the video call, give Molly access to your microphone and camera. + Untuk menjawab panggilan video, berikan Molly akses kepada mikrofon dan kamera anda. Molly memerlukan kebenaran Mikrofon dan Kamera untuk memanggil atau menerima panggilan, tetapi telah ditolak secara kekal. Sila terus ke menu tetapan aplikasi, pilih \"Kebenaran\", dan dayakan \"Mikrofon\" dan \"Kamera\". Dijawab pada peranti yang dipautkan. Ditolak pada peranti yang dipautkan. @@ -3359,7 +3361,9 @@ Seterusnya + Cipta PIN abjad angka + Cipta PIN angka @@ -3419,8 +3423,8 @@ Masukkan PIN yang anda buat untuk akaun anda. Ini berbeza dengan kod pengesahan SMS anda. Masukkan PIN yang anda buat untuk akaun anda. - Masukkan PIN abjad angka - Masukkan PIN angka + + Tukar papan kekunci PIN tidak betul. Cuba lagi. Terlupa PIN? PIN tidak betul @@ -5894,13 +5898,11 @@ Tapis mengikut panggilan terlepas Pilih semua - + Padam Padamkan %1$d panggilan? - - Padam untuk saya %1$d panggilan dipadamkan diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index e74624da88..53c609a25b 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -855,6 +855,8 @@ လင့်ခ်ကို ကြိုကြည့်ခြင်း မရရှိပါ ဤအဖွဲ့လင့်ခ်ကို အသုံးမပြုတော့ပါ။ %1$s · %2$s + + Signal ကောလ်တွင် ပါဝင်ရန် ဤလင့်ခ်ကို သုံးပါ @@ -1567,8 +1569,8 @@ သင့်တွင် ကြိုးစားရန် %1$d ကြိမ် ကျန်ရှိပါတော့သည်။ ကြိုးစားနိုင်ရန်အကြိမ် မရှိတော့လျှင် ပင်နံပါတ်အသစ်တစ်ခု ဖန်တီးနိုင်ပါသည်။ မှတ်ပုံစာရင်းသွင်း၍ သင့်အကောင့်ကို ပြန်သုံးနိုင်ပါသည်၊ သို့သော် သိမ်းဆည်းထားသော အချို့သောအပြင်အဆင်များ (ဥပမာ သင့်ပရိုဖိုင်း အချက်အလက်များ) ဆုံရှုံးပါမည်။ Signal မှတ်ပုံတင်ခြင်း - Android ပေါ်တွင် ပင်နံပါတ်နှင့် ပါတ်သက်သော အကူအညီရယူလိုပါသည် - အက္ခရာနံပါတ်ပါ ပင်နံပါတ်ကို ရိုက်ထည့်ပါ - နံပါတ်ပါ ပင်နံပါတ်ကို ရိုက်ထည့်ပါ + + ကီးဘုတ် ပြောင်းရန် သင်၏ပင်နံပါတ်ကိုဖန်တီးပါ @@ -2244,7 +2246,7 @@ ကောလ် ဖြေဆိုရန် Molly အား သင့်မိုက်ခရိုဖုန်းကို သုံးခွင့်ပေးပါ။ - To answer the video call, give Molly access to your microphone and camera. + ဗီဒီယိုကောလ် ဖြေဆိုရန် Molly အား သင့်မိုက်ခရိုဖုန်းနှင့် ကင်မရာကို သုံးခွင့်ပေးပါ။ ဖုန်းခေါ်ဆိုမှုပြုနိုင်ရန် Mollyမှ မိုက်ခရိုဖုန်း နှင့် ကင်မရာအား အသုံးပြုခွင့် ရရန်လိုအပ်သည်။ သို့သော် လုံးဝခွင့်မပြုပါ ဟုရွေးထားပြီး ဖြစ်နေသဖြင့် အပ်ပလီကေးရှင်း အပြင်အဆင်သို့ သွား၍ ခွင့်ပြုချက်များကို ရွေးချယ်ကာ မိုက်ခရိုဖုန်း နှင့် ကင်မရာကို အသုံးပြုနိုင်အောင် ပြုလုပ်ပါ။ ချိတ်ဆက်ထားသော စက်ပေါ်တွင် ဖြေဆိုခဲ့ပါသည်။ ချိတ်ဆက်ထားသော စက်ပေါ်တွင် ငြင်းဆိုခဲ့ပါသည်။ @@ -3359,7 +3361,9 @@ နောက်ထက် + အက္ခရာနံပါတ်ပါ ပင်နံပါတ် ကိုဖန်တီးပါ + နံပါတ်ပါ ပင်နံပါတ်ကိုဖန်တီးပါ @@ -3419,8 +3423,8 @@ သင့်အကောင့်အတွက် ဖန်တီးထားသော ပင်နံပါတ် ထည့်ပါ၊ SMS စာတိုအနေဖြင့်ပေးပို့သည့် အတည်ပြုကုဒ်နှင့် မတူပါ။ သင့်အကောင့်အတွက် သင်ဖန်တီးထားသော PIN ကို ရိုက်ထည့်ပါ။ - အက္ခရာနံပါတ်ပါ ပင်နံပါတ်ကို ရိုက်ထည့်ပါ - နံပါတ်ပါ ပင်နံပါတ်ကို ရိုက်ထည့်ပါ + + ကီးဘုတ် ပြောင်းရန် ပင်နံပါတ်မှားနေသည်။ နောက်တစ်ကြိမ် ကြိုးစားပါ။ ပင်နံပါတ်ကို မေ့သွားပြီလား? ပင်နံပါတ်မှားနေသည် @@ -5894,13 +5898,11 @@ လွတ်သွားသည်များကို စစ်ထုတ်ပြီး အားလုံး ရွေးရန် - + ဖျက်ရန် ကောလ် %1$d ခုကို ဖျက်မည်လား။ - - ကျွန်ုပ်အတွက် ဖျက်မည် ကောလ် %1$d ခုကို ဖျက်လိုက်ပါပြီ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index d0097fe686..a738dc0b5d 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -317,7 +317,7 @@ Kunne ikke laste ned videoen. Du må sende den på nytt. - redigert %1$s + Redigert %1$s Bli med i samtalen @@ -883,6 +883,8 @@ Ingen forhåndsvisning tilgjengelig Denne gruppelenken er ikke aktiv %1$s · %2$s + + Bruk denne lenken for å bli med i en Signal-samtale @@ -1626,8 +1628,8 @@ Du har %1$d forsøk igjen. Hvis du går tom for forsøk, kan du opprette en ny PIN. Du kan registrere og bruke kontoen din, men du vil miste noen lagrede innstillinger som profilinformasjonen din. Signal Registrering - Hjelp ønskes til PIN for Android - Skriv inn alfanumerisk PIN - Skriv inn numerisk PIN + + Bytt tastatur Opprett PIN-koden @@ -2314,7 +2316,7 @@ Gi Molly tilgang til mikrofonen for å svare på anropet. - To answer the video call, give Molly access to your microphone and camera. + Gi Molly tilgang til mikrofonen og kameraet for å svare på videoanropet. Molly krever tillatelser fra systemet for å kunne ringe eller motta samtaler, men du har valgt å avslå minst én av disse permanent. Gå til «Apper»-menyen på systemet og slå på tillatelser for «Mikrofon» og «Kamera». Besvart på en koblet enhet. Avslått på en koblet enhet. @@ -3443,7 +3445,9 @@ Neste + Opprett alfanumerisk PIN-kode + Opprett numerisk PIN-kode @@ -3505,8 +3509,8 @@ Skriv inn PIN-koden du opprettet for kontoen din. Dette er forskjellig fra SMS-bekreftelseskoden. Angi PIN-koden som er knyttet til kontoen din. - Skriv inn alfanumerisk PIN - Skriv inn numerisk PIN + + Bytt tastatur Feil PIN-kode. Prøv igjen. Har du glemt PIN-koden? Feil PIN-kode @@ -6018,14 +6022,12 @@ Viser kun ubesvarte Velg alle - + Slett Vil du slette %1$d anrop? Vil du slette %1$d anrop? - - Slett for meg %1$d anrop slettet diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 571d32b4d0..8da7184616 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -317,7 +317,7 @@ Kan video niet downloaden. Je zal het opnieuw moeten verzenden. - bewerkt %1$s + Bewerkt %1$s Aan oproep deelnemen @@ -883,6 +883,8 @@ Geen voorbeeldafbeelding beschikbaar Deze groepslink is niet actief %1$s · %2$s + + Gebruik deze link om deel te nemen aan een Signal-oproep @@ -949,7 +951,7 @@ Wie mogen nieuwe leden toevoegen? - Wie mogen de groepsinformatie aanpassen? + Wie mogen de groepsinformatie bewerken? %1$d persoon toegevoegd. @@ -964,7 +966,7 @@ Het bijwerken van de groep is mislukt. Probeer het later opnieuw Het bijwerken van de groep is mislukt vanwege een netwerkfout. Probeer het later opnieuw - Naam en afbeelding aanpassen + Naam en afbeelding bewerken Verouderde groep Dit is een verouderde groep. Om nieuwe functionaliteiten zoals groepsbeheer te kunnen gebruiken moet je een nieuwe groep aanmaken. Dit is een verouderde groep; om nieuwe functionaliteiten te kunnen gebruiken zoals @vermeldingen en beheerders, @@ -1387,14 +1389,14 @@ De groepsafbeelding is aangepast. - Je hebt de instelling voor wie de groepsinformatie kan wijzigen op ‘%1$s’ ingesteld. - %1$s heeft de instelling voor wie de groepsinformatie kan wijzigen op ‘%2$s’ ingesteld. - De instelling voor wie de groepsinformatie kan aanpassen is op ‘%1$s’ ingesteld. + Je hebt de instelling voor wie de groepsinformatie kan bewerken op ‘%1$s’ ingesteld. + %1$s heeft de instelling voor wie de groepsinformatie kan bewerken op ‘%2$s’ ingesteld. + De instelling voor wie de groepsinformatie kan bewerken is op ‘%1$s’ ingesteld. - Je hebt de instelling voor wie de groepslidmaatschap kan aanpassen op ‘%1$s’ ingesteld. - %1$s heeft de instelling voor wie de groepslidmaatschap kan aanpassen op ‘%2$s’ ingesteld. - De instelling voor wie de groepslidmaatschap kan aanpassen is op ‘%1$s’ ingesteld. + Je hebt de instelling voor wie de groepslidmaatschap kan bewerken op ‘%1$s’ ingesteld. + %1$s heeft de instelling voor wie de groepslidmaatschap kan bewerken op ‘%2$s’ ingesteld. + De instelling voor wie de groepslidmaatschap kan bewerken is op ‘%1$s’ ingesteld. Je hebt de groepsinstellingen aangepast om alle groepsleden de mogelijkheid te geven om berichten te verzenden en oproepen te beginnen. @@ -1626,8 +1628,8 @@ Je hebt nog %1$d pogingen resterend. Wanneer je geen pogingen meer over hebt, dan kun je nog wel een nieuwe pincode aanmaken. Je kunt Signal dan wel weer registreren met dit telefoonnummer, maar sommige instellingen zoals je profielnaam, -foto en -omschrijving zullen dan verloren gaan. Signal-registratratie - Ik heb hulp nodig bij de Signal-pincode op een Android apparaat - Een alfanumerieke pincode invoeren - Een numerieke pincode invoeren + + Toetsenbord wisselen Pincode aanmaken @@ -1839,7 +1841,7 @@ Er wordt een verificatiecode naar dit nummer gestuurd. Providertarieven kunnen van toepassing zijn. Je zal gebeld worden om dit telefoonnummer te verifiëren. Is je telefoonnummer hierboven correct? - Telefoonnummer wijzigen + Telefoonnummer bewerken De Google Play Services zijn niet aanwezig Dit apparaat bevat geen Google Play Services. Je kunt Molly nog steeds gebruiken, maar deze configuratie kan de betrouwbaarheid en prestaties verslechteren.\n\nAls je geen gevorderde gebruiker bent, geen aangepaste ROM gebruikt of denkt dat dit bericht onterecht wordt weergegeven, neem dan contact op met support@molly.im voor hulp met probleemoplossen. Ik begrijp het @@ -2314,7 +2316,7 @@ Je moet Molly toestaan om de microfoon te gebruiken voor je de oproep kunt beantwoorden. - To answer the video call, give Molly access to your microphone and camera. + Geef Molly toegang tot je microfoon en camera om de video-oproep te beantwoorden. Molly heeft toegang nodig tot de microfoon en de camera om oproepen te maken of te ontvangen, maar deze toegang is pertinent geweigerd. Ga naar de instellingen voor deze app, tik op ‘Machtigingen’ en schakel ‘Microfoon’ en ‘Camera’ in. Beantwoord vanaf een gekoppeld apparaat. Geweigerd vanaf een gekoppeld apparaat. @@ -2677,7 +2679,7 @@ Ik werk aan iets nieuws - Groep aanpassen + Groep bewerken Groepsnaam Groepsomschrijving @@ -3443,7 +3445,9 @@ Volgende + Alfanumerieke pincode aanmaken + Numerieke pincode aanmaken @@ -3505,8 +3509,8 @@ Voer de pincode in welke je eerder hebt aangemaakt om informatie versleuteld op te slaan op Signals servers. Dit is niet dezelfde code als de telefoonnummerverificatie-code die je per sms of telefoonoproep hebt ontvangen. Voer de pincode in die je voor je account hebt gemaakt. - Een alfanumerieke pincode invoeren - Een numerieke pincode invoeren + + Toetsenbord wisselen Foutieve pincode, probeer het opnieuw. Pincode vergeten? Foutieve pincode @@ -3835,7 +3839,7 @@ Uit de groep verwijderen De beheerdersbevoegdheden van %1$s intrekken? - "“%1$s” zal de groepsinformatie en groepslidmaatschap kunnen aanpassen." + "“%1$s” zal de groepsinformatie en groepslidmaatschap kunnen bewerken." %1$s uit deze groep verwijderen? @@ -4501,12 +4505,12 @@ Groepsleden toevoegen - Groepsinformatie aanpassen + Groepsinformatie bewerken Berichten verzenden & oproepen beginnen Alle groepsleden Uitsluitend beheerders Wie mogen nieuwe leden toevoegen? - Wie mogen de groepsinformatie aanpassen? + Wie mogen de groepsinformatie bewerken? Wie mogen berichten verzenden en oproepen beginnen? @@ -4908,7 +4912,7 @@ Opslaan - Dit meldingsprofiel aanpassen + Dit meldingsprofiel bewerken Er bestaat al een meldingsprofiel met deze naam @@ -4954,7 +4958,7 @@ Wissen - Meldingsprofiel aanpassen + Meldingsprofiel bewerken Elke dag @@ -5264,7 +5268,7 @@ - Verhaalnaam wijzigen + Verhaalnaam bewerken Verhaalnaam @@ -6018,14 +6022,12 @@ Gefilterd op gemist Alles selecteren - + Wissen %1$d oproep verwijderen? %1$d oproepen verwijderen? - - Voor mij wissen %1$d oproep verwijderd diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index a7feba90ea..ba3107a881 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -883,6 +883,8 @@ ਕੋਈ ਲਿੰਕ ਝਲਕ ਉਪਲਬਧ ਨਹੀਂ ਇਹ ਗਰੁੱਪ ਲਿੰਕ ਕਿਰਿਆਸ਼ੀਲ ਨਹੀਂ ਹੈ %1$s . %2$s + + Signal ਕਾਲ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਣ ਲਈ ਇਸ ਲਿੰਕ ਦੀ ਵਰਤੋਂ ਕਰੋ @@ -1626,8 +1628,8 @@ ਤੁਹਾਡੇ ਕੋਲ %1$d ਕੋਸ਼ਿਸ਼ਾਂ ਬਾਕੀ ਹਨ। ਤੁਹਾਡੀਆਂ ਕੋਸ਼ਿਸ਼ਾਂ ਖਤਮ ਹੋ ਜਾਣ \'ਤੇ, ਤੁਸੀਂ ਇੱਕ ਨਵਾਂ PIN ਬਣਾ ਸਕਦੇ ਹੋ। ਤੁਸੀਂ ਰਜਿਸਟਰ ਕਰਕੇ ਆਪਣਾ ਖਾਤਾ ਵਰਤ ਸਕਦੇ ਹੋ ਪਰ ਤੁਸੀਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਕੁਝ ਜਾਣਕਾਰੀ ਗੁਆ ਬੈਠੋਗੇ ਜਿਵੇਂ ਕਿ ਤੁਹਾਡੀ ਪ੍ਰੋਫ਼ਾਈਲ ਦੀ ਜਾਣਕਾਰੀ। Signal ਰਜਿਸਟ੍ਰੇਸ਼ਨ - Android ਦੇ ਲਈ PIN ਨੂੰ ਲੈਕੇ ਮਦਦ ਦੀ ਲੋੜ ਹੈ - ਅੰਕਾਂ-ਅੱਖਰਾਂ ਵਾਲਾ PIN ਪਾਓ - ਅੰਕਾਂ ਵਾਲਾ PIN ਪਾਓ + + ਕੀਬੋਰਡ ਬਦਲੋ ਆਪਣਾ PIN ਬਣਾਓ @@ -2314,7 +2316,7 @@ ਕਾਲ ਦਾ ਜਵਾਬ ਦੇਣ ਲਈ Molly ਨੂੰ ਆਪਣੇ ਮਾਈਕਰੋਫ਼ੋਨ ਲਈ ਪਹੁੰਚ ਦਿਓ। - To answer the video call, give Molly access to your microphone and camera. + ਵੀਡੀਓ ਕਾਲ ਦਾ ਜਵਾਬ ਦੇਣ ਲਈ, Molly ਨੂੰ ਆਪਣੇ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਅਤੇ ਕੈਮਰੇ ਤੱਕ ਪਹੁੰਚ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ। Molly ਨੂੰ ਕਾਲ ਕਰਨ ਲਈ ਮਾਈਕ੍ਰੋਫੋਨ ਅਤੇ ਕੈਮਰਾ ਇਜਾਜ਼ਤਾਂ ਦੀ ਲੋੜ ਹੈ, ਪਰ ਇਹਨਾਂ ਲਈ ਸਥਾਈ ਤੌਰ ’ਤੇ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ| ਕਿਰਪਾ ਕਰਕੇ ਐਪ ਸੈਟਿੰਗਾਂ ’ਤੇ ਜਾਰੀ ਰੱਖੋ, \"ਇਜਾਜ਼ਤਾਂ\" ਚੁਣੋ, ਅਤੇ \"ਮਾਈਕ੍ਰੋਫ਼ੋਨ\" ਅਤੇ \"ਕੈਮਰਾ\" ਨੂੰ ਸਮਰੱਥ ਕਰੋ। ਲਿੰਕ ਕੀਤੀ ਡਿਵਾਈਸ ’ਤੇ ਜਵਾਬ ਦਿੱਤਾ। ਲਿੰਕ ਕੀਤੀ ਡਿਵਾਈਸ ’ਤੇ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ। @@ -3443,7 +3445,9 @@ ਅਗਲਾ + ਅੰਕ-ਅੱਖਰੀ PIN ਬਣਾਓ + ਅੰਕਾਂ ਨਾਲ PIN ਬਣਾਓ @@ -3505,8 +3509,8 @@ ਆਪਣੇ ਖਾਤੇ ਲਈ ਤੁਹਾਡੇ ਵੱਲੋਂ ਬਣਾਇਆ PIN ਦਿਓ। ਇਹ ਤੁਹਾਡੇ SMS ਤਸਦੀਕ ਕੋਡ ਤੋਂ ਵੱਖਰਾ ਹੁੰਦਾ ਹੈ. ਉਹ PIN ਦਰਜ ਕਰੋ ਜੋ ਤੁਸੀਂ ਆਪਣੇ ਖਾਤੇ ਲਈ ਬਣਾਇਆ ਹੈ। - ਅੱਖਰਾਂ ਅਤੇ ਸੰਖਿਆਵਾਂ ਤੋਂ ਬਣਿਆ PIN ਦਰਜ ਕਰੋ - ਅੰਕਾਂ ਵਾਲਾ ਪਿੰਨ ਦਿਓ + + ਕੀਬੋਰਡ ਬਦਲੋ ਗਲਤ PIN। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਕੀ PIN ਭੁੱਲ ਗਏ ਹੋ? ਗਲਤ PIN @@ -6018,14 +6022,12 @@ ਮਿਸਡ ਕਾਲਾਂ ਅਨੁਸਾਰ ਫਿਲਟਰ ਕੀਤਾ ਗਿਆ ਸਭ ਨੂੰ ਚੁਣੋ - + ਮਿਟਾਓ ਕੀ %1$d ਕਾਲ ਨੂੰ ਮਿਟਾਉਣਾ ਹੈ? ਕੀ %1$d ਕਾਲਾਂ ਨੂੰ ਮਿਟਾਉਣਾ ਹੈ? - - ਮੇਰੇ ਲਈ ਮਿਟਾਓ %1$d ਕਾਲ ਮਿਟਾਈ ਗਈ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e8a8e3293a..a49a00ebac 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -319,7 +319,7 @@ Nie udało się pobrać wideo. Wyślij je ponownie. - edytowana %1$s + Edytowana %1$s Dołącz do rozmowy @@ -939,6 +939,8 @@ Brak dostępnego podglądu linku Link do tej grupy jest nieaktywny %1$s · %2$s + + Użyj linku, aby dołączyć do rozmowy Signal @@ -1744,8 +1746,8 @@ Pozostały Ci %1$d próby. Jeśli się skończą, możesz utworzyć nowy PIN. Możesz zarejestrować się i używać swojego konta, ale stracisz niektóre zapisane ustawienia, takie jak informacje w Twoim profilu. Rejestracja Signal - Potrzebna pomoc z kodem PIN Signal dla systemu Android - Wpisz alfanumeryczny PIN - Wpisz liczbowy PIN + + Przełącz klawiaturę Utwórz swój PIN @@ -2454,7 +2456,7 @@ Aby odebrać połączenie, zezwól Molly na dostęp do mikrofonu. - To answer the video call, give Molly access to your microphone and camera. + Aby odebrać połączenie wideo, zezwól Molly na dostęp do mikrofonu i kamery. Molly wymaga pozwolenia na dostęp do mikrofonu i aparatu w celu odbierania oraz wykonywania połączeń, ale zostało one na stałe odrzucone. Przejdź do ustawień aplikacji, wybierz \"Uprawnienia\" i włącz \"Mikrofon\" oraz \"Aparat\". Odebrane na połączonym urządzeniu. Odrzucone na połączonym urządzeniu. @@ -3510,7 +3512,7 @@ Dodaj do ekranu głównego Utwórz dymek - Format text + Formatuj tekst Powiększ okno @@ -3611,7 +3613,9 @@ Dalej + Utwórz alfanumeryczny PIN + Utwórz liczbowy PIN @@ -3677,8 +3681,8 @@ Wpisz kod PIN, który utworzyłeś(aś) dla swojego konta. Ten kod nie jest Twoim kodem weryfikacyjnym SMS. Wprowadź kod PIN utworzony dla tego konta. - Wpisz alfanumeryczny PIN - Wpisz liczbowy PIN + + Przełącz klawiaturę Nieprawidłowy PIN. Spróbuj ponownie. Zapomniałeś(aś) PIN? Nieprawidłowy PIN @@ -6166,7 +6170,7 @@ Zasłonięte - Clear formatting + Wyczyść formatowanie @@ -6266,7 +6270,7 @@ Filtruj po nieodebranych Zaznacz wszystko - + Usuń Skasować %1$d połączenie? @@ -6274,8 +6278,6 @@ Skasować %1$d połączeń? Skasować %1$d połączenia ? - - Usuń u mnie skasowano %1$d połączenie diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d93e5fc823..280fc37bd0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -317,7 +317,7 @@ Não foi possível baixar o vídeo. Você terá que enviar o vídeo novamente. - editado há %1$s + Editado há %1$s Participar da chamada @@ -883,6 +883,8 @@ Nenhuma pré-visualização disponível para o link O link deste grupo não está ativo %1$s · %2$s + + Use este link para participar de uma chamada do Signal @@ -1626,8 +1628,8 @@ Você tem %1$d tentativas restantes. Se não acertar, pode criar um novo PIN. Você pode se registrar e usar sua conta, mas perderá algumas configurações salvas como as informações do seu perfil. Registro no Signal - Preciso de ajuda com o PIN para Android - Digite o PIN alfanumérico - Digite o PIN numérico + + Alternar teclado Criar seu PIN @@ -2314,7 +2316,7 @@ Para atender a chamada, conceda ao Molly acesso ao seu microfone. - To answer the video call, give Molly access to your microphone and camera. + Para atender a videochamada, permita que o Molly acesse seu microfone e câmera. O Molly precisa das permissões Microfone e Câmera para fazer ou receber chamadas, mas elas foram permanentemente negadas. Favor ir no menu de configurações de aplicativos, selecionar \"Permissões\", e habilitar \"Microfone\" e \"Câmera\". Atendido em um dispositivo vinculado. Recusado em um dispositivo vinculado. @@ -3443,7 +3445,9 @@ Avançar + Criar um PIN alfanumérico + Criar PIN numérico @@ -3505,8 +3509,8 @@ Digite o PIN que você criou para sua conta. Ele é diferente do seu código de verificação por SMS. Insira o PIN que criou para sua conta. - Digite o PIN alfanumérico - Digite o PIN numérico + + Alternar teclado PIN incorreto. Tente novamente. Esqueceu seu PIN? PIN incorreto @@ -6018,14 +6022,12 @@ Filtrar por chamadas perdidas Selecionar todas - + Excluir Excluir %1$d chamada? Excluir %1$d chamadas? - - Excluir para mim %1$d chamada excluída diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 1fddb47933..64644c58aa 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -317,7 +317,7 @@ Não é possível transferir o vídeo. {0} terá de a enviar outra vez. - editada %1$s + Editada %1$s Entrar na chamada @@ -883,6 +883,8 @@ Pré-visualização de hiperligação indisponível Este link de grupo não se encontra ativo %1$s · %2$s + + Use este link para se juntar à chamada do Signal @@ -1626,8 +1628,8 @@ Restam-lhe %1$d tentativas. Quando terminarem as tentativas poderá criar um novo PIN. Poderá voltar a registar-se e utilizar a sua conta mas irá perder algumas definições guardadas, como a sua informação do perfil. Registo Signal - Necessita de ajuda com o PIN para Android - Introduza um PIN alfanumérico - Introduza um PIN numérico + + Trocar de teclado Crie o seu PIN @@ -2314,7 +2316,7 @@ Para poder atender a chamada, conceda ao Molly o acesso ao seu microfone. - To answer the video call, give Molly access to your microphone and camera. + Para poder atender a videochamada, conceda ao Molly o acesso ao seu microfone e câmara. O Molly requer permissões de acesso ao microfone e à câmara, para efectuar e receber chamadas, mas estas foram negadas permanentemente. Por favor, aceda às definições das aplicações do seu telemóvel, selecione a aplicação Molly e, em \"Permissões\" ative o \"Microfone\" e a \"Câmara\". Atendida num dispositivo associado. Recusada num dispositivo associado. @@ -3443,7 +3445,9 @@ Seguinte + Criar um PIN alfanumérico + Criar um PIN numérico @@ -3505,8 +3509,8 @@ Introduza o PIN que criou para a sua conta. Ele é diferente do seu código de verificação SMS. Insira o PIN que criou para a sua conta. - Introduza um PIN alfanumérico - Introduza um PIN numérico + + Trocar de teclado PIN incorreto. Tente novamente. Esqueceu-se do PIN? PIN incorreto @@ -6018,14 +6022,12 @@ Filtrar por perdidas Selecionar tudo - + Eliminar Eliminar %1$d chamada? Eliminar %1$d chamadas? - - Eliminar para mim %1$d chamada eliminada diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2164bb7b3b..ba472fedb3 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -318,7 +318,7 @@ Nu se poate descărca videoclipul. Va trebui să îl trimiți din nou. - editat la %1$s + Editat la %1$s Alătură-te apelului @@ -911,6 +911,8 @@ Previzualizarea linkului nu este disponibilă Acest link de grup nu este activ %1$s · %2$s + + Folosește acest link pentru a te alătura unui Apel Signal @@ -1685,8 +1687,8 @@ Mai ai %1$d de încercări rămase. Dacă rămâi fără încercări, poți crea un cod PIN nou. Poți înregistra și utiliza contul, dar vei pierde unele setări salvate, cum ar fi informațiile despre profil. Înregistrare Signal - Am nevoie de asistență pentru PIN Android - Introdu un PIN alfanumeric - Introdu un PIN numeric + + Schimbă tastaturile Creează-ți PIN-ul @@ -2384,7 +2386,7 @@ Pentru a răspunde, permite lui Molly accesul la microfon. - To answer the video call, give Molly access to your microphone and camera. + Ca să răspunzi la apelul video, dă Acces pentru Molly la microfon și cameră. Molly are nevoie de permisiunile pentru Microfon și Cameră pentru a putea primi apeluri dar i-a fost refuzat accesul permanent. Te rugăm mergi în meniul de setări al aplicației, selectează \"Permisiuni\" și activează \"Microfon\" și \"Cameră\". Răspuns de pe un dispozitiv asociat. Respins de pe un dispozitiv asociat. @@ -3527,7 +3529,9 @@ Următorul + Creează un PIN alfanumeric + Creează un PIN numeric @@ -3591,8 +3595,8 @@ Introdu codul PIN pe care l-ai creat pentru contul tău. Acesta este diferit față de codul de verificare prin SMS. Introdu PIN-ul pe care l-ai creat pentru contul tău. - Introdu un PIN alfanumeric - Introdu un PIN numeric + + Schimbă tastaturile PIN incorect. Încearcă din nou. Ai uitat PIN-ul? PIN incorect @@ -6142,15 +6146,13 @@ Filtrat pe bază de ratate Selectează tot - + Șterge Ștergi %1$d apel? Ștergi %1$d apeluri? Ștergi %1$d de apeluri? - - Șterge doar pentru mine %1$d apel șters diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2a184e6073..f42dc8aefb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -319,7 +319,7 @@ Невозможно загрузить видео. Вам нужно отправить его снова. - изменено %1$s + Изменено %1$s Присоединиться к звонку @@ -939,6 +939,8 @@ Нет предпросмотра ссылки Эта ссылка на группу неактивна %1$s · %2$s + + Используйте эту ссылку, чтобы присоединиться к звонку Signal @@ -1744,8 +1746,8 @@ У вас осталось %1$d попыток. Если вы исчерпаете попытки, вы сможете создать новый PIN-код. Вы сможете зарегистрироваться и использовать свою учётную запись, но потеряете некоторые сохранённые настройки, например информацию вашего профиля. Регистрация в Signal - Нужна помощь с PIN-кодом для Android - Ввести буквенно-цифровой PIN-код - Ввести цифровой PIN-код + + Переключить клавиатуру Создать PIN-код @@ -2454,7 +2456,7 @@ Для ответа на звонок предоставьте Molly доступ к микрофону. - To answer the video call, give Molly access to your microphone and camera. + Для ответа на видеозвонок предоставьте Molly доступ к микрофону и камере. Molly требуются разрешения на доступ к микрофону и камере для совершения или принятия звонков, но они были вами отклонены. Нажмите «Продолжить», чтобы перейти в настройки приложения, откройте «Разрешения» и включите «Микрофон» и «Камера». Принят на привязанном устройстве. Отклонён на привязанном устройстве. @@ -3611,7 +3613,9 @@ Далее + Создать буквенно-цифровой PIN-код + Создать цифровой PIN-код @@ -3677,8 +3681,8 @@ Введите PIN-код, который вы создали для своей учётной записи. Это не то же самое. что проверочный код из SMS. Введите Пин-код, созданный для вашей учётной записи. - Ввести буквенно-цифровой PIN-код - Ввести цифровой PIN-код + + Переключить клавиатуру Неверный PIN-код. Попробуйте ещё раз. Забыли PIN-код? Неверный PIN-код @@ -6266,7 +6270,7 @@ Отфильтровано по пропущенным Выбрать все - + Удалить Удалить %1$d звонок? @@ -6274,8 +6278,6 @@ Удалить %1$d звонков? Удалить %1$d звонка? - - Удалить для меня %1$d звонок удалён diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index efe01e4d22..780783b85a 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -319,7 +319,7 @@ Video sa nepodarilo stiahnuť. Musíte ho odoslať znova. - upravené %1$s + Upravené %1$s Pripojiť sa k hovoru @@ -939,6 +939,8 @@ Náhľad odkazu nedostupný Tento odkaz do skupiny nie je aktívny %1$s . %2$s + + Na pripojenie k Signal hovoru použite tento odkaz @@ -1744,8 +1746,8 @@ Zostáva vám ešte %1$d pokusov. Ak počet pokusov vyčerpáte, môžete si vytvoriť nový PIN. Môžete sa zaregistrovať a používať svoj účet naďalej, ale stratíte niektoré uložené nastavenia, ako napríklad profilové informácie. Signal registrácia - potrebujem pomoct s PIN na Androide - Zadajte alfanumerický PIN kód - Zadajte číselný PIN kód + + Prepnúť klávesnicu Vytvorte si PIN @@ -2454,7 +2456,7 @@ Ak chcete prijať hovor, poskytnite Molly prístup k mikrofónu. - To answer the video call, give Molly access to your microphone and camera. + Ak chcete prijať videohovor, poskytnite Mollyu prístup k mikrofónu a kamere. Molly potrebuje prístup k mikrofónu a fotoaparátu aby mohol volať alebo prijímať hovory, ale prístup bol natrvalo zakázaný. Prosím v nastaveniach aplikácií zvoľte \"Oprávnenia\", a povoľte \"Mikrofón\" a \"Fotoaparát\". Prijaté na pripojenom zariadení. Odmietnuté na pripojenom zariadení. @@ -3611,7 +3613,9 @@ Ďalší + Vytvoriť alfanumerický PIN kód + Vytvoriť numerický PIN kód @@ -3677,8 +3681,8 @@ Zadajte PIN, ktorý ste si pre váš účet vytvorili. Nie je to to isté ako SMS overovací kód. Zadajte PIN kód, ktorý ste vytvorili pre svoj účet. - Zadajte alfanumerický PIN kód - Zadajte číselný PIN kód + + Prepnúť klávesnicu Nesprávny PIN kód. Skúste to znova. Zabudli ste PIN? Nesprávny PIN @@ -6266,7 +6270,7 @@ Filtrovať podľa zmeškaných Označiť všetko - + Vymazať Vymazať %1$d hovor? @@ -6274,8 +6278,6 @@ Vymazať %1$d hovoru? Vymazať %1$d hovorov? - - Vymazať pre mňa %1$d hovor vymazaný diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 63a283ba67..3a15c90e22 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -319,7 +319,7 @@ Videoposnetka ni mogoče prenesti. Ponovno ga boste morali poslati. - urejeno pred %1$s + Urejeno pred %1$s Pridruži se klicu @@ -939,6 +939,8 @@ Predogled ni na voljo Povezava do skupine ni aktivna %1$s · %2$s + + Uporabite to povezavo, da se pridružite Signalovemu klicu @@ -1744,8 +1746,8 @@ Na voljo imate še %1$d poskusov. Če boste ostali brez, lahko ustvarite novi PIN. Z njim se lahko ponovno registrirate in uporabljate svoj račun, izgubili pa boste nekatere nastavitve, kot so npr. informacije iz vašega profila. Registracija storitve Signal - Potrebujem pomoč pri vnosu PINa za Android - Vnesite alfanumerični PIN - Vnesite številčni PIN + + Preklopi tipkovnico Ustvarite svoj PIN @@ -2454,7 +2456,7 @@ Za prevzem klica omogočite aplikaciji Molly dostop do mikrofona naprave. - To answer the video call, give Molly access to your microphone and camera. + Če želite sprejeti video klic, omogočite Mollyu dostop do svojega mikrofona in kamere. Dostop do mikrofona in kamere je bil trajno onemogočen. Aplikacija Molly potrebuje dovoljenje za dostop do mikrofona in kamere za klicanje. Prosimo, pojdite v meni Nastavitve aplikacij, izberite \"Dovoljenja\" in omogočite dovoljenji pod postavkama \"Mikrofon\" in \"Kamera\". Odgovorjeno na povezani napravi. Zavrnjeno na povezani napravi. @@ -3611,7 +3613,9 @@ Naprej + Ustvari alfanumerični PIN + Ustvari številčni PIN @@ -3677,8 +3681,8 @@ Vnesite PIN, ki ste ga ustvarili za zaklep svojega računa. Tu ne gre za SMS potrditveno kodo. Vnesite PIN, ki ste ga ustvarili za svoj račun. - Vnesite alfanumerični PIN - Vnesite številčni PIN + + Preklopi tipkovnico Napačen PIN. Poskusite znova. Ste pozabili PIN? Napačen PIN @@ -6266,7 +6270,7 @@ Filtrirano po zgrešenih Označi vse - + Izbriši Želite izbrisati %1$d klic? @@ -6274,8 +6278,6 @@ Želite izbrisati %1$d klice? Želite izbrisati %1$d klicev? - - Izbriši zame %1$d izbrisan klic diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 60f6ecab2f..5d42e50445 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -317,7 +317,7 @@ Videoja nuk mund të shkarkohet. Duhet ta dërgosh përsëri. - përpunuar %1$s + Përpunuar %1$s Bashkoju thirrjes @@ -883,6 +883,8 @@ S\\’ka lidhje për paraparje Kjo lidhje grupi s\\’është aktive %1$s · %2$s + + Përdor këtë lidhje për t\'iu bashkuar një thirrjeje në Signal @@ -1626,8 +1628,8 @@ Keni edhe %1$d prova. Nëse ju mbarohen provat, mund të krijoni një PIN të ri. Mund të regjistroni dhe përdorni llogarinë tuaj, por do të humbni disa nga rregullimet e ruajtura, bie fjala, të dhënat e profilit tuaj. Regjistrim Signal-i - Ju Duhet Ndihmë me PIN-in për Android - Jepni PIN alfanumerik - Jepni PIN numerik + + Ndërro tastierën Krijoni PIN-in tuaj @@ -2314,7 +2316,7 @@ Që t’i përgjigjeni thirrjes, jepini Molly-it leje të përdorë mikrofonin tuaj. - To answer the video call, give Molly access to your microphone and camera. + Që t\'i përgjigjesh thirrjes me video, jepi leje Molly të përdorë mikrofonin dhe kamerën. Që të bëjë ose pranojë thirrje, Molly-i lyp leje mbi Mikrofonin dhe Kamerën, por këto i janë mohuar. Ju lutemi, kaloni te rregullimet e aplikacionit, përzgjidhni \"Leje\", dhe aktivizoni \"Mikrofonin\" dhe \"Kamerën\". U përgjigj në një pajisje të lidhur. Hedhur tej në një pajisje të lidhur. @@ -3443,7 +3445,9 @@ Pasuesi + Krijo PIN alfanumerik + Krijo PIN numerik @@ -3505,8 +3509,8 @@ Jepni PIN-in që krijuar për llogarinë tuaj. Ky është tjetër gjë nga kodi juaj i verifikimit me SMS. Vendos kodin PIN që krijove për llogarinë tënde. - Jepni PIN alfanumerik - Jepni PIN numerik + + Ndërro tastierën PIN i pasaktë. Riprovoni. Harruat PIN-in? PIN i pasaktë @@ -6018,14 +6022,12 @@ Filtruar sipas të humburave Përzgjidh të gjitha - + Fshiji Të fshihet %1$d thirrje? Të fshihen %1$d thirrjet? - - Fshije për mua %1$d thirrja u fshi diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 324273b50e..6f640fcf26 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -317,7 +317,7 @@ Преузимање видео снимка није успело. Мораћете поново да га пошаљете. - измењено %1$s + Измењено %1$s Придружите се позиву @@ -883,6 +883,8 @@ Prikaz linka nije dostupan Link za grupu nije aktivan %1$s · %2$s + + Користите овај линк да се придружите позиву преко Signal-а @@ -1626,8 +1628,8 @@ Преостало вам је још %1$d покушаја. Ако искористите све покушаје, моћи ћете да креирате нови PIN. Можете да региструјете и користите свој налог, али изгубићете неке сачуване поставке као што су ваши подаци на профилу. Signal регистрација - Помоћ за PIN na Android-у - Унеси алфанумерички PIN - Унеси нумерички PIN + + Промени тастатуру Направите ваш PIN @@ -2314,7 +2316,7 @@ Да би одговорили, Дајте Molly-у приступ микрофону. - To answer the video call, give Molly access to your microphone and camera. + Да бисте могли да се јавите на видео позив, морате да дате Molly-у приступ микрофону и камери. Molly захтева приступ микрофону и камери да би успоставио или примио позив, али су му дозволе трајно забрањене. Молимо вас да у апликацији за подешавање телефона Molly-у дозволите пруиступ микрофону и камери. Одговорено на другом уређају. Одбијено на другом уређају. @@ -3443,7 +3445,9 @@ Даље + Додај алфанумерички PIN + Додај нумерички PIN @@ -3505,8 +3509,8 @@ Унесите PIN вашег налога. Ово није исто што и ваш SMS верификациони кôд. Унесите PIN који сте креирали за свој налог. - Унеси алфанумерички PIN - Унеси нумерички PIN + + Промени тастатуру Нетачан PIN. Покушајте поново. Заборавили сте ПИН? Нетачан PIN @@ -6018,14 +6022,12 @@ Филтрирано према пропуштеним позивима Изабери све - + Избриши Желите ли да избришете позиве (%1$d)? Желите ли да избришете позиве (%1$d)? - - Избриши за мене Избрисаних позива: %1$d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index ae116ea9e1..b79044a5b4 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -317,7 +317,7 @@ Det går inte att ladda ner videon. Du behöver skicka den igen. - redigerad %1$s + Redigerad %1$s Gå med i samtalet @@ -883,6 +883,8 @@ Ingen länkförhandsvisning tillgänglig Denna grupplänk är inte aktiv %1$s · %2$s + + Använd den här länken för att gå med i ett Signal-samtal @@ -1626,8 +1628,8 @@ Du har %1$d försök kvar. Om du får slut på försök kan du skapa en ny PIN-kod. Du kan registrera och använda ditt konto men du kommer att förlora några sparade inställningar som din profilinformation. Signal-registrering - Behöver du hjälp med PIN-kod för Android - Ange alfanumerisk PIN-kod - Ange numerisk PIN-kod + + Växla tangentbord Skapa din PIN-kod @@ -2314,7 +2316,7 @@ För att svara på samtalet, ge Molly åtkomst till din mikrofon. - To answer the video call, give Molly access to your microphone and camera. + Ge Molly åtkomst till din mikrofon och kamera för att svara på videosamtalet. Molly behöver behörigheterna Mikrofon och Kamera för att ringa och ta emot samtal, men de har avfärdats permanent. Fortsätt till inställningar för appar, välj \"Behörigheter\" och aktivera \"Mikrofon\" och \"Kamera\". Besvarades på en länkad enhet. Avböjde på en länkad enhet. @@ -3443,7 +3445,9 @@ Nästa + Skapa alfanumerisk PIN-kod + Skapa numerisk PIN-kod @@ -3505,8 +3509,8 @@ Ange PIN-koden du skapade för ditt konto. Denna skiljer sig från din SMS-verifieringskod. Ange pinkoden du skapade för ditt konto. - Ange alfanumerisk PIN-kod - Ange numerisk PIN-kod + + Växla tangentbord Felaktig PIN-kod. Försök igen. Glömt PIN-kod? Felaktig PIN-kod @@ -6018,14 +6022,12 @@ Filtrerad efter missade Markera alla - + Ta bort Ta bort %1$d samtal? Ta bort %1$d samtal? - - Ta bort för mig %1$d samtal raderat diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index d194f6205e..49e7eae2c6 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -317,7 +317,7 @@ Huwezi kupakua video. Utahitaji kuituma tena. - imehaririwa %1$s + Imehaririwa %1$s Jiunge na mazungumzo ya simu @@ -883,6 +883,8 @@ Hakuna hakiki la kiungo Kiungo hiki cha kikundi hakitumiki. %1$s · %2$s + + Tumia kiungo kujiunga kwenye Simu ya Signal @@ -1626,8 +1628,8 @@ Una majaribio %1$dyaliyosalia. Ukimaliza majaribio unaweza kuunda PINI mpya. Unaweza kujisajili na kutumia akaunti yako ila utapoteza mipangilio fulani iliyohifadhiwa kama vile taarifa za wasifu wako. Usajili wa Signal - Nahitaji Usaidizi wa Nambari ya Usajili kwenye Android - Ingiza Nenosiri la herufi na nambari - Ingiza Nenosiri lenye nambari + + Badili kibodi Unda Nenosiri lako @@ -2314,7 +2316,7 @@ Kupokea simu, iwezeshe Molly kutumia maikrofoni yako. - To answer the video call, give Molly access to your microphone and camera. + Ili kupokea simu ya video, ipe Molly ruhusa ya kufikia maikrofoni na kamera yako. Molly inahitaji ruhusa ya kipaza sauti na Kamera ili kufanya au kupokea simu, lakini zimekataliwa kabisa. Tafadhali endelea kwenye mipangilio ya programu, chagua \"Ruhusa\", na uwezeshe \"Kipaza sauti\" na \"Kamera\". Umejibu kwenye kifaa kilichounganishwa. Umekataa kwenye kifaa kilichounganishwa. @@ -3443,7 +3445,9 @@ Ifuatayo + Unda Nenosiri lenye herufi na nambari + Unda Nenosiri lenye nambari @@ -3505,8 +3509,8 @@ Ingiza Nenosiri ulilobuni kwa hii akaunti. Hii ni tofauti na nambari za kuthibitisha kupitia Ujumbe mfupi. Ingiza PIN uliyounda ya akaunti yako. - Ingiza Nenosiri la herufi na nambari - Ingiza Nenosiri lenye nambari + + Badili kibodi Nenosiri sio sahihi. Jaribu tena. Umesahau Nenosiri? Nenosiri sio sahihi @@ -6018,14 +6022,12 @@ Imechujwa kwa simu fifi Chagua zote - + Futa Futa simu %1$d? Futa simu %1$d? - - Futa kwangu Simu %1$d zimefutwa diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 2ba1741111..5e895c47d0 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -883,6 +883,8 @@ இணைப்பு முன்னோட்டம் எதுவும் கிடைக்கவில்லை இந்த குழு இணைப்பு செயலில் இல்லை %1$s · %2$s + + Signal அழைப்பில் சேர இந்த இணைப்பைப் பயன்படுத்தவும் @@ -1626,8 +1628,8 @@ உங்களிடம் %1$d முயற்சிகள் உள்ளன. நீங்கள் வரையறுக்கப்பட்ட முயற்சிகள் முடிந்தால், நீங்கள் ஒரு புதிய பின்னை உருவாக்கலாம். புதிய PIN மூலம் உங்கள் கணக்கைப் பதிவுசெய்து பயன்படுத்தலாம், ஆனால் உங்கள் சுயவிவரத் தகவல் போன்ற சில சேமிக்கப்பட்ட அமைப்புகளை இழப்பீர்கள். Signal பதிவு - Android க்கான PIN உடன் உதவி தேவை - எண்ணெழுத்து பின்னை உள்ளிடவும் - எண் பின்னை உள்ளிடவும் + + கீபோர்டை மாற்றுக உங்கள் பின்னை உருவாக்கவும் @@ -2314,7 +2316,7 @@ அழைப்பிற்கு பதிலளிக்க, உங்கள் மைக்ரோஃபோன் அணுகலை சிக்னலுக்கு வழங்கவும். - To answer the video call, give Molly access to your microphone and camera. + வீடியோ அழைப்பிற்கு பதிலளிக்க, உங்கள் மைக்ரோஃபோன் மற்றும் கேமரா அணுகலை Mollyக்கு வழங்கவும். அழைப்புகளைச் செய்ய அல்லது பெற Molly கு மைக்ரோஃபோன் மற்றும் கேமரா அனுமதிகள் தேவை, ஆனால் அவை நிரந்தரமாக மறுக்கப்பட்டுள்ளன. பயன்பாடு அமைப்புகளைத் தொடரவும், \"அனுமதிகள்\" என்பதைத் தேர்ந்தெடுத்து, \"மைக்ரோஃபோன்\" மற்றும் \"கேமரா\" ஐ இயக்கவும். இணைக்கப்பட்ட சாதனத்தில் பதிலளிக்கப்பட்டது. இணைக்கப்பட்ட சாதனத்தில் மறுக்கப்பட்டது. @@ -3443,7 +3445,9 @@ அடுத்தது + எண்ணெழுத்து பின்னை உருவாக்கவும் + எண் பின்னை உருவாக்கவும்  @@ -3505,8 +3509,8 @@ உங்கள் கணக்கிற்கு நீங்கள் உருவாக்கிய பின்னை உள்ளிடவும். இது உங்கள் SMS சரிபார்ப்புக் குறியீட்டிலிருந்து வேறுபட்டது. உங்கள் கணக்கிற்கு நீங்கள் உருவாக்கிய பின்னை உள்ளிடவும். - எண்ணெழுத்து பின்னை உள்ளிடவும் - எண் பின்னை உள்ளிடவும் + + கீபோர்டை மாற்றுக தவறான பின். மீண்டும் முயற்சி செய்க. பின் மறந்துவிட்டீர்களா? தவறான பின் @@ -6018,14 +6022,12 @@ தவறியதன் மூலம் வடிகட்டப்பட்டது அனைத்தையும் தேர்ந்தெடு - + நீக்கு %1$d அழைப்பை நீக்க வேண்டுமா? %1$d அழைப்புகளை நீக்க வேண்டுமா? - - எனக்கு மட்டும் நீக்கு %1$d அழைப்பு நீக்கப்பட்டது diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index f440898405..abdbe8682b 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -883,6 +883,8 @@ లింక్ పూర్వప్రదర్శన అందుబాటులో లేదు ఈ సమూహ లింక్ సక్రియంగా లేదు %1$s · %2$s + + Signal కాల్‌లో చేరడానికి ఈ లింక్ ఉపయోగించండి @@ -1626,8 +1628,8 @@ మీకు %1$d ప్రయత్నాలు మిగిలివున్నాయి. అన్ని ప్రయత్నాలు కోల్పోతే, క్రొత్త పిన్ సృష్టించుకోవచ్చు. ఈ విధముగా మీరు ఖాతా నమోదు చేసి వాడవచ్చు కాని, మీ పూర్వ సరళికలను కోల్పోతారు. Signal నమోదీకరణ - ఆండ్రోయిడ్ కొరకు పిన్ తొ సహాయం కావలెను - ఆల్ఫాన్యూమరిక్ పిన్ను నమోదు చేయండి - సంఖ్యా పిన్ను నమోదు చేయండి + + కీబోర్డ్ మారండి మీ పిన్ను సృష్టించండి @@ -2314,7 +2316,7 @@ కాల్‌కు సమాధానం ఇవ్వడానికి, Molly కు మీ మైక్రోఫోన్‌ యాక్సెస్‌ని ఇవ్వండి. - To answer the video call, give Molly access to your microphone and camera. + వీడియో కాల్‌కు సమాధానం ఇవ్వడానికి, Molly కు మీ మైక్రోఫోన్ మరియు కెమెరాకు యాక్సెస్‌ను ఇవ్వండి. కాల్స్ చేయడానికి మరియు కాల్స్ స్వీకరించడానికి Mollyకి మైక్రోఫోన్ మరియు కెమెరా అనుమతులు అవసరం, కానీ అవి శాశ్వతంగా తిరస్కరించబడ్డాయి. దయచేసి అనువర్తనం సెట్టింగ్లకు కొనసాగించండి, \"అనుమతులు\" ఎంచుకోండి మరియు \"మైక్రోఫోన్\" మరియు \"కెమెరా\" ని ప్రారంభించండి. లింక్ చేయబడిన పరికరంలో సమాధానం ఇవ్వబడింది. లింక్ చేయబడిన పరికరంలో తిరస్కరించబడింది. @@ -3443,7 +3445,9 @@ తరువాత + ఆల్ఫాన్యూమరిక్ పిన్ను సృష్టించండి + సంఖ్యా పిన్ను సృష్టించండి @@ -3505,8 +3509,8 @@ మీ ఖాతా కోసం మీరు సృష్టించిన పిన్‌ను నమోదు చేయండి. ఇది మీ SMS ధృవీకరణ కోడ్‌కు భిన్నంగా ఉంటుంది. మీ ఖాతా కొరకు మీరు సృష్టించిన PIN ను ఎంటర్ చేయండి. - ఆల్ఫాన్యూమరిక్ పిన్ను నమోదు చేయండి - సంఖ్యా పిన్ను నమోదు చేయండి + + కీబోర్డ్ మారండి తప్పు పిన్. మళ్ళీ ప్రయత్నించండి. పిన్ను మరచిపోయారా? తప్పయిన పిన్ @@ -6018,14 +6022,12 @@ మిస్ అయినవి ఫిల్టర్ చేయబడ్డాయి అన్నిటిని ఎంచుకోండి - + తొలగించండి %1$d కాల్ తొలగించేదా? %1$d కాల్‌లను తొలగించేదా? - - నాకు తొలగించండి %1$d కాల్ తొలగించబడింది diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 4e69b80a92..d6955e1098 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -855,6 +855,8 @@ ไม่มีภาพตัวอย่างลิงก์ให้ดู ลิงก์นี้ไม่ได้เปิดให้ใช้ในตอนนี้ %1$s · %2$s + + ใช้ลิงก์นี้เพื่อเข้าร่วมการโทรของ Signal @@ -1567,8 +1569,8 @@ คุณสามารถลองได้อีก %1$d ครั้ง ถ้าคุณลองจนครบจำนวนครั้งแล้วคุณสามารถสร้างรหัส PIN ใหม่ คุณสามารถลงทะเบียนและใช้บัญชีของคุณได้ แต่คุณจะสูญเสียการตั้งค่าเดิมเช่นข้อมูลโปรไฟล์ของคุณ การลงทะเบียน Signal - ต้องการความช่วยเหลือเกี่ยวกับ PIN สำหรับ Android - ใส่รหัส PIN ด้วยตัวอักษรและตัวเลข - ใส่รหัส PIN ด้วยตัวเลข + + สลับแป้นพิมพ์ สร้างรหัส PIN ของคุณ @@ -2244,7 +2246,7 @@ หากต้องการรับสาย ต้องให้สิทธิ์ Molly เข้าถึงไมโครโฟนของคุณ - To answer the video call, give Molly access to your microphone and camera. + อนุญาตให้ Molly เข้าถึงไมโครโฟนและกล้องของคุณเพื่อรับสายวิดีโอคอล เพื่อที่จะโทรออกและรับสาย Molly ต้องได้รับอนุญาตให้เข้าถึงไมโครโฟนและกล้อง แต่คำขอนั้นถูกปฏิเสธอย่างถาวร กรุณาไปที่เมนูตั้งค่าแอป เลือก \"การอนุญาต\" และเปิดใช้งาน \"ไมโครโฟน\" และ \"กล้อง\" มีการรับสายบนอุปกรณ์ที่เชื่อมโยงอยู่ ถูกปฏิเสธสายบนอุปกรณ์ที่เชื่อมโยงอยู่ @@ -3359,7 +3361,9 @@ ต่อไป + สร้างรหัส PIN ด้วยตัวอักษรและตัวเลข + สร้างรหัส PIN ด้วยตัวเลข @@ -3419,8 +3423,8 @@ ใส่รหัส PIN ที่คุณสร้างขึ้นสำหรับบัญชีของคุณ รหัสนี้แตกต่างจากรหัสตรวจยืนยันทาง SMS ใส่ PIN ที่คุณสร้างไว้สำหรับบัญชีของคุณ - ใส่รหัส PIN ด้วยตัวอักษรและตัวเลข - ใส่รหัส PIN ด้วยตัวเลข + + สลับแป้นพิมพ์ รหัส PIN ไม่ถูกต้อง ลองอีกครั้ง ลืมรหัส PIN หรือ? รหัส PIN ไม่ถูกต้อง @@ -5894,13 +5898,11 @@ กรองสายที่ไม่ได้รับ เลือกทั้งหมด - + ลบ ลบการโทร %1$d รายการใช่หรือไม่ - - ลบของฉัน ลบการโทร %1$d รายการแล้ว diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 28f1f35c4d..b357f8197c 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -317,7 +317,7 @@ Hindi ma-download ang video Kailangan mo itong i-send ulit. - in-edit %1$s + In-edit %1$s Mag-join sa call @@ -883,6 +883,8 @@ Walang available na link preview Hindi active ang group link na ito %1$s · %2$s + + Gamitin ang link na ito para sumali sa Signal Call @@ -1626,8 +1628,8 @@ Mayroon kang %1$d attempts na natitira. Kung maubusan ka ng attempts, pwede kang gumawa ng bagong PIN. Maaari kang mag-register at gamitin ang account mo pero mawawala ang ilang saved settings gaya ng iyong profile information. Pagrehistro sa Signal - Kailangan ng tulong sa PIN para sa Android - Ilagay ang alphanumeric na PIN - Ilagay ang numeric na PIN + + Magpalit ng keyboard Likhain ang iyong PIN @@ -2314,7 +2316,7 @@ To answer the call, bigyan ng access ang Molly sa iyong microphone. - To answer the video call, give Molly access to your microphone and camera. + Para sagutin ang video call, bigyan ng access ang Molly sa microphone at camera mo. Kailangan ng Molly ang pahintulot sa Mikropono at Camera upang tumawag at sumagot ng tawag, ngunit ito ay permanenteng ipinagbabawal. Pumunta sa app settings, piliin ang \"Mga Pahintulot\", at i-enable ang \"Mikropono\" at \"Camera\". Answered on a linked device. Declined on a linked device. @@ -3443,7 +3445,9 @@ Susunod + Lumikha ng alphanumeric na PIN + Lumikha ng numeric na PIN @@ -3505,8 +3509,8 @@ Ipasok ang PIN na iyong nilikha para sa iyong account. Ito ay iba sa iyong SMS verification code. Ilagay ang ginawa mong PIN para sa account na ito. - Ilagay ang alphanumeric na PIN - Ilagay ang numeric na PIN + + Magpalit ng keyboard Hindi tama ang PIN. Subukang muli. Nakalimutan ang PIN? Hindi tama ang PIN @@ -6018,14 +6022,12 @@ Missed calls lang ang ipakita Piliin lahat - + Burahin Gusto mo bang burahin ang %1$d call? Gusto mo bang burahin ang %1$d calls? - - Burahin sa \'kin %1$d call ang binura diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8f53fc91b5..bc3422b122 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -317,7 +317,7 @@ Video indirilemiyor. Videoyu tekrar göndermen gerekecek. - %1$s düzenlendi + %1$s Düzenlendi Aramaya katıl @@ -883,6 +883,8 @@ Bağlantı önizlemesi mevcut değil Grup bağlantısı etkin değil %1$s · %2$s + + Bir Signal Aramasına katılmak için bu bağlantıyı kullan @@ -1626,8 +1628,8 @@ %1$d deneme hakkınız kaldı. Eğer hakkınız biterse, yeni bir tane oluşturabilirsiniz. Hesabınızı kaydedebilir ve kullanabilirsiniz, ancak profil bilgileriniz gibi kaydedilmiş bazı ayarları kaybedersiniz. Signal Kaydı - Android için PIN ile ilgili Yardıma İhtiyacınız Var mı - Alfanumerik PIN\'i Girin - Sayısal PIN\'i Girin + + Klavyeyi değiştir PIN\'inizi oluşturun @@ -2314,7 +2316,7 @@ Aramayı yanıtlamak için Molly\'e mikrofon erişim izni verin. - To answer the video call, give Molly access to your microphone and camera. + Görüntülü aramayı yanıtlamak için Molly\'in mikrofonuna ve kamerana erişimine izin ver. Molly, arama yapmak ve almak için Mikrofon ve Kamera iznine ihtiyaç duyar, fakat bu izin kalıcı olarak reddedilmiş. Lütfen uygulama ayarları menüsüne girip \"İzinler\" kısmını seçin, \"Mikrofon\" ve \"Kamera\"yı etkinleştirin. Bağlı cihazdan yanıtlandı. Bağlı cihazdan reddedildi. @@ -3443,7 +3445,9 @@ İleri + Alfanumerik PIN oluştur + Sayısal PIN oluştur @@ -3505,8 +3509,8 @@ Bu hesap için oluşturduğunuz PIN kodunu girin. Bu kod, SMS doğrulama kodunuzdan farklıdır. Hesabın için oluşturduğun PIN\'i gir. - Alfanumerik PIN\'i Girin - Sayısal PIN\'i Girin + + Klavyeyi değiştir Yanlış PIN. Tekrar deneyin. PIN\'inizi mi unuttunuz? Yanlış PIN @@ -6018,14 +6022,12 @@ Cevapsıza göre filtrelendi Hepsini seç - + Sil %1$d arama silinsin mi? %1$d arama silinsin mi? - - Benden sil %1$d arama silindi diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 577093adc7..ad192c9dfa 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -855,6 +855,8 @@ ئىشلىتىلىشچان ئۇلانما ئالدىن كۆزىتىشى يوق بۇ گۇرۇپپىنىڭ ئۇلانمىسى ئاكتىپ ئەمەس %1$s · %2$s + + بۇ ئۇلانما بىلەن Signal چاقىرىقىغا قوشۇلۇش @@ -1567,8 +1569,8 @@ سىزنىڭ يەنە %1$d قېتىملىق پۇرسىتىڭىز قالدى. سىناق قېتىم سانى چەككە يەتسە، يېڭىدىن بىر PIN قۇرسىڭىز بولىدۇ. ھېساباتىڭىزنى تىزىملىتالايسىز ۋە ئىشلىتەلەيسىز، ئەمما ئارخىپ ئۇچۇرلىرى دېگەندەك ساقلانغان بەزى تەڭشەكلەر يوقىلىپ كېتىدۇ. Signal تىزىملىتىش - Android نىڭ PIN بىلەن ياردەمگە موھتاج - ھەرپ-بەلگىلىك PIN كىرگۈزۈڭ - سان-ھەرپلىك PIN كىرگۈزۈڭ + + كۇنۇپكا تاختىسىنى ئالماشتۇرۇش يېڭى PIN قۇر @@ -2244,7 +2246,7 @@ چاقىرىققا جاۋاب بېرىش ئۈچۈن، Molly مىكروفون ھوقۇقىنى تەلەپ قىلىدۇ. - To answer the video call, give Molly access to your microphone and camera. + ۋىدېيولۇق چاقىرىققا جاۋاب قايتۇرۇش ئۈچۈن Molly غا مىكروفون ۋە كامېرا ئىجازىتىنى بېرىڭ. سۆزلىشىشتە Molly مىكروفون ۋە كامېرا ئىجازىتىگە ئېھتىياجلىق، ئەمما ئۇلار رەت قىلىندى. ئەپ تەڭشىكىدىن، «ھوقۇقلار» نى تاللاپ ۋە «مىكروفون» ۋە «كامېرا» نى قوزغىتىڭ. باغلانغان بىر ئۈسكۈنىدە جاۋاب قايتۇردى. باغلانغان بىر ئۈسكۈنىدە رەت قىلدى. @@ -3359,7 +3361,9 @@ كېيىنكى + ھەرپ سان PIN قۇر + سان PIN قۇر @@ -3419,8 +3423,8 @@ ھېساباتىڭىز ئۈچۈن قۇرغان PIN نىڭىزنى كىرگۈزۈڭ. بۇ سىزنىڭ قىسقا ئۇچۇرلۇق دەلىللەش كودىڭىزغا ئوخشىمايدۇ. ھېساباتىڭىز ئۈچۈن قۇرغان PIN نى كىرگۈزۈڭ. - ھەرپ-بەلگىلىك PIN كىرگۈزۈڭ - سان-ھەرپلىك PIN كىرگۈزۈڭ + + كۇنۇپكا تاختىسىنى ئالماشتۇرۇش PIN خاتا، قايتا سىناڭ. PINنى ئۇنۇتتىڭىز؟ خاتا PIN @@ -5894,13 +5898,11 @@ ئېلىنمىغانلارنى سۈزۈش ھەممىنى تاللاش - + ئۆچۈرۈش %1$d چاقىرىقنى ئ‍ۆچۈرەمسىز؟ - - مەن ئۈچۈن ئۆچۈرۈش %1$d چاقىرىق ئ‍ۆچۈرۈلدى diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d00e33fcb6..2420c5eecb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -319,7 +319,7 @@ Не вдалося завантажити відео. Потрібно буде надіслати його ще раз. - змінено %1$s + Змінено %1$s Приєднатися до дзвінка @@ -939,6 +939,8 @@ Немає попереднього перегляду посилання Посилання групи неактивне %1$s · %2$s + + Приєднайтеся до виклику Signal через це посилання @@ -1744,8 +1746,8 @@ У вас залишилось %1$d спроб. Якщо ви вичерпаєте спроби, ви зможете створити новий PIN-код. Ви зможете зареєструватися та використовувати свій обліковий запис, але втратите деякі збережені настройки, наприклад інформацію вашого профілю. Реєстрація в Signal - Потрібна допомога з PIN-кодом для Android - Введіть буквено-цифровий PIN-код - Введіть цифровий PIN-код + + Перемкнути клавіатуру Створити PIN-код @@ -2454,7 +2456,7 @@ Для відповіді на дзвінок надайте Molly доступ до мікрофона. - To answer the video call, give Molly access to your microphone and camera. + Щоб прийняти відеовиклик, надайте Molly доступ до мікрофона і камери. Molly потребує дозволів \"Мікрофон\" та \"Камера\", щоб здійснювати виклики, але наразі доступу немає. Будь ласка, перейдіть до налаштувань додатку, оберіть \"Дозволи\", та увімкніть \"Мікрофон\" та \"Камера\". Розмову прийнято на прив\'язаному пристрої. Відхилений на прив\'язаному пристрої. @@ -3611,7 +3613,9 @@ Далі + Створити Буквенно-Цифровий PIN-код + Створити цифровий PIN-код @@ -3677,8 +3681,8 @@ Введіть PIN-код, який ви створили для свого облікового запису. Це не те ж саме. що код перевірки з SMS. Введіть PIN-код, який ви створили для свого акаунту. - Введіть буквено-цифровий PIN-код - Введіть цифровий PIN-код + + Перемкнути клавіатуру Неправильний PIN-код. Будь ласка спробуйте ще раз. Забули PIN? Неправельний PIN-код @@ -6266,7 +6270,7 @@ Фільтр за пропущеними Вибрати все - + Видалити Видалити %1$d виклик? @@ -6274,8 +6278,6 @@ Видалити %1$d викликів? Видалити %1$d виклику? - - Видалити для мене Видалено %1$d дзвінок diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 52c57ea4aa..30bd478b7b 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -883,6 +883,8 @@ کوئی لنک پیش نظارہ دستیاب نہیں ہے یہ گروپ لنک فعال نہیں ہے %1$s · %2$s + + Signal کال میں شامل ہونے کے لیے یہ لنک استعمال کریں @@ -1626,8 +1628,8 @@ آپ کے پاس %1$d کوششیں باقی ہیں۔ اگر آپ کی کوششیں ختم ہوجاتی ہیں تو ، آپ نیا PIN بنا سکتے ہیں۔ آپ اپنا اکاؤنٹ رجسٹر اور استعمال کرسکتے ہیں لیکن آپ اپنی محفوظ شدہ ترتیبات جیسے اپنے پروفائل کی معلومات سے محروم ہوجائیں گے۔ Signal اندراج - Android کے لئے PIN کے ساتھ مدد کی ضرورت ہے - Alphanumeric کا پن درج کریں - عددی پن درج کریں + + کی بورڈ سوئچ کریں اپنا پن بنائیں @@ -2314,7 +2316,7 @@ کال کا جواب دینے کے لیے، Molly کو اپنے مائیکروفون تک رسائی دیں۔ - To answer the video call, give Molly access to your microphone and camera. + ویڈیو کال کا جواب دینے کے لیے، Molly کو اپنے مائیکرو فون اور کیمرے تک رسائی فراہم کریں۔ Molly کو کالیں وصول کرنے یا بنانے کیلئے مائکروفون اور کیمرہ کی اجازت کی ضرورت ہے، لیکن وہ مستقل طور پر انکاری ہیں۔ براہ کرم ایپ کی ترتیبات میں جائیں، \"اجازت نامہ\"منتخب کریں اور \"مائکروفون \"اور \"کیمرہ\" فعال کریں۔ منسلک آلہ پر جواب دیا گیا۔ منسلک آلہ سے انکار کردیا۔ @@ -3443,7 +3445,9 @@ اگلا + Alphanumeric پن بنائیں + عددی پن بنائیں @@ -3505,8 +3509,8 @@ اپنے اکاؤنٹ کیلئے جو پن بنایا ہے داخل کریں۔ یہ آپ کے تصدیقی ایس ایم ایس سے مختلف ہے۔ وہ پِن درج کریں جو آپ نے اپنے اکاؤنٹ کے لیے تخلیق کیا تھا۔ - Alphanumeric کا پن درج کریں - عددی پن درج کریں + + کی بورڈ سوئچ کریں غلط پن. دوبارہ کوشش کریں. پن بھول گیا؟ غلط پن @@ -6018,14 +6022,12 @@ مسڈ کے ذریعے فلٹر کیا گیا تمام منتخب کریں - + حذف کریں %1$d کال حذف کریں؟ %1$d کالز حذف کریں؟ - - میرے لیے حذف کریں %1$d کال حذف کی diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b0090bb823..93e8d8e1c8 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -316,7 +316,7 @@ Không thể tải video xuống. Bạn cần gửi lại. - đã chỉnh sửa %1$s + Đã chỉnh sửa %1$s Tham gia cuộc gọi @@ -855,6 +855,8 @@ Xem trước liên kết không khả dụng Đường dẫn nhóm không khả dụng %1$s · %2$s + + Sử dụng đường dẫn này để tham gia cuộc gọi Signal @@ -1567,8 +1569,8 @@ Bạn còn %1$d lần thử. Nếu bạn hết lần thử, bạn có thể tạo PIN mới. Bạn có thể đăng kí và dùng tài khoản của bạn nhưng sẽ mất một số dữ liệu như thông tin hồ sơ. Đăng kí Signal - Cần giúp đỡ về PIN trên Android - Nhập mã PIN gồm chữ và số - Nhập mã PIN bao gồm số + + Đổi bàn phím Tạo mã PIN của bạn @@ -2244,7 +2246,7 @@ Để trả lời cuộc gọi, hãy cho phép Molly truy cập microphone. - To answer the video call, give Molly access to your microphone and camera. + Để trả lời cuộc gọi video, cho phép Molly truy cập microphone và camera. Molly cần quyền truy cập Micro và Máy ảnh để nhận cuộc gọi và gọi, nhưng đã bị từ chối vĩnh viễn. Vui lòng mở cài đặt ứng dụng, chọn \"Quyền\" và bật \"Micro\" và \"Máy ảnh\" Đã trả lời trên một thiết bị liên kết. Đã từ chối trên một thiết bị liên kết. @@ -3359,7 +3361,9 @@ Tiếp + Tạo mã PIN bao gồm chữ và số + Tạo mã PIN bao gồm số @@ -3419,8 +3423,8 @@ Nhập mã PIN bạn đã tạo cho tài khoản. Mã này khác với mã xác minh SMS của bạn. Nhập mã PIN bạn đã tạo cho tài khoản của mình. - Nhập mã PIN gồm chữ và số - Nhập mã PIN bao gồm số + + Đổi bàn phím PIN không đúng. Hãy thử lại. Quên mã PIN? Mã PIN không chính xác @@ -5894,13 +5898,11 @@ Đã lọc cuộc gọi nhỡ Chọn tất cả - + Xóa Xóa %1$d cuộc gọi? - - Xóa cho tôi Đã xóa %1$d cuộc gọi diff --git a/app/src/main/res/values-yue/strings.xml b/app/src/main/res/values-yue/strings.xml index 4ad77549b0..a88915117b 100644 --- a/app/src/main/res/values-yue/strings.xml +++ b/app/src/main/res/values-yue/strings.xml @@ -855,6 +855,8 @@ 條拎未有預覽 呢條谷拎未有生效 %1$s · %2$s + + 用呢條連結嚟加入 Signal 通話 @@ -1567,8 +1569,8 @@ 您仲可以試多 %1$d 次。如果用晒畀您嘅機會,都撞唔啱您先前整落嗰個 PIN 碼,您整過個新嘅都得。咁您就可以註冊同使用您嘅帳戶,但係您就會冇咗先前儲存落嘅一啲設定,例如話您嘅個人資料資訊。 Signal 註冊 - Android 版 PIN 碼需要幫手 - 輸入字母數字 PIN 碼 - 輸入數字 PIN 碼 + + 切換鍵盤 建立您嘅 PIN 碼 @@ -2244,7 +2246,7 @@ 如果要接聽通話,請允許 Molly 存取您部機個咪。 - To answer the video call, give Molly access to your microphone and camera. + 如果要接聽視像通話,請允許 Molly 存取你個咪同相機。 Molly 要攞「麥克風」同「相機」權限,先可以撥打同接聽通話,但權限已被永久拒絕。請到呢個 app 嘅應用程式設定,揀選「權限」,然後啟用「麥克風」同「相機」。 已用連結咗嘅機接聽。 已用連結咗嘅機拒接。 @@ -3359,7 +3361,9 @@ 下一步 + 建立字母數字 PIN 碼 + 建立數字 PIN 碼 @@ -3419,8 +3423,8 @@ 先前您咪幫您個帳戶整過一個 PIN 碼嘅,請您喺度輸入返。咪搞錯呀吓,呢度唔係講緊您個短訊驗證碼。 輸入你為帳戶建立嘅 PIN 碼。 - 輸入字母數字 PIN 碼 - 輸入數字 PIN 碼 + + 切換鍵盤 PIN 碼唔啱。請再試一次。 唔記得咗 PIN 碼? PIN 碼唔啱 @@ -5894,13 +5898,11 @@ 根據未接來電篩選 全部揀晒 - + 刪除 係咪要删除 %1$d 個通話呀? - - 喺我自己部機度刪除 刪除咗 %1$d 個通話 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 04c31fbc37..be5964a5de 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -855,6 +855,8 @@ 没有预览链接可用 此群组链接未激活 %1$s · %2$s + + 使用此链接加入 Signal 通话 @@ -1567,8 +1569,8 @@ 您还剩%1$d次机会。若尝试次数达到上限,您可以创建新的 PIN 码。您可以注册和使用您的账户,但会丢失所有保存过的设置,比如您的个人介绍。 Signal 注册 - 需要安卓 PIN 码相关帮助 - 输入字母数字 PIN - 输入数字 PIN + + 切换键盘 创建 PIN @@ -2244,7 +2246,7 @@ 如要接通,请允许 Molly 使用您的麦克风。 - To answer the video call, give Molly access to your microphone and camera. + 如要接通视频通话,请允许 Molly 使用您的麦克风和相机。 Molly 需“麦克风”和“相机”权限,来进行通话,但该权限已永久禁用。请访问应用设置菜单,选择“权限”并启用“麦克风”和“相机”。 已在在其它设备上接听。 已在其它设备上拒接。 @@ -3359,7 +3361,9 @@ 下一步 + 创建字母数字 PIN + 创建数字 PIN 码 @@ -3419,8 +3423,8 @@ 输入帐户的 PIN 码。该密码不是短信验证码。 请输入您为您的账户创建的 PIN 码。 - 输入字母数字 PIN - 输入数字 PIN + + 切换键盘 PIN 错误,请重试。 忘记 PIN? PIN 错误 @@ -5894,13 +5898,11 @@ 已按未接通话筛选 选择全部 - + 删除 删除 %1$d 个通话? - - 为我删除 已删除 %1$d 个通话 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index a3b6f17a23..7a3c8c051a 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -855,6 +855,8 @@ 連結預覽欠奉 此群組連結不在使用中 %1$s · %2$s + + 使用此連結以加入 Signal 通話 @@ -1567,8 +1569,8 @@ 您尚餘 %1$d 次嘗試機會。如果您耗盡了猜測,您可建立一個新的 PIN 碼。註冊並使用您的帳戶不成問題,惟您將會遺失部分已儲存的設定,像是您的個人資料資訊。 Signal 註冊 — Android 用戶需要 PIN 碼的協助 - 輸入字母數字 PIN 碼 - 輸入數字 PIN 碼 + + 切換鍵盤 建立您的 PIN 碼 @@ -2244,7 +2246,7 @@ 若要接聽通話,請授予 Molly 存取您的咪高峰。 - To answer the video call, give Molly access to your microphone and camera. + 若要接聽視訊通話,請允許 Molly 存取你的麥克風和相機。 Molly 需要咪高峰和相機權限才可致電或接聽通話,但已被永久拒絕。請前往應用程式設定,選擇「權限」,然後啟用「咪高峰」和「相機」。 已在連結的裝置上接聽。 已在連結的裝置上拒接。 @@ -3359,7 +3361,9 @@ 下一步 + 建立字母數字 PIN 碼 + 建立數字 PIN 碼 @@ -3419,8 +3423,8 @@ 輸入你為帳戶建立的 PIN 碼。這與你的SMS 驗證碼不同。 輸入你為帳戶建立的 PIN 碼。 - 輸入以字母及數字組成的 PIN 碼 - 輸入以數字組成的 PIN 碼 + + 切換鍵盤 PIN 碼不正確。請再試一次。 忘記 PIN 碼? PIN 碼不正確 @@ -5894,13 +5898,11 @@ 依據未接通話篩選 全選 - + 刪除 要刪除 %1$d 個通話嗎? - - 為我刪除 已刪除 %1$d 通話 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index cfd5e94ceb..11cbe45718 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -855,6 +855,8 @@ 沒有可用的連結預覽 此群組連結無效 %1$s · %2$s + + 使用此連結以加入 Signal 通話 @@ -1567,8 +1569,8 @@ 你還有 %1$d 次嘗試的機會。機會耗盡後,你可建立一個新的 PIN 碼。註冊和使用帳號將不成問題,惟你會遺失一些已儲存的設定,例如你的個人資料訊息。 Signal 註冊 - 在 Android 上需要 PIN 碼協助 - 輸入字母數字 PIN 碼 - 輸入數字 PIN 碼 + + 切換鍵盤 建立你的 PIN 碼 @@ -2244,7 +2246,7 @@ 若要接聽通話,請允許 Molly 存取你的麥克風。 - To answer the video call, give Molly access to your microphone and camera. + 若要接聽視訊通話,請允許 Molly 存取你的麥克風和相機。 Molly 需要\"麥克風\"及\"相機\"的權限以接聽來電,但是現在被設定為永久拒絕使用。請到應用程式設定中,選取\"權限\",並啟用\"麥克風\"及\"相機\"的權限。 在已連結的裝置回答。 拒絕在已連結的裝置。 @@ -3359,7 +3361,9 @@ 下一步 + 建立字母數字 PIN 碼 + 建立數字 PIN 碼 @@ -3419,8 +3423,8 @@ 輸入你為帳戶建立的 PIN 碼。 這與你的簡訊驗證碼不同。 輸入你為帳戶建立的 PIN 碼。 - 輸入字母數字PIN碼 - 輸入數字PIN碼 + + 切換鍵盤 不正確的PIN碼。請再試一次。 忘記 PIN 碼? 錯誤的 PIN 碼 @@ -5894,13 +5898,11 @@ 依據未接通話篩選 全選 - + 刪除 要刪除 %1$d 個通話嗎? - - 為我刪除 已刪除 %1$d 通話 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 729575f27b..ee318f3415 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -317,7 +317,7 @@ Can\'t download video. You will need to send it again. - edited\u2000%1$s + Edited\u2000%1$s Join call @@ -882,6 +882,8 @@ No link preview available This group link is not active %1$s · %2$s + + Use this link to join a Signal Call @@ -1626,8 +1628,8 @@ You have %1$d attempts remaining. If you run out of attempts, you can create a new PIN. You can register and use your account but you\'ll lose some saved settings like your profile information. Signal Registration - Need Help with PIN for Android - Enter alphanumeric PIN - Enter numeric PIN + + Switch keyboard Create your PIN @@ -3443,7 +3445,9 @@ Next + Create alphanumeric PIN + Create numeric PIN https://support.signal.org/hc/articles/360007059792 @@ -3505,8 +3509,8 @@ Enter the PIN you created for your account. This is different from your SMS verification code. Enter the PIN you created for your account. - Enter alphanumeric PIN - Enter numeric PIN + + Switch keyboard Incorrect PIN. Try again. Forgot PIN? Incorrect PIN @@ -6018,14 +6022,12 @@ Filtered by missed Select all - + Delete Delete %1$d call? Delete %1$d calls? - - Delete for me %1$d call deleted diff --git a/app/src/release/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt b/app/src/release/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt new file mode 100644 index 0000000000..48c5f9536c --- /dev/null +++ b/app/src/release/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/InternalConversationTestFragment.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.internal.conversation + +import androidx.fragment.app.Fragment + +/** + * STUB + */ +class InternalConversationTestFragment : Fragment() diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt b/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt index 5ae719ed2d..bae5ac9493 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/FakeMessageRecords.kt @@ -40,6 +40,7 @@ object FakeMessageRecords { key: String = "", relay: String = "", digest: ByteArray = byteArrayOf(), + incrementalDigest: ByteArray = byteArrayOf(), fastPreflightId: String = "", voiceNote: Boolean = false, borderless: Boolean = false, @@ -69,6 +70,7 @@ object FakeMessageRecords { key, relay, digest, + incrementalDigest, fastPreflightId, voiceNote, borderless, 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 e98da4a115..757ce5893b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt @@ -242,6 +242,7 @@ class UploadDependencyGraphTest { attachment.key, attachment.relay, attachment.digest, + attachment.incrementalDigest, attachment.fastPreflightId, attachment.isVoiceNote, attachment.isBorderless, diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isLegal.java b/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isLegal.java index 46f8d961cd..9f6b7f2108 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isLegal.java +++ b/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isLegal.java @@ -9,6 +9,7 @@ import static junit.framework.TestCase.assertEquals; +@SuppressWarnings("NewClassNamingConvention") @RunWith(Parameterized.class) public class LinkUtilTest_isLegal { diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isValidPreviewUrl.kt b/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isValidPreviewUrl.kt index 90cc548fc2..e2a183c032 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isValidPreviewUrl.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/LinkUtilTest_isValidPreviewUrl.kt @@ -5,6 +5,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +@Suppress("ClassName") @RunWith(Parameterized::class) class LinkUtilTest_isValidPreviewUrl(private val input: String, private val output: Boolean) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/NameUtil_getAbbreviation.kt b/app/src/test/java/org/thoughtcrime/securesms/util/NameUtil_getAbbreviation.kt index 3de13abc34..0efbfe4b3e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/NameUtil_getAbbreviation.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/NameUtil_getAbbreviation.kt @@ -7,6 +7,7 @@ import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config +@Suppress("ClassName") @RunWith(value = ParameterizedRobolectricTestRunner::class) @Config(manifest = Config.NONE, application = Application::class) class NameUtil_getAbbreviation( diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 744f29a774..e41190b2a6 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,5 +1,5 @@ buildscript { - val kotlinVersion by extra("1.7.20") + val kotlinVersion by extra("1.8.10") repositories { google() @@ -10,3 +10,12 @@ buildscript { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } + +allprojects { + // Needed because otherwise the kapt task defaults to jvmTarget 17, which "poisons the well" and requires us to bump up too + tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "11" + } + } +} diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index bd92562e9b..3ae0e35b8a 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -1,26 +1,24 @@ - - plugins { - `kotlin-dsl` - id("groovy-gradle-plugin") + `kotlin-dsl` + id("groovy-gradle-plugin") } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinDslPluginOptions { - jvmTarget.set("11") + jvmTarget.set("11") } dependencies { - implementation(libs.kotlin.gradle.plugin) - implementation(libs.android.library) - implementation(libs.android.application) - implementation(project(":tools")) + implementation(libs.kotlin.gradle.plugin) + implementation(libs.android.library) + implementation(libs.android.application) + implementation(project(":tools")) - // These allow us to reference the dependency catalog inside of our compiled plugins - implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) - implementation(files(testLibs.javaClass.superclass.protectionDomain.codeSource.location)) + // These allow us to reference the dependency catalog inside of our compiled plugins + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(files(testLibs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/build-logic/plugins/src/main/java/signal-library.gradle.kts b/build-logic/plugins/src/main/java/signal-library.gradle.kts index 673c8e0f60..bb0326de08 100644 --- a/build-logic/plugins/src/main/java/signal-library.gradle.kts +++ b/build-logic/plugins/src/main/java/signal-library.gradle.kts @@ -15,56 +15,56 @@ val signalMinSdkVersion: Int by extra val signalJavaVersion: JavaVersion by extra plugins { - id("com.android.library") - id("kotlin-android") - id("android-constants") + id("com.android.library") + id("kotlin-android") + id("android-constants") } android { - buildToolsVersion = signalBuildToolsVersion - compileSdkVersion = signalCompileSdkVersion + buildToolsVersion = signalBuildToolsVersion + compileSdkVersion = signalCompileSdkVersion - defaultConfig { - minSdk = signalMinSdkVersion - targetSdk = signalTargetSdkVersion - multiDexEnabled = true - } + defaultConfig { + minSdk = signalMinSdkVersion + targetSdk = signalTargetSdkVersion + multiDexEnabled = true + } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = signalJavaVersion - targetCompatibility = signalJavaVersion - } + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = signalJavaVersion + targetCompatibility = signalJavaVersion + } - kotlinOptions { - jvmTarget = "11" - } + kotlinOptions { + jvmTarget = "11" + } - lint { - disable += "InvalidVectorPath" - } + lint { + disable += "InvalidVectorPath" + } } dependencies { - lintChecks(project(":lintchecks")) + lintChecks(project(":lintchecks")) - coreLibraryDesugaring(libs.android.tools.desugar) + coreLibraryDesugaring(libs.android.tools.desugar) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.fragment.ktx) - implementation(libs.androidx.annotation) - implementation(libs.androidx.appcompat) - implementation(libs.rxjava3.rxandroid) - implementation(libs.rxjava3.rxjava) - implementation(libs.rxjava3.rxkotlin) - implementation(libs.androidx.multidex) - implementation(libs.kotlin.stdlib.jdk8) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.annotation) + implementation(libs.androidx.appcompat) + implementation(libs.rxjava3.rxandroid) + implementation(libs.rxjava3.rxjava) + implementation(libs.rxjava3.rxkotlin) + implementation(libs.androidx.multidex) + implementation(libs.kotlin.stdlib.jdk8) - testImplementation(testLibs.junit.junit) - testImplementation(testLibs.mockito.core) - testImplementation(testLibs.mockito.android) - testImplementation(testLibs.mockito.kotlin) - testImplementation(testLibs.robolectric.robolectric) - testImplementation(testLibs.androidx.test.core) - testImplementation(testLibs.androidx.test.core.ktx) + testImplementation(testLibs.junit.junit) + testImplementation(testLibs.mockito.core) + testImplementation(testLibs.mockito.android) + testImplementation(testLibs.mockito.kotlin) + testImplementation(testLibs.robolectric.robolectric) + testImplementation(testLibs.androidx.test.core) + testImplementation(testLibs.androidx.test.core.ktx) } diff --git a/build-logic/plugins/src/main/java/signal-sample-app.gradle.kts b/build-logic/plugins/src/main/java/signal-sample-app.gradle.kts index d88cdc6955..5894fce379 100644 --- a/build-logic/plugins/src/main/java/signal-sample-app.gradle.kts +++ b/build-logic/plugins/src/main/java/signal-sample-app.gradle.kts @@ -17,59 +17,59 @@ val signalMinSdkVersion: Int by extra val signalJavaVersion: JavaVersion by extra plugins { - id("com.android.application") - id("kotlin-android") - id("android-constants") + id("com.android.application") + id("kotlin-android") + id("android-constants") } android { - buildToolsVersion = signalBuildToolsVersion - compileSdkVersion = signalCompileSdkVersion + buildToolsVersion = signalBuildToolsVersion + compileSdkVersion = signalCompileSdkVersion - defaultConfig { - versionCode = 1 - versionName = "1.0" + defaultConfig { + versionCode = 1 + versionName = "1.0" - minSdk = signalMinSdkVersion - targetSdk = signalTargetSdkVersion - multiDexEnabled = true - } + minSdk = signalMinSdkVersion + targetSdk = signalTargetSdkVersion + multiDexEnabled = true + } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = signalJavaVersion - targetCompatibility = signalJavaVersion - } + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = signalJavaVersion + targetCompatibility = signalJavaVersion + } - kotlinOptions { - jvmTarget = "11" - } + kotlinOptions { + jvmTarget = "11" + } } dependencies { - coreLibraryDesugaring(libs.android.tools.desugar) + coreLibraryDesugaring(libs.android.tools.desugar) - implementation(project(":core-util")) + implementation(project(":core-util")) - coreLibraryDesugaring(libs.android.tools.desugar) + coreLibraryDesugaring(libs.android.tools.desugar) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.fragment.ktx) - implementation(libs.androidx.annotation) - implementation(libs.androidx.appcompat) - implementation(libs.rxjava3.rxandroid) - implementation(libs.rxjava3.rxjava) - implementation(libs.rxjava3.rxkotlin) - implementation(libs.androidx.multidex) - implementation(libs.material.material) - implementation(libs.androidx.constraintlayout) - implementation(libs.kotlin.stdlib.jdk8) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.annotation) + implementation(libs.androidx.appcompat) + implementation(libs.rxjava3.rxandroid) + implementation(libs.rxjava3.rxjava) + implementation(libs.rxjava3.rxkotlin) + implementation(libs.androidx.multidex) + implementation(libs.material.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.kotlin.stdlib.jdk8) - testImplementation(testLibs.junit.junit) - testImplementation(testLibs.mockito.core) - testImplementation(testLibs.mockito.android) - testImplementation(testLibs.mockito.kotlin) - testImplementation(testLibs.robolectric.robolectric) - testImplementation(testLibs.androidx.test.core) - testImplementation(testLibs.androidx.test.core.ktx) + testImplementation(testLibs.junit.junit) + testImplementation(testLibs.mockito.core) + testImplementation(testLibs.mockito.android) + testImplementation(testLibs.mockito.kotlin) + testImplementation(testLibs.robolectric.robolectric) + testImplementation(testLibs.androidx.test.core) + testImplementation(testLibs.androidx.test.core.ktx) } diff --git a/build-logic/plugins/src/main/java/translations.gradle b/build-logic/plugins/src/main/java/translations.gradle index 9b089e8e79..ef6179fc6b 100644 --- a/build-logic/plugins/src/main/java/translations.gradle +++ b/build-logic/plugins/src/main/java/translations.gradle @@ -133,6 +133,8 @@ task postTranslateIpFetch { ext.kbs_ips='${staticIpResolver.resolveToBuildConfig("api.backup.signal.org")}' ext.sfu_ips='${staticIpResolver.resolveToBuildConfig("sfu.voip.signal.org")}' ext.content_proxy_ips='${staticIpResolver.resolveToBuildConfig("contentproxy.signal.org")}' + ext.svr2_ips='${staticIpResolver.resolveToBuildConfig("svr2.signal.org")}' + ext.cdsi_ips='${staticIpResolver.resolveToBuildConfig("cdsi.signal.org")}' """.stripIndent().trim() } } diff --git a/build-logic/tools/build.gradle.kts b/build-logic/tools/build.gradle.kts index 4c697ca4c4..b99591b1b1 100644 --- a/build-logic/tools/build.gradle.kts +++ b/build-logic/tools/build.gradle.kts @@ -1,15 +1,15 @@ plugins { - id("org.jetbrains.kotlin.jvm") - id("java-library") + id("org.jetbrains.kotlin.jvm") + id("java-library") } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } dependencies { - implementation(libs.dnsjava) - testImplementation(testLibs.junit.junit) - testImplementation(testLibs.mockk) + implementation(libs.dnsjava) + testImplementation(testLibs.junit.junit) + testImplementation(testLibs.mockk) } diff --git a/build.gradle b/build.gradle index 3e707c9086..3153cbae60 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ import org.gradle.api.services.BuildService +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - ext.kotlin_version = '1.7.20' + ext.kotlin_version = '1.8.10' repositories { google() mavenCentral() @@ -12,10 +13,9 @@ buildscript { } } dependencies { - classpath 'com.android.tools:r8:3.3.75' - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.0.0' classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'app.cash.exhaustive:exhaustive-gradle:0.1.1' classpath ('com.squareup.wire:wire-gradle-plugin:4.4.3') { @@ -33,6 +33,15 @@ wrapper { distributionType = Wrapper.DistributionType.ALL } +allprojects { + // Needed because otherwise the kapt task defaults to jvmTarget 17, which "poisons the well" and requires us to bump up too + tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "11" + } + } +} + subprojects { ext.lib_signal_service_version_number = "2.15.3" ext.lib_signal_service_group_info = "org.whispersystems" diff --git a/core-ui/build.gradle b/core-ui/build.gradle index 427f8f473f..fa1334cf12 100644 --- a/core-ui/build.gradle +++ b/core-ui/build.gradle @@ -10,7 +10,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = '1.3.2' + kotlinCompilerExtensionVersion = '1.4.4' } } diff --git a/core-ui/src/main/java/org/signal/core/ui/Rows.kt b/core-ui/src/main/java/org/signal/core/ui/Rows.kt index e39ea40397..e47814a52b 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Rows.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Rows.kt @@ -112,8 +112,8 @@ object Rows { Row( modifier = modifier .fillMaxWidth() - .padding(defaultPadding()) .clickable(enabled = onClick != null, onClick = onClick ?: {}) + .padding(defaultPadding()) ) { Icon( imageVector = icon, @@ -135,8 +135,8 @@ object Rows { text = text, modifier = modifier .fillMaxWidth() - .padding(defaultPadding()) .clickable(enabled = onClick != null, onClick = onClick ?: {}) + .padding(defaultPadding()) ) } } diff --git a/core-util/src/main/java/org/signal/core/util/concurrent/MaybeCompat.kt b/core-util/src/main/java/org/signal/core/util/concurrent/MaybeCompat.kt new file mode 100644 index 0000000000..3237072314 --- /dev/null +++ b/core-util/src/main/java/org/signal/core/util/concurrent/MaybeCompat.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util.concurrent + +import io.reactivex.rxjava3.core.Maybe +import io.reactivex.rxjava3.exceptions.Exceptions +import io.reactivex.rxjava3.plugins.RxJavaPlugins + +/** + * Kotlin 1.8 started respecting RxJava nullability annotations but RxJava has some oddities where it breaks those rules. + * This essentially re-implements [Maybe.fromCallable] with an emitter so we don't have to do it everywhere ourselves. + */ +object MaybeCompat { + fun fromCallable(callable: () -> T?): Maybe { + return Maybe.create { emitter -> + val result = try { + callable() + } catch (e: Throwable) { + Exceptions.throwIfFatal(e) + if (!emitter.isDisposed) { + emitter.onError(e) + } else { + RxJavaPlugins.onError(e) + } + return@create + } + + if (!emitter.isDisposed) { + if (result == null) { + emitter.onComplete() + } else { + emitter.onSuccess(result) + } + } + } + } +} diff --git a/core-util/src/main/java/org/signal/core/util/logging/Scrubber.java b/core-util/src/main/java/org/signal/core/util/logging/Scrubber.java index 37336a55ed..bd3fcb7818 100644 --- a/core-util/src/main/java/org/signal/core/util/logging/Scrubber.java +++ b/core-util/src/main/java/org/signal/core/util/logging/Scrubber.java @@ -89,6 +89,12 @@ private Scrubber() { "mobi", "by", "cat", "wiki", "la", "ga", "xxx", "cf", "hr", "ng", "jobs", "online", "kz", "ug", "gq", "ae", "is", "lv", "pro", "fm", "tips", "ms", "sa", "app")); + /** + * Base16 Call Link Key Pattern + */ + private static final Pattern CALL_LINK_PATTERN = Pattern.compile("([bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4})(-[bBcCdDfFgGhHkKmMnNpPqQrRsStTxXzZ]{4}){7}"); + private static final String CALL_LINK_CENSOR_SUFFIX = "-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"; + public static CharSequence scrub(@NonNull CharSequence in) { in = scrubE164(in); @@ -98,6 +104,7 @@ public static CharSequence scrub(@NonNull CharSequence in) { in = scrubUuids(in); in = scrubDomains(in); in = scrubIpv4(in); + in = scrubCallLinkKeys(in); return in; } @@ -173,6 +180,16 @@ private static CharSequence scrubIpv4(@NonNull CharSequence in) { (matcher, output) -> output.append(IPV4_CENSOR)); } + private static CharSequence scrubCallLinkKeys(@NonNull CharSequence in) { + return scrub(in, + CALL_LINK_PATTERN, + ((matcher, output) -> { + String match = matcher.group(1); + output.append(match); + output.append(CALL_LINK_CENSOR_SUFFIX); + })); + } + private static CharSequence scrub(@NonNull CharSequence in, @NonNull Pattern pattern, @NonNull ProcessMatch processMatch) { final StringBuilder output = new StringBuilder(in.length()); diff --git a/core-util/src/test/java/org/signal/core/util/StringExtensions_asListContains.kt b/core-util/src/test/java/org/signal/core/util/StringExtensions_asListContains.kt index 1f476d740e..d3c6ce794f 100644 --- a/core-util/src/test/java/org/signal/core/util/StringExtensions_asListContains.kt +++ b/core-util/src/test/java/org/signal/core/util/StringExtensions_asListContains.kt @@ -5,6 +5,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +@Suppress("ClassName") @RunWith(Parameterized::class) class StringExtensions_asListContains( private val model: String, diff --git a/core-util/src/test/java/org/signal/core/util/logging/ScrubberTest.java b/core-util/src/test/java/org/signal/core/util/logging/ScrubberTest.java index b907dccb2f..6898fd3ddc 100644 --- a/core-util/src/test/java/org/signal/core/util/logging/ScrubberTest.java +++ b/core-util/src/test/java/org/signal/core/util/logging/ScrubberTest.java @@ -113,6 +113,18 @@ public static Collection data() { { "Not an ipv4 3.141", "Not an ipv4 3.141" + }, + + { "A Call Link Root Key BCDF-FGHK-MNPQ-RSTX-ZRQH-BCDF-FGHM-STXZ", + "A Call Link Root Key BCDF-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" + }, + + { "Not a Call Link Root Key (Invalid Characters) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-FGHM-STXZ", + "Not a Call Link Root Key (Invalid Characters) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-FGHM-STXZ" + }, + + { "Not a Call Link Root Key (Missing Quartet) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-STXZ", + "Not a Call Link Root Key (Missing Quartet) BCAF-FGHK-MNPQ-RSTX-ZRQH-BCDF-STXZ" } }); diff --git a/dependencies.gradle b/dependencies.gradle index 759bc378a4..b2f3d94593 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -20,141 +20,140 @@ dependencyResolutionManagement { version('accompanist', '0.28.0') // Android Plugins - alias('android-library').to('com.android.library', 'com.android.library.gradle.plugin').versionRef('android-gradle-plugin') - alias('android-application').to('com.android.application', 'com.android.application.gradle.plugin').versionRef('android-gradle-plugin') + library('android-library', 'com.android.library', 'com.android.library.gradle.plugin').versionRef('android-gradle-plugin') + library('android-application', 'com.android.application', 'com.android.application.gradle.plugin').versionRef('android-gradle-plugin') // Compose - alias('androidx-compose-bom').to('androidx.compose:compose-bom:2023.05.01') - alias('androidx-compose-material3').to('androidx.compose.material3', 'material3').withoutVersion() - alias('androidx-compose-ui-tooling-preview').to('androidx.compose.ui', 'ui-tooling-preview').withoutVersion() - alias('androidx-compose-ui-tooling-core').to('androidx.compose.ui', 'ui-tooling').withoutVersion() - alias('androidx-compose-runtime-livedata').to('androidx.compose.runtime', 'runtime-livedata').withoutVersion() - alias('androidx-compose-rxjava3').to('androidx.compose.runtime:runtime-rxjava3:1.4.2') + library('androidx-compose-bom', 'androidx.compose:compose-bom:2023.05.01') + library('androidx-compose-material3', 'androidx.compose.material3', 'material3').withoutVersion() + library('androidx-compose-ui-tooling-preview', 'androidx.compose.ui', 'ui-tooling-preview').withoutVersion() + library('androidx-compose-ui-tooling-core', 'androidx.compose.ui', 'ui-tooling').withoutVersion() + library('androidx-compose-runtime-livedata', 'androidx.compose.runtime', 'runtime-livedata').withoutVersion() + library('androidx-compose-rxjava3', 'androidx.compose.runtime:runtime-rxjava3:1.4.2') // Accompanist - alias('accompanist-permissions').to('com.google.accompanist', 'accompanist-permissions').versionRef('accompanist') + library('accompanist-permissions', 'com.google.accompanist', 'accompanist-permissions').versionRef('accompanist') // Desugaring - alias('android-tools-desugar').to('com.android.tools:desugar_jdk_libs:1.1.5') + library('android-tools-desugar', 'com.android.tools:desugar_jdk_libs:1.1.5') // Kotlin - alias('kotlin-stdlib-jdk8').to('org.jetbrains.kotlin', 'kotlin-stdlib-jdk8').versionRef('kotlin') - alias('kotlin-gradle-plugin').to('org.jetbrains.kotlin', 'kotlin-gradle-plugin').versionRef('kotlin') + library('kotlin-stdlib-jdk8', 'org.jetbrains.kotlin', 'kotlin-stdlib-jdk8').versionRef('kotlin') + library('kotlin-gradle-plugin', 'org.jetbrains.kotlin', 'kotlin-gradle-plugin').versionRef('kotlin') // Android X - alias('androidx-activity-ktx').to('androidx.activity', 'activity-ktx').versionRef('androidx-activity') - alias('androidx-appcompat').to('androidx.appcompat', 'appcompat').versionRef('androidx-appcompat') - alias('androidx-core-ktx').to('androidx.core:core-ktx:1.10.0') - alias('androidx-fragment').to('androidx.fragment', 'fragment').versionRef('androidx-fragment') - alias('androidx-fragment-ktx').to('androidx.fragment', 'fragment-ktx').versionRef('androidx-fragment') - alias('androidx-fragment-testing').to('androidx.fragment', 'fragment-testing').versionRef('androidx-fragment') - alias('androidx-annotation').to('androidx.annotation:annotation:1.4.0') - alias('androidx-constraintlayout').to('androidx.constraintlayout:constraintlayout:2.1.4') - alias('androidx-window-window').to('androidx.window', 'window').versionRef('androidx-window') - alias('androidx-window-java').to('androidx.window', 'window-java').versionRef('androidx-window') - alias('androidx-recyclerview').to('androidx.recyclerview:recyclerview:1.2.1') - alias('androidx-legacy-support').to('androidx.legacy:legacy-support-v13:1.0.0') - alias('androidx-legacy-preference').to('androidx.legacy:legacy-preference-v14:1.0.0') - alias('androidx-preference').to('androidx.preference:preference:1.1.1') - alias('androidx-gridlayout').to('androidx.gridlayout:gridlayout:1.0.0') - alias('androidx-exifinterface').to('androidx.exifinterface:exifinterface:1.3.3') - alias('androidx-multidex').to('androidx.multidex:multidex:2.0.1') - alias('androidx-navigation-fragment-ktx').to('androidx.navigation', 'navigation-fragment-ktx').versionRef('androidx-navigation') - alias('androidx-navigation-ui-ktx').to('androidx.navigation', 'navigation-ui-ktx').versionRef('androidx-navigation') - alias('androidx-lifecycle-viewmodel-ktx').to('androidx.lifecycle', 'lifecycle-viewmodel-ktx').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-livedata-core').to('androidx.lifecycle', 'lifecycle-livedata').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-livedata-ktx').to('androidx.lifecycle', 'lifecycle-livedata-ktx').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-process').to('androidx.lifecycle', 'lifecycle-process').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-viewmodel-savedstate').to('androidx.lifecycle', 'lifecycle-viewmodel-savedstate').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-common-java8').to('androidx.lifecycle', 'lifecycle-common-java8').versionRef('androidx-lifecycle') - alias('androidx-lifecycle-reactivestreams-ktx').to('androidx.lifecycle', 'lifecycle-reactivestreams-ktx').versionRef('androidx-lifecycle') - alias('androidx-camera-core').to('androidx.camera', 'camera-core').versionRef('androidx-camera') - alias('androidx-camera-camera2').to('androidx.camera', 'camera-camera2').versionRef('androidx-camera') - alias('androidx-camera-lifecycle').to('androidx.camera', 'camera-lifecycle').versionRef('androidx-camera') - alias('androidx-camera-view').to('androidx.camera', 'camera-view').versionRef('androidx-camera') - alias('androidx-concurrent-futures').to('androidx.concurrent:concurrent-futures:1.0.0') - alias('androidx-autofill').to('androidx.autofill:autofill:1.0.0') - alias('androidx-biometric').to('androidx.biometric:biometric:1.2.0-alpha04') - alias('androidx-sharetarget').to('androidx.sharetarget:sharetarget:1.2.0-rc02') - alias('androidx-sqlite').to('androidx.sqlite:sqlite:2.1.0') - alias('androidx-core-role').to('androidx.core:core-role:1.0.0') - alias('androidx-profileinstaller').to('androidx.profileinstaller:profileinstaller:1.2.2') - alias('androidx-asynclayoutinflater').to('androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01') - alias('androidx-asynclayoutinflater-appcompat').to('androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01') - alias('androidx-webkit').to('androidx.webkit:webkit:1.4.0') + library('androidx-activity-ktx', 'androidx.activity', 'activity-ktx').versionRef('androidx-activity') + library('androidx-appcompat', 'androidx.appcompat', 'appcompat').versionRef('androidx-appcompat') + library('androidx-core-ktx', 'androidx.core:core-ktx:1.10.0') + library('androidx-fragment', 'androidx.fragment', 'fragment').versionRef('androidx-fragment') + library('androidx-fragment-ktx', 'androidx.fragment', 'fragment-ktx').versionRef('androidx-fragment') + library('androidx-fragment-testing', 'androidx.fragment', 'fragment-testing').versionRef('androidx-fragment') + library('androidx-annotation', 'androidx.annotation:annotation:1.4.0') + library('androidx-constraintlayout', 'androidx.constraintlayout:constraintlayout:2.1.4') + library('androidx-window-window', 'androidx.window', 'window').versionRef('androidx-window') + library('androidx-window-java', 'androidx.window', 'window-java').versionRef('androidx-window') + library('androidx-recyclerview', 'androidx.recyclerview:recyclerview:1.2.1') + library('androidx-legacy-support', 'androidx.legacy:legacy-support-v13:1.0.0') + library('androidx-legacy-preference', 'androidx.legacy:legacy-preference-v14:1.0.0') + library('androidx-preference', 'androidx.preference:preference:1.1.1') + library('androidx-gridlayout', 'androidx.gridlayout:gridlayout:1.0.0') + library('androidx-exifinterface', 'androidx.exifinterface:exifinterface:1.3.3') + library('androidx-multidex', 'androidx.multidex:multidex:2.0.1') + library('androidx-navigation-fragment-ktx', 'androidx.navigation', 'navigation-fragment-ktx').versionRef('androidx-navigation') + library('androidx-navigation-ui-ktx', 'androidx.navigation', 'navigation-ui-ktx').versionRef('androidx-navigation') + library('androidx-lifecycle-viewmodel-ktx', 'androidx.lifecycle', 'lifecycle-viewmodel-ktx').versionRef('androidx-lifecycle') + library('androidx-lifecycle-livedata-core', 'androidx.lifecycle', 'lifecycle-livedata').versionRef('androidx-lifecycle') + library('androidx-lifecycle-livedata-ktx', 'androidx.lifecycle', 'lifecycle-livedata-ktx').versionRef('androidx-lifecycle') + library('androidx-lifecycle-process', 'androidx.lifecycle', 'lifecycle-process').versionRef('androidx-lifecycle') + library('androidx-lifecycle-viewmodel-savedstate', 'androidx.lifecycle', 'lifecycle-viewmodel-savedstate').versionRef('androidx-lifecycle') + library('androidx-lifecycle-common-java8', 'androidx.lifecycle', 'lifecycle-common-java8').versionRef('androidx-lifecycle') + library('androidx-lifecycle-reactivestreams-ktx', 'androidx.lifecycle', 'lifecycle-reactivestreams-ktx').versionRef('androidx-lifecycle') + library('androidx-camera-core', 'androidx.camera', 'camera-core').versionRef('androidx-camera') + library('androidx-camera-camera2', 'androidx.camera', 'camera-camera2').versionRef('androidx-camera') + library('androidx-camera-lifecycle', 'androidx.camera', 'camera-lifecycle').versionRef('androidx-camera') + library('androidx-camera-view', 'androidx.camera', 'camera-view').versionRef('androidx-camera') + library('androidx-concurrent-futures', 'androidx.concurrent:concurrent-futures:1.0.0') + library('androidx-autofill', 'androidx.autofill:autofill:1.0.0') + library('androidx-biometric', 'androidx.biometric:biometric:1.1.0') + library('androidx-sharetarget', 'androidx.sharetarget:sharetarget:1.2.0-rc02') + library('androidx-sqlite', 'androidx.sqlite:sqlite:2.1.0') + library('androidx-core-role', 'androidx.core:core-role:1.0.0') + library('androidx-profileinstaller', 'androidx.profileinstaller:profileinstaller:1.2.2') + library('androidx-asynclayoutinflater', 'androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01') + library('androidx-asynclayoutinflater-appcompat', 'androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01') + library('androidx-webkit', 'androidx.webkit:webkit:1.4.0') // Material - alias('material-material').to('com.google.android.material:material:1.8.0') + library('material-material', 'com.google.android.material:material:1.8.0') // Google - alias('google-protobuf-javalite').to('com.google.protobuf:protobuf-javalite:3.11.4') - alias('google-libphonenumber').to('com.googlecode.libphonenumber:libphonenumber:8.12.54') - alias('google-play-services-maps').to('com.google.android.gms:play-services-maps:18.1.0') - alias('google-play-services-auth').to('com.google.android.gms:play-services-auth:20.3.0') - alias('google-zxing-android-integration').to('com.google.zxing:android-integration:3.3.0') - alias('google-zxing-core').to('com.google.zxing:core:3.4.1') - alias('google-ez-vcard').to('com.googlecode.ez-vcard:ez-vcard:0.9.11') - alias('google-jsr305').to('com.google.code.findbugs:jsr305:3.0.2') - alias('google-guava-android').to('com.google.guava:guava:30.0-android') - alias('google-flexbox').to('com.google.android.flexbox:flexbox:3.0.0') + library('google-protobuf-javalite', 'com.google.protobuf:protobuf-javalite:3.11.4') + library('google-libphonenumber', 'com.googlecode.libphonenumber:libphonenumber:8.12.54') + library('google-play-services-maps', 'com.google.android.gms:play-services-maps:18.1.0') + library('google-play-services-auth', 'com.google.android.gms:play-services-auth:20.3.0') + library('google-zxing-android-integration', 'com.google.zxing:android-integration:3.3.0') + library('google-zxing-core', 'com.google.zxing:core:3.4.1') + library('google-ez-vcard', 'com.googlecode.ez-vcard:ez-vcard:0.9.11') + library('google-jsr305', 'com.google.code.findbugs:jsr305:3.0.2') + library('google-guava-android', 'com.google.guava:guava:30.0-android') + library('google-flexbox', 'com.google.android.flexbox:flexbox:3.0.0') // Exoplayer - alias('exoplayer-core').to('com.google.android.exoplayer', 'exoplayer-core').versionRef('exoplayer') - alias('exoplayer-ui').to('com.google.android.exoplayer', 'exoplayer-ui').versionRef('exoplayer') - alias('exoplayer-extension-mediasession').to('com.google.android.exoplayer', 'extension-mediasession').versionRef('exoplayer') + library('exoplayer-core', 'com.google.android.exoplayer', 'exoplayer-core').versionRef('exoplayer') + library('exoplayer-ui', 'com.google.android.exoplayer', 'exoplayer-ui').versionRef('exoplayer') + library('exoplayer-extension-mediasession', 'com.google.android.exoplayer', 'extension-mediasession').versionRef('exoplayer') bundle('exoplayer', ['exoplayer-core', 'exoplayer-ui', 'exoplayer-extension-mediasession']) // Firebase - alias('firebase-messaging').to('com.google.firebase:firebase-messaging:23.1.0') + library('firebase-messaging', 'com.google.firebase:firebase-messaging:23.1.2') // 1st Party - alias('libsignal-client').to('org.signal', 'libsignal-client').versionRef('libsignal-client') - alias('libsignal-android').to('org.signal', 'libsignal-android').versionRef('libsignal-client') - alias('signal-aesgcmprovider').to('org.signal:aesgcmprovider:0.0.3') - alias('molly-ringrtc').to('im.molly:ringrtc-android:2.28.1-1') - alias('signal-android-database-sqlcipher').to('org.signal:sqlcipher-android:4.5.4-S2') + 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('molly-ringrtc', 'im.molly:ringrtc-android:2.28.1-1') + library('signal-android-database-sqlcipher', 'org.signal:sqlcipher-android:4.5.4-S2') // MOLLY - alias('gosimple-nbvcxz').to('me.gosimple:nbvcxz:1.5.0') - alias('molly-native-utils').to('im.molly:native-utils:1.0.0') - alias('molly-argon2').to('im.molly:argon2:13.1-1') + library('gosimple-nbvcxz', 'me.gosimple:nbvcxz:1.5.0') + library('molly-native-utils', 'im.molly:native-utils:1.0.0') + library('molly-argon2', 'im.molly:argon2:13.1-1') // Third Party - alias('greenrobot-eventbus').to('org.greenrobot:eventbus:3.0.0') - alias('jackson-core').to('com.fasterxml.jackson.core:jackson-databind:2.9.9.2') - alias('square-okhttp3').to('com.squareup.okhttp3:okhttp:3.12.13') - alias('square-okhttp3-dnsoverhttps').to('com.squareup.okhttp3:okhttp-dnsoverhttps:3.12.13') - alias('square-okio').to('com.squareup.okio:okio:3.0.0') - alias('square-leakcanary').to('com.squareup.leakcanary:leakcanary-android:2.7') - alias('rxjava3-rxjava').to('io.reactivex.rxjava3:rxjava:3.0.13') - alias('rxjava3-rxandroid').to('io.reactivex.rxjava3:rxandroid:3.0.0') - alias('rxjava3-rxkotlin').to('io.reactivex.rxjava3:rxkotlin:3.0.1') - alias('rxdogtag').to('com.uber.rxdogtag2:rxdogtag:2.0.1') - alias('conscrypt-android').to('org.conscrypt:conscrypt-android:2.0.0') - alias('mobilecoin').to('com.mobilecoin:android-sdk:5.0.0') - alias('leolin-shortcutbadger').to('me.leolin:ShortcutBadger:1.1.22') - alias('emilsjolander-stickylistheaders').to('se.emilsjolander:stickylistheaders:2.7.0') - alias('apache-httpclient-android').to('org.apache.httpcomponents:httpclient-android:4.3.5') - alias('glide-glide').to('com.github.bumptech.glide', 'glide').versionRef('glide') - alias('glide-compiler').to('com.github.bumptech.glide', 'compiler').versionRef('glide') - alias('roundedimageview').to('com.makeramen:roundedimageview:2.1.0') - alias('materialish-progress').to('com.pnikosis:materialish-progress:1.5') - alias('subsampling-scale-image-view').to('com.davemorrissey.labs:subsampling-scale-image-view:3.10.0') - alias('android-tooltips').to('com.tomergoldst.android:tooltips:1.0.6') - alias('stream').to('com.annimon:stream:1.1.8') - alias('lottie').to('com.airbnb.android:lottie:5.2.0') - alias('dnsjava').to('dnsjava:dnsjava:2.1.9') - alias('nanohttpd-webserver').to('org.nanohttpd:nanohttpd-webserver:2.3.1') - alias('kotlinx-collections-immutable').to('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5') + library('greenrobot-eventbus', 'org.greenrobot:eventbus:3.0.0') + library('jackson-core', 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2') + library('square-okhttp3', 'com.squareup.okhttp3:okhttp:3.12.13') + library('square-okhttp3-dnsoverhttps', 'com.squareup.okhttp3:okhttp-dnsoverhttps:3.12.13') + library('square-okio', 'com.squareup.okio:okio:3.0.0') + library('square-leakcanary', 'com.squareup.leakcanary:leakcanary-android:2.7') + library('rxjava3-rxjava', 'io.reactivex.rxjava3:rxjava:3.0.13') + library('rxjava3-rxandroid', 'io.reactivex.rxjava3:rxandroid:3.0.0') + library('rxjava3-rxkotlin', 'io.reactivex.rxjava3:rxkotlin:3.0.1') + library('rxdogtag', 'com.uber.rxdogtag2:rxdogtag:2.0.1') + library('conscrypt-android', 'org.conscrypt:conscrypt-android:2.5.2') + library('mobilecoin', 'com.mobilecoin:android-sdk:5.0.0') + library('leolin-shortcutbadger', 'me.leolin:ShortcutBadger:1.1.22') + library('emilsjolander-stickylistheaders', 'se.emilsjolander:stickylistheaders:2.7.0') + library('apache-httpclient-android', 'org.apache.httpcomponents:httpclient-android:4.3.5') + library('glide-glide', 'com.github.bumptech.glide', 'glide').versionRef('glide') + library('glide-compiler', 'com.github.bumptech.glide', 'compiler').versionRef('glide') + library('roundedimageview', 'com.makeramen:roundedimageview:2.1.0') + library('materialish-progress', 'com.pnikosis:materialish-progress:1.5') + library('subsampling-scale-image-view', 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0') + library('android-tooltips', 'com.tomergoldst.android:tooltips:1.0.6') + library('stream', 'com.annimon:stream:1.1.8') + library('lottie', 'com.airbnb.android:lottie:5.2.0') + library('dnsjava', 'dnsjava:dnsjava:2.1.9') + library('nanohttpd-webserver', 'org.nanohttpd:nanohttpd-webserver:2.3.1') + library('kotlinx-collections-immutable', 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5') // Can't use the newest version because it hits some weird NoClassDefFoundException - alias('jknack-handlebars').to('com.github.jknack:handlebars:4.0.7') - alias('kotlinx-collections-immutable').to('org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5') + library('jknack-handlebars', 'com.github.jknack:handlebars:4.0.7') // Mp4Parser - alias('mp4parser-isoparser').to('org.mp4parser', 'isoparser').versionRef('mp4parser') - alias('mp4parser-streaming').to('org.mp4parser', 'streaming').versionRef('mp4parser') - alias('mp4parser-muxer').to('org.mp4parser', 'muxer').versionRef('mp4parser') + library('mp4parser-isoparser', 'org.mp4parser', 'isoparser').versionRef('mp4parser') + library('mp4parser-streaming', 'org.mp4parser', 'streaming').versionRef('mp4parser') + library('mp4parser-muxer', 'org.mp4parser', 'muxer').versionRef('mp4parser') bundle('mp4parser', ['mp4parser-isoparser', 'mp4parser-streaming', 'mp4parser-muxer']) } @@ -162,11 +161,11 @@ dependencyResolutionManagement { version('androidx-test-ext-junit', '1.1.3') // Macrobench/Baseline profiles - alias ('androidx-test-ext-junit').to('androidx.test.ext', 'junit').versionRef('androidx-test-ext-junit') - alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0') - alias('uiautomator').to('androidx.test.uiautomator:uiautomator:2.2.0') - alias('androidx-benchmark-macro').to('androidx.benchmark:benchmark-macro-junit4:1.1.1') - alias('androidx-benchmark-micro').to('androidx.benchmark:benchmark-junit4:1.1.0-beta04') + library('androidx-test-ext-junit', 'androidx.test.ext', 'junit').versionRef('androidx-test-ext-junit') + library('espresso-core', 'androidx.test.espresso:espresso-core:3.4.0') + library('uiautomator', 'androidx.test.uiautomator:uiautomator:2.2.0') + library('androidx-benchmark-macro', 'androidx.benchmark:benchmark-macro-junit4:1.1.1') + library('androidx-benchmark-micro', 'androidx.benchmark:benchmark-junit4:1.1.0-beta04') } testLibs { @@ -174,34 +173,35 @@ dependencyResolutionManagement { version('androidx-test-ext-junit', '1.1.1') version('robolectric', '4.8.1') - alias('junit-junit').to('junit:junit:4.13.2') - alias('androidx-test-core').to('androidx.test', 'core').versionRef('androidx-test') - alias('androidx-test-core-ktx').to('androidx.test', 'core-ktx').versionRef('androidx-test') - alias('androidx-test-ext-junit').to('androidx.test.ext', 'junit').versionRef('androidx-test-ext-junit') - alias('androidx-test-ext-junit-ktx').to('androidx.test.ext', 'junit-ktx').versionRef('androidx-test-ext-junit') - alias('androidx-test-orchestrator').to('androidx.test:orchestrator:1.4.1') - alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0') - alias('mockito-core').to('org.mockito:mockito-inline:4.6.1') - alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0') - alias('mockito-android').to('org.mockito:mockito-android:4.6.1') - alias('robolectric-robolectric').to('org.robolectric', 'robolectric').versionRef('robolectric') - alias('robolectric-shadows-multidex').to('org.robolectric', 'shadows-multidex').versionRef('robolectric') - alias('bouncycastle-bcprov-jdk15on').to('org.bouncycastle:bcprov-jdk15on:1.70') - alias('hamcrest-hamcrest').to('org.hamcrest:hamcrest:2.2') - alias('assertj-core').to('org.assertj:assertj-core:3.11.1') - alias('square-okhttp-mockserver').to('com.squareup.okhttp3:mockwebserver:3.12.13') - alias('mockk').to('io.mockk:mockk:1.13.2') - alias('mockk-android').to('io.mockk:mockk-android:1.13.2') - - alias('conscrypt-openjdk-uber').to('org.conscrypt:conscrypt-openjdk-uber:2.0.0') + library('junit-junit', 'junit:junit:4.13.2') + library('androidx-test-core', 'androidx.test', 'core').versionRef('androidx-test') + library('androidx-test-core-ktx', 'androidx.test', 'core-ktx').versionRef('androidx-test') + library('androidx-test-ext-junit', 'androidx.test.ext', 'junit').versionRef('androidx-test-ext-junit') + library('androidx-test-ext-junit-ktx', 'androidx.test.ext', 'junit-ktx').versionRef('androidx-test-ext-junit') + library('androidx-test-orchestrator', 'androidx.test:orchestrator:1.4.1') + library('espresso-core', 'androidx.test.espresso:espresso-core:3.4.0') + library('mockito-core', 'org.mockito:mockito-inline:4.6.1') + library('mockito-kotlin', 'org.mockito.kotlin:mockito-kotlin:4.0.0') + library('mockito-android', 'org.mockito:mockito-android:4.6.1') + library('robolectric-robolectric', 'org.robolectric', 'robolectric').versionRef('robolectric') + library('robolectric-shadows-multidex', 'org.robolectric', 'shadows-multidex').versionRef('robolectric') + library('bouncycastle-bcprov-jdk15on', 'org.bouncycastle:bcprov-jdk15on:1.70') + library('bouncycastle-bcpkix-jdk15on', 'org.bouncycastle:bcpkix-jdk15on:1.70') + library('hamcrest-hamcrest', 'org.hamcrest:hamcrest:2.2') + library('assertj-core', 'org.assertj:assertj-core:3.11.1') + library('square-okhttp-mockserver', 'com.squareup.okhttp3:mockwebserver:3.12.13') + library('mockk', 'io.mockk:mockk:1.13.2') + library('mockk-android', 'io.mockk:mockk-android:1.13.2') + + library('conscrypt-openjdk-uber', 'org.conscrypt:conscrypt-openjdk-uber:2.5.2') } lintLibs { version('lint', '30.2.2') - alias('lint-api').to('com.android.tools.lint', 'lint-api').versionRef('lint') - alias('lint-checks').to('com.android.tools.lint', 'lint-checks').versionRef('lint') - alias('lint-tests').to('com.android.tools.lint', 'lint-tests').versionRef('lint') + library('lint-api', 'com.android.tools.lint', 'lint-api').versionRef('lint') + library('lint-checks', 'com.android.tools.lint', 'lint-checks').versionRef('lint') + library('lint-tests', 'com.android.tools.lint', 'lint-tests').versionRef('lint') } } } diff --git a/gradle.properties b/gradle.properties index e786fac0ca..87d972fb8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,3 +4,6 @@ org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true kapt.incremental.apt=false +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 81cf5a4c02..9f229af6ae 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -9,7 +9,7 @@ Run the following command to update this file after adding or updating a depende For more information, see: https://docs.gradle.org/current/userguide/dependency_verification.html --> - + true false @@ -52,23 +52,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - @@ -132,17 +121,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - @@ -213,11 +196,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -278,14 +256,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -294,14 +264,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -318,14 +280,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -334,22 +288,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - @@ -358,14 +296,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -374,22 +304,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - @@ -398,14 +312,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -414,14 +320,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -430,14 +328,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -487,12 +377,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -825,11 +715,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -840,9 +725,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -855,20 +740,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - + + + - - + + @@ -1021,27 +903,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - @@ -1050,19 +911,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - @@ -1089,14 +937,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1105,14 +945,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1129,14 +961,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1167,17 +991,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - @@ -1191,9 +1009,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - @@ -1212,9 +1027,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - @@ -1227,14 +1039,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1243,14 +1047,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1282,14 +1078,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1393,14 +1181,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1439,9 +1219,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - @@ -1454,14 +1231,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1522,21 +1291,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - @@ -1552,11 +1311,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -1607,11 +1361,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -1638,22 +1387,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - @@ -1670,35 +1403,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1779,14 +1483,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -1808,11 +1504,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + @@ -1826,11 +1532,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -1841,9 +1542,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -1856,9 +1557,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -1871,16 +1572,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -1891,16 +1592,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -1911,6 +1612,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -1926,11 +1632,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -1941,19 +1642,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + - - - + + + @@ -1961,11 +1657,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -1976,16 +1667,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -1996,16 +1687,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -2016,9 +1707,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -2031,16 +1722,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -2051,16 +1742,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -2071,16 +1762,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -2091,26 +1782,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - + + + - - - + + + - - + + - - + + @@ -2121,14 +1806,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -2145,12 +1822,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -2169,9 +1846,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + + + + @@ -2184,9 +1864,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -2199,12 +1879,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + @@ -2223,6 +1900,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2231,14 +1916,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -2255,12 +1932,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -2279,17 +1956,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + - - - - - - + + + @@ -2308,12 +1990,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -2332,12 +2014,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -2356,6 +2038,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2364,14 +2054,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -2388,6 +2070,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2414,11 +2104,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -2429,14 +2114,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - - + + + @@ -2444,14 +2129,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + @@ -2459,14 +2139,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + @@ -2474,14 +2149,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + @@ -2489,11 +2159,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -2504,19 +2169,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - - - - + + + @@ -2524,14 +2189,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + @@ -2539,23 +2199,13 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - + + + - - + + @@ -2564,11 +2214,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -2579,16 +2224,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -2599,25 +2244,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - + + + - - - - - - - + + @@ -2630,25 +2267,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - - - - + + + - - - - + + @@ -2661,25 +2290,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - - - - + + + - - - - + + @@ -2692,25 +2313,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - - - - + + + - - - - + + @@ -2723,67 +2336,43 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - + + + - - + + - - - + + + - - - + + + + + + - - - - - - - - - - - - - - + + + - - - + + + - - - - - - - - - - - - - - - + + @@ -2796,25 +2385,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - + + + - - - + + + - - - - - - - + + @@ -2827,6 +2408,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -2917,11 +2503,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3032,11 +2613,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3097,6 +2673,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3135,11 +2716,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3165,21 +2741,26 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + - - - - - @@ -3205,6 +2786,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3215,16 +2801,31 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + @@ -3255,6 +2856,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3265,14 +2871,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - + + + @@ -3285,11 +2896,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3300,6 +2906,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3335,9 +2946,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -3350,9 +2961,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -3360,6 +2971,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3434,11 +3050,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3498,6 +3109,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + @@ -3514,6 +3149,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + @@ -3530,6 +3181,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3546,6 +3205,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3554,6 +3221,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3570,6 +3245,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3586,6 +3269,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3602,6 +3293,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + @@ -3618,6 +3325,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + @@ -3650,6 +3381,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3812,11 +3551,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -3846,14 +3580,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -3899,14 +3625,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -3915,14 +3633,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -3931,14 +3641,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -4024,21 +3726,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - @@ -4054,16 +3746,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - @@ -4079,6 +3761,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4108,6 +3795,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -4116,11 +3811,24 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + @@ -4131,6 +3839,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4141,6 +3854,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4151,6 +3869,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4161,11 +3884,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + @@ -4176,6 +3909,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4282,51 +4020,106 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4416,11 +4209,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -4431,11 +4219,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -4501,6 +4284,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4526,6 +4314,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -4546,16 +4339,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + - - - - - @@ -4604,11 +4397,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + @@ -4624,12 +4427,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + @@ -4657,26 +4460,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - @@ -4697,34 +4480,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -4732,16 +4495,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - @@ -4752,34 +4505,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - + + + @@ -4787,16 +4525,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - @@ -4807,65 +4535,31 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - + + + - - + + - - - - - - - - - - - - - - - - - + @@ -4875,14 +4569,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -4891,29 +4577,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - @@ -4922,39 +4590,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -4962,24 +4605,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - - - - - - - - - - - - + + + @@ -4997,9 +4630,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + @@ -5007,27 +4640,17 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - + + + - - + + @@ -5035,29 +4658,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + - - - - - - - - + + + @@ -5065,46 +4678,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5130,26 +4713,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - @@ -5160,11 +4728,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -5180,26 +4743,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - @@ -5220,11 +4768,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -5235,16 +4778,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - + + + + + @@ -5255,11 +4798,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -5270,19 +4808,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - + + + @@ -5290,49 +4823,16 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -5362,14 +4862,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -5383,14 +4875,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -5412,14 +4896,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - - - - @@ -5446,6 +4922,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -5462,11 +4943,24 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + @@ -5475,6 +4969,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -5483,6 +4985,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -5621,11 +5131,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -5790,11 +5295,6 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - - - @@ -5849,9 +5349,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f7189a776c..b22661d9bf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionSha256Sum=f30b29580fe11719087d698da23f3b0f0d04031d8995f7dd8275a31f7674dc01 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/libfakegms/build.gradle b/libfakegms/build.gradle index da0255360e..808023f035 100644 --- a/libfakegms/build.gradle +++ b/libfakegms/build.gradle @@ -10,8 +10,8 @@ android { compileSdkVersion = signalCompileSdkVersion defaultConfig { - minSdkVersion signalMinSdkVersion - targetSdkVersion signalTargetSdkVersion + minSdk = signalMinSdkVersion + targetSdk = signalTargetSdkVersion } compileOptions { @@ -21,6 +21,6 @@ android { } dependencies { - implementation libs.androidx.fragment.ktx + implementation libs.androidx.fragment lintChecks project(':lintchecks') } \ No newline at end of file diff --git a/libnetcipher/build.gradle b/libnetcipher/build.gradle index ccca831f69..1b1dbdf85f 100644 --- a/libnetcipher/build.gradle +++ b/libnetcipher/build.gradle @@ -10,8 +10,8 @@ android { compileSdkVersion = signalCompileSdkVersion defaultConfig { - minSdkVersion signalMinSdkVersion - targetSdkVersion signalTargetSdkVersion + minSdk = signalMinSdkVersion + targetSdk = signalTargetSdkVersion } compileOptions { @@ -22,6 +22,6 @@ android { dependencies { implementation project(':core-util') - implementation 'androidx.annotation:annotation-jvm:1.6.0' + implementation libs.androidx.annotation lintChecks project(':lintchecks') } diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index 032231a6b4..7db2a71db4 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + apply plugin: 'java-library' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'java-test-fixtures' @@ -12,18 +14,16 @@ group = lib_signal_service_group_info java { withJavadocJar() withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(11)) +tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "11" } } -compileJava { - options.release = 11 -} - configurations { ideaTestFixturesImplementation { extendsFrom testFixturesImplementation; canBeConsumed false; canBeResolved true } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 02779e6cdd..bd09f16ec9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -154,7 +154,7 @@ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, Fi if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener); - return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get()); + return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), pointer.getincrementalDigest().orElse(new byte[0])); } public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 2df917d2df..d177ab3e5f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; @@ -438,7 +439,7 @@ public SendMessageResult sendEditMessage(SignalServiceAddress recipient, long targetSentTimestamp) throws UntrustedIdentityException, IOException { - Log.d(TAG, "[" + message.getTimestamp() + "] Sending an edit message."); + Log.d(TAG, "[" + message.getTimestamp() + "] Sending an edit message for " + targetSentTimestamp + "."); Content content = createEditMessageContent(new SignalServiceEditMessage(targetSentTimestamp, message)); @@ -641,12 +642,14 @@ public List sendDataMessage(List public SendMessageResult sendSyncMessage(SignalServiceDataMessage dataMessage) throws IOException, UntrustedIdentityException { + Log.d(TAG, "[" + dataMessage.getTimestamp() + "] Sending self-sync message."); return sendSyncMessage(createSelfSendSyncMessage(dataMessage), Optional.empty()); } public SendMessageResult sendSelfSyncEditMessage(SignalServiceEditMessage editMessage) throws IOException, UntrustedIdentityException { + Log.d(TAG, "[" + editMessage.getDataMessage().getTimestamp() + "] Sending self-sync edit message for " + editMessage.getTargetSentTimestamp() + "."); return sendSyncMessage(createSelfSendSyncEditMessage(editMessage), Optional.empty()); } @@ -762,7 +765,7 @@ private SignalServiceAttachmentPointer uploadAttachmentV2(SignalServiceAttachmen v2UploadAttributes = socket.getAttachmentV2UploadAttributes(); } - Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes); + Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes); return new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()), @@ -771,7 +774,8 @@ private SignalServiceAttachmentPointer uploadAttachmentV2(SignalServiceAttachmen Optional.of(Util.toIntExact(attachment.getLength())), attachment.getPreview(), attachment.getWidth(), attachment.getHeight(), - Optional.of(attachmentIdAndDigest.second()), + Optional.of(attachmentIdAndDigest.second().getDigest()), + Optional.of(attachmentIdAndDigest.second().getIncrementalDigest()), attachment.getFileName(), attachment.getVoiceNote(), attachment.isBorderless(), @@ -811,7 +815,7 @@ public ResumableUploadSpec getResumableUploadSpec() throws IOException { } private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException { - byte[] digest = socket.uploadAttachment(attachmentData); + AttachmentDigest digest = socket.uploadAttachment(attachmentData); return new SignalServiceAttachmentPointer(attachmentData.getResumableUploadSpec().getCdnNumber(), new SignalServiceAttachmentRemoteId(attachmentData.getResumableUploadSpec().getCdnKey()), attachment.getContentType(), @@ -820,7 +824,8 @@ private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmen attachment.getPreview(), attachment.getWidth(), attachment.getHeight(), - Optional.of(digest), + Optional.of(digest.getDigest()), + Optional.ofNullable(digest.getIncrementalDigest()), attachment.getFileName(), attachment.getVoiceNote(), attachment.isBorderless(), diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyCollection.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyCollection.kt index 8bd7f0c528..08a02dfa21 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyCollection.kt +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyCollection.kt @@ -6,27 +6,16 @@ package org.whispersystems.signalservice.api.account import org.signal.libsignal.protocol.IdentityKey -import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.state.KyberPreKeyRecord -import org.signal.libsignal.protocol.state.PreKeyRecord import org.signal.libsignal.protocol.state.SignedPreKeyRecord -import org.whispersystems.signalservice.api.push.ServiceIdType /** * Holder class to pass around a bunch of prekeys that we send off to the service during registration. - * As the service does not return the submitted prekeys,we need to hold them in memory so that when + * As the service does not return the submitted prekeys, we need to hold them in memory so that when * the service approves the keys we have a local copy to persist. */ data class PreKeyCollection( - val identityKeyPair: IdentityKeyPair, - val nextSignedPreKeyId: Int, - val ecOneTimePreKeyIdOffset: Int, - val lastResortKyberPreKeyId: Int, - val oneTimeKyberPreKeyIdOffset: Int, - val serviceIdType: ServiceIdType, val identityKey: IdentityKey, val signedPreKey: SignedPreKeyRecord, - val oneTimeEcPreKeys: List, - val lastResortKyberPreKey: KyberPreKeyRecord, - val oneTimeKyberPreKeys: List + val lastResortKyberPreKey: KyberPreKeyRecord ) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java index b6ce5829ce..b9d853708c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java @@ -8,6 +8,8 @@ import org.signal.libsignal.protocol.InvalidMacException; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice; +import org.signal.libsignal.protocol.incrementalmac.IncrementalMacInputStream; import org.signal.libsignal.protocol.kdf.HKDFv3; import org.whispersystems.signalservice.internal.util.ContentLengthInputStream; import org.whispersystems.signalservice.internal.util.Util; @@ -51,7 +53,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { private long totalRead; private byte[] overflowBuffer; - public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest) + public static InputStream createForAttachment(File file, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest) throws InvalidMessageException, IOException { try { @@ -71,7 +73,18 @@ public static InputStream createForAttachment(File file, long plaintextLength, b verifyMac(fin, file.length(), mac, digest); } - InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), parts[0], file.length() - BLOCK_SIZE - mac.getMacLength()); + final FileInputStream innerStream = new FileInputStream(file); + + boolean hasIncrementalMac = incrementalDigest != null && incrementalDigest.length > 0; + + InputStream wrap = !hasIncrementalMac ? innerStream + : new IncrementalMacInputStream( + innerStream, + parts[1], + ChunkSizeChoice.inferChunkSize(Math.max(Math.toIntExact(file.length()), 1)), + incrementalDigest); + + InputStream inputStream = new AttachmentCipherInputStream(wrap, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength()); if (plaintextLength != 0) { inputStream = new ContentLengthInputStream(inputStream, plaintextLength); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index cb46dc7505..1516483d62 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -958,6 +958,7 @@ private GroupChange.Actions getVerifiedActions(GroupChange groupChange) try { signature = new NotarySignature(groupChange.getServerSignature().toByteArray()); } catch (InvalidInputException e) { + Log.w(TAG, "Invalid input while verifying group change", e); throw new VerificationFailedException(); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java index 6ea49c346a..47d2b9bb5b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java @@ -25,6 +25,7 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { private final Optional size; private final Optional preview; private final Optional digest; + private final Optional incrementalDigest; private final Optional fileName; private final boolean voiceNote; private final boolean borderless; @@ -44,6 +45,7 @@ public SignalServiceAttachmentPointer(int cdnNumber, int width, int height, Optional digest, + Optional incrementalDigest, Optional fileName, boolean voiceNote, boolean borderless, @@ -53,21 +55,22 @@ public SignalServiceAttachmentPointer(int cdnNumber, long uploadTimestamp) { super(contentType); - this.cdnNumber = cdnNumber; - this.remoteId = remoteId; - this.key = key; - this.size = size; - this.preview = preview; - this.width = width; - this.height = height; - this.digest = digest; - this.fileName = fileName; - this.voiceNote = voiceNote; - this.borderless = borderless; - this.caption = caption; - this.blurHash = blurHash; - this.uploadTimestamp = uploadTimestamp; - this.gif = gif; + this.cdnNumber = cdnNumber; + this.remoteId = remoteId; + this.key = key; + this.size = size; + this.preview = preview; + this.width = width; + this.height = height; + this.digest = digest; + this.incrementalDigest = incrementalDigest; + this.fileName = fileName; + this.voiceNote = voiceNote; + this.borderless = borderless; + this.caption = caption; + this.blurHash = blurHash; + this.uploadTimestamp = uploadTimestamp; + this.gif = gif; } public int getCdnNumber() { @@ -108,6 +111,10 @@ public Optional getDigest() { return digest; } + public Optional getincrementalDigest() { + return incrementalDigest; + } + public boolean getVoiceNote() { return voiceNote; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java index cb02be7b8f..904146e1f2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/AttachmentPointerUtil.java @@ -25,6 +25,7 @@ public static SignalServiceAttachmentPointer createSignalAttachmentPointer(Signa pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.empty(), pointer.getWidth(), pointer.getHeight(), pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.empty(), + pointer.hasIncrementalDigest() ? Optional.of(pointer.getIncrementalDigest().toByteArray()) : Optional.empty(), pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.empty(), (pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE)) != 0, (pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE)) != 0, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt new file mode 100644 index 0000000000..cbe51e971b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/crypto/AttachmentDigest.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.crypto + +data class AttachmentDigest(val digest: ByteArray, val incrementalDigest: 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 eaee05c97b..ace511da1f 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 @@ -110,6 +110,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException; import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException; import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException; @@ -432,7 +433,8 @@ public VerifyAccountResponse submitRegistrationRequest(@Nullable String sessionI aciLastResortKyberPreKey, pniLastResortKyberPreKey, gcmRegistrationId, - skipDeviceTransfer); + skipDeviceTransfer, + true); String response = makeServiceRequest(path, "POST", JsonUtil.toJson(body), NO_HEADERS, new RegistrationSessionResponseHandler(), Optional.empty()); return JsonUtil.fromJson(response, VerifyAccountResponse.class); @@ -1345,7 +1347,7 @@ public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() } } - public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes) + public AttachmentDigest uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes) throws IOException { return uploadToCdn0(AVATAR_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), @@ -1358,17 +1360,17 @@ public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttribute null, null); } - public Pair uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes) + public Pair uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes) throws PushNetworkException, NonSuccessfulResponseCodeException { - long id = Long.parseLong(uploadAttributes.getAttachmentId()); - byte[] digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), - uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(), - uploadAttributes.getCredential(), uploadAttributes.getDate(), - uploadAttributes.getSignature(), attachment.getData(), - "application/octet-stream", attachment.getDataSize(), - attachment.getOutputStreamFactory(), attachment.getListener(), - attachment.getCancelationSignal()); + long id = Long.parseLong(uploadAttributes.getAttachmentId()); + AttachmentDigest digest = uploadToCdn0(ATTACHMENT_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), + uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(), + uploadAttributes.getCredential(), uploadAttributes.getDate(), + uploadAttributes.getSignature(), attachment.getData(), + "application/octet-stream", attachment.getDataSize(), + attachment.getOutputStreamFactory(), attachment.getListener(), + attachment.getCancelationSignal()); return new Pair<>(id, digest); } @@ -1382,7 +1384,7 @@ public ResumableUploadSpec getResumableUploadSpec(AttachmentV3UploadAttributes u System.currentTimeMillis() + CDN2_RESUMABLE_LINK_LIFETIME_MILLIS); } - public byte[] uploadAttachment(PushAttachmentData attachment) throws IOException { + public AttachmentDigest uploadAttachment(PushAttachmentData attachment) throws IOException { if (attachment.getResumableUploadSpec() == null || attachment.getResumableUploadSpec().getExpirationTimestamp() < System.currentTimeMillis()) { throw new ResumeLocationInvalidException(); @@ -1472,11 +1474,11 @@ private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumb } } - private byte[] uploadToCdn0(String path, String acl, String key, String policy, String algorithm, - String credential, String date, String signature, - InputStream data, String contentType, long length, - OutputStreamFactory outputStreamFactory, ProgressListener progressListener, - CancelationSignal cancelationSignal) + private AttachmentDigest uploadToCdn0(String path, String acl, String key, String policy, String algorithm, + String credential, String date, String signature, + InputStream data, String contentType, long length, + OutputStreamFactory outputStreamFactory, ProgressListener progressListener, + CancelationSignal cancelationSignal) throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(0), random); @@ -1516,7 +1518,7 @@ private byte[] uploadToCdn0(String path, String acl, String key, String policy, } try (Response response = call.execute()) { - if (response.isSuccessful()) return file.getTransmittedDigest(); + if (response.isSuccessful()) return file.getAttachmentDigest(); else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response); } catch (PushNetworkException | NonSuccessfulResponseCodeException e) { throw e; @@ -1577,7 +1579,7 @@ private String getResumableUploadUrl(String signedUrl, Map heade } } - private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException { + private AttachmentDigest uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException { ConnectionHolder connectionHolder = getRandom(cdnClientsMap.get(2), random); OkHttpClient okHttpClient = connectionHolder.getClient() .newBuilder() @@ -1593,7 +1595,7 @@ private byte[] uploadToCdn2(String resumableUrl, InputStream data, String conten try (NowhereBufferedSink buffer = new NowhereBufferedSink()) { file.writeTo(buffer); } - return file.getTransmittedDigest(); + return file.getAttachmentDigest(); } Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, resumableUrl)) @@ -1611,7 +1613,7 @@ private byte[] uploadToCdn2(String resumableUrl, InputStream data, String conten } try (Response response = call.execute()) { - if (response.isSuccessful()) return file.getTransmittedDigest(); + if (response.isSuccessful()) return file.getAttachmentDigest(); else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response); } catch (PushNetworkException | NonSuccessfulResponseCodeException e) { throw e; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt index 60edee8827..4657b58373 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionRequestBody.kt @@ -17,5 +17,6 @@ data class RegistrationSessionRequestBody( @JsonProperty val aciPqLastResortPreKey: KyberPreKeyEntity, @JsonProperty val pniPqLastResortPreKey: KyberPreKeyEntity, @JsonProperty val gcmToken: GcmRegistrationId?, - @JsonProperty val skipDeviceTransfer: Boolean + @JsonProperty val skipDeviceTransfer: Boolean, + @JsonProperty val requireAtomic: Boolean = true ) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java deleted file mode 100644 index f7fda59176..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.whispersystems.signalservice.internal.push.http; - - -import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream; -import org.whispersystems.signalservice.api.crypto.DigestingOutputStream; - -import java.io.IOException; -import java.io.OutputStream; - -public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory { - - private final byte[] key; - private final byte[] iv; - - public AttachmentCipherOutputStreamFactory(byte[] key, byte[] iv) { - this.key = key; - this.iv = iv; - } - - @Override - public DigestingOutputStream createFor(OutputStream wrap) throws IOException { - return new AttachmentCipherOutputStream(key, iv, wrap); - } - -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt new file mode 100644 index 0000000000..25623ad241 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/AttachmentCipherOutputStreamFactory.kt @@ -0,0 +1,40 @@ +package org.whispersystems.signalservice.internal.push.http + +import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice +import org.signal.libsignal.protocol.incrementalmac.IncrementalMacOutputStream +import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream +import org.whispersystems.signalservice.api.crypto.DigestingOutputStream +import java.io.IOException +import java.io.OutputStream + +/** + * Creates [AttachmentCipherOutputStream] using the provided [key] and [iv]. + * + * [createFor] is straightforward, and is the legacy behavior. + * [createIncrementalFor] first wraps the stream in an [IncrementalMacOutputStream] to calculate MAC digests on chunks as the stream is written to. + * + * @property key + * @property iv + */ +class AttachmentCipherOutputStreamFactory(private val key: ByteArray, private val iv: ByteArray) : OutputStreamFactory { + companion object { + private const val AES_KEY_LENGTH = 32 + } + + @Throws(IOException::class) + override fun createFor(wrap: OutputStream): DigestingOutputStream { + return AttachmentCipherOutputStream(key, iv, wrap) + } + + @Throws(IOException::class) + fun createIncrementalFor(wrap: OutputStream?, length: Long, incrementalDigestOut: OutputStream?): DigestingOutputStream { + if (length > Int.MAX_VALUE) { + throw IllegalArgumentException("Attachment length overflows int!") + } + + val privateKey = key.sliceArray(AES_KEY_LENGTH until key.size) + val chunkSizeChoice = ChunkSizeChoice.inferChunkSize(length.toInt().coerceAtLeast(1)) + val incrementalStream = IncrementalMacOutputStream(wrap, privateKey, chunkSizeChoice, incrementalDigestOut) + return createFor(incrementalStream) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java deleted file mode 100644 index b39e29eaff..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.whispersystems.signalservice.internal.push.http; - - - -import org.whispersystems.signalservice.api.crypto.DigestingOutputStream; -import org.whispersystems.signalservice.api.crypto.SkippingOutputStream; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; -import org.whispersystems.signalservice.api.util.Preconditions; - -import java.io.IOException; -import java.io.InputStream; - -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; - -public class DigestingRequestBody extends RequestBody { - - private final InputStream inputStream; - private final OutputStreamFactory outputStreamFactory; - private final String contentType; - private final long contentLength; - private final ProgressListener progressListener; - private final CancelationSignal cancelationSignal; - private final long contentStart; - - private byte[] digest; - - public DigestingRequestBody(InputStream inputStream, - OutputStreamFactory outputStreamFactory, - String contentType, long contentLength, - ProgressListener progressListener, - CancelationSignal cancelationSignal, - long contentStart) - { - Preconditions.checkArgument(contentLength >= contentStart); - Preconditions.checkArgument(contentStart >= 0); - - this.inputStream = inputStream; - this.outputStreamFactory = outputStreamFactory; - this.contentType = contentType; - this.contentLength = contentLength; - this.progressListener = progressListener; - this.cancelationSignal = cancelationSignal; - this.contentStart = contentStart; - } - - @Override - public MediaType contentType() { - return MediaType.parse(contentType); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - DigestingOutputStream outputStream = outputStreamFactory.createFor(new SkippingOutputStream(contentStart, sink.outputStream())); - byte[] buffer = new byte[8192]; - - int read; - long total = 0; - - while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { - if (cancelationSignal != null && cancelationSignal.isCanceled()) { - throw new IOException("Canceled!"); - } - - outputStream.write(buffer, 0, read); - total += read; - - if (progressListener != null) { - progressListener.onAttachmentProgress(contentLength, total); - } - } - - outputStream.flush(); - digest = outputStream.getTransmittedDigest(); - } - - @Override - public long contentLength() { - if (contentLength > 0) return contentLength - contentStart; - else return -1; - } - - public byte[] getTransmittedDigest() { - return digest; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt new file mode 100644 index 0000000000..adf5e8b133 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBody.kt @@ -0,0 +1,82 @@ +package org.whispersystems.signalservice.internal.push.http + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import org.whispersystems.signalservice.api.crypto.DigestingOutputStream +import org.whispersystems.signalservice.api.crypto.SkippingOutputStream +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment +import org.whispersystems.signalservice.internal.crypto.AttachmentDigest +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +/** + * This [RequestBody] encrypts the data written to it before it is sent. + */ +class DigestingRequestBody( + private val inputStream: InputStream, + private val outputStreamFactory: OutputStreamFactory, + private val contentType: String, + private val contentLength: Long, + private val progressListener: SignalServiceAttachment.ProgressListener?, + private val cancelationSignal: CancelationSignal?, + private val contentStart: Long +) : RequestBody() { + lateinit var transmittedDigest: ByteArray + private set + var incrementalDigest: ByteArray? = null + private set + + init { + require(contentLength >= contentStart) + require(contentStart >= 0) + } + + override fun contentType(): MediaType? { + return MediaType.parse(contentType) + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + val digestStream = ByteArrayOutputStream() + val inner = SkippingOutputStream(contentStart, sink.outputStream()) + val isIncremental = outputStreamFactory is AttachmentCipherOutputStreamFactory + val outputStream: DigestingOutputStream = if (isIncremental) { + (outputStreamFactory as AttachmentCipherOutputStreamFactory).createIncrementalFor(inner, contentLength, digestStream) + } else { + outputStreamFactory.createFor(inner) + } + + val buffer = ByteArray(8192) + var read: Int + var total: Long = 0 + + while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) { + if (cancelationSignal?.isCanceled == true) { + throw IOException("Canceled!") + } + outputStream.write(buffer, 0, read) + total += read.toLong() + progressListener?.onAttachmentProgress(contentLength, total) + } + + outputStream.flush() + if (isIncremental) { + outputStream.close() + digestStream.close() + incrementalDigest = digestStream.toByteArray() + } + transmittedDigest = outputStream.transmittedDigest + } + + override fun contentLength(): Long { + return if (contentLength > 0) contentLength - contentStart else -1 + } + + fun getAttachmentDigest() = AttachmentDigest(transmittedDigest, incrementalDigest) + + companion object { + const val TAG = "DigestingRequestBody" + } +} diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 7baf8f8096..74effa322e 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -667,20 +667,21 @@ message AttachmentPointer { fixed64 cdnId = 1; string cdnKey = 15; } - optional string contentType = 2; - optional bytes key = 3; - optional uint32 size = 4; - optional bytes thumbnail = 5; - optional bytes digest = 6; - optional string fileName = 7; - optional uint32 flags = 8; - optional uint32 width = 9; - optional uint32 height = 10; - optional string caption = 11; - optional string blurHash = 12; - optional uint64 uploadTimestamp = 13; - optional uint32 cdnNumber = 14; - // Next ID: 16 + optional string contentType = 2; + optional bytes key = 3; + optional uint32 size = 4; + optional bytes thumbnail = 5; + optional bytes digest = 6; + optional bytes incrementalDigest = 16; + optional string fileName = 7; + optional uint32 flags = 8; + optional uint32 width = 9; + optional uint32 height = 10; + optional string caption = 11; + optional string blurHash = 12; + optional uint64 uploadTimestamp = 13; + optional uint32 cdnNumber = 14; + // Next ID: 17 } message GroupContext { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java index e9b4883eb7..004a1ce18d 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java @@ -3,6 +3,7 @@ import org.conscrypt.Conscrypt; import org.junit.Test; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.incrementalmac.InvalidMacException; import org.signal.libsignal.protocol.kdf.HKDFv3; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory; @@ -17,9 +18,11 @@ import java.io.OutputStream; import java.security.Security; import java.util.Arrays; +import java.util.Random; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS; public final class AttachmentCipherTest { @@ -32,9 +35,9 @@ public final class AttachmentCipherTest { public void attachment_encryptDecrypt() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "Peter Parker".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -46,9 +49,9 @@ public void attachment_encryptDecrypt() throws IOException, InvalidMessageExcept public void attachment_encryptDecryptEmpty() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -57,19 +60,19 @@ public void attachment_encryptDecryptEmpty() throws IOException, InvalidMessageE } @Test - public void attachment_decryptFailOnBadKey() throws IOException{ + public void attachment_decryptFailOnBadKey() throws IOException { File cipherFile = null; boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Gwen Stacy".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); - byte[] badKey = new byte[64]; + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Gwen Stacy".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badKey = new byte[64]; cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -82,19 +85,19 @@ public void attachment_decryptFailOnBadKey() throws IOException{ } @Test - public void attachment_decryptFailOnBadDigest() throws IOException{ + public void attachment_decryptFailOnBadDigest() throws IOException { File cipherFile = null; boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Mary Jane Watson".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); - byte[] badDigest = new byte[32]; + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Mary Jane Watson".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badDigest = new byte[32]; cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -106,9 +109,42 @@ public void attachment_decryptFailOnBadDigest() throws IOException{ assertTrue(hitCorrectException); } + @Test + public void attachment_decryptFailOnBadIncrementalDigest() throws IOException { + File cipherFile = null; + boolean hitCorrectException = false; + + try { + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = new byte[1000000]; + + new Random().nextBytes(plaintextInput); + + EncryptResult encryptResult = encryptData(plaintextInput, key, true); + byte[] badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.length); + + cipherFile = writeToFile(encryptResult.ciphertext); + + + InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, badDigest); + byte[] plaintextOutput = readInputStreamFully(decryptedStream); + fail(); + } catch (InvalidMacException e) { + hitCorrectException = true; + } catch (InvalidMessageException e) { + hitCorrectException = false; + } finally { + if (cipherFile != null) { + cipherFile.delete(); + } + } + + assertTrue(hitCorrectException); + } + @Test public void attachment_encryptDecryptPaddedContent() throws IOException, InvalidMessageException { - int[] lengths = { 531, 600, 724, 1019, 1024 }; + int[] lengths = { 531, 600, 724, 1019, 1024 }; for (int length : lengths) { byte[] plaintextInput = new byte[length]; @@ -117,24 +153,26 @@ public void attachment_encryptDecryptPaddedContent() throws IOException, Invalid plaintextInput[i] = (byte) 0x97; } - byte[] key = Util.getSecretBytes(64); - ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput); - InputStream dataStream = new PaddingInputStream(inputStream, length); - ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream(); - DigestingOutputStream digestStream = new AttachmentCipherOutputStreamFactory(key, null).createFor(encryptedStream); + byte[] key = Util.getSecretBytes(64); + byte[] iv = Util.getSecretBytes(16); + ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput); + InputStream paddedInputStream = new PaddingInputStream(inputStream, length); + ByteArrayOutputStream destinationOutputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream incrementalDigestOutputStream = new ByteArrayOutputStream(); + DigestingOutputStream encryptingOutputStream = new AttachmentCipherOutputStreamFactory(key, iv).createIncrementalFor(destinationOutputStream, length, incrementalDigestOutputStream); - Util.copy(dataStream, digestStream); - digestStream.flush(); + Util.copy(paddedInputStream, encryptingOutputStream); - byte[] digest = digestStream.getTransmittedDigest(); - byte[] encryptedData = encryptedStream.toByteArray(); + encryptingOutputStream.flush(); + encryptingOutputStream.close(); - encryptedStream.close(); - inputStream.close(); + byte[] encryptedData = destinationOutputStream.toByteArray(); + byte[] digest = encryptingOutputStream.getTransmittedDigest(); + byte[] incrementalDigest = incrementalDigestOutputStream.toByteArray(); File cipherFile = writeToFile(encryptedData); - InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest); + InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest, incrementalDigest); byte[] plaintextOutput = readInputStreamFully(decryptedStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -149,13 +187,13 @@ public void attachment_decryptFailOnNullDigest() throws IOException { boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] plaintextInput = "Aunt May".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + byte[] key = Util.getSecretBytes(64); + byte[] plaintextInput = "Aunt May".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); cipherFile = writeToFile(encryptResult.ciphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -175,14 +213,14 @@ public void attachment_decryptFailOnBadMac() throws IOException { try { byte[] key = Util.getSecretBytes(64); byte[] plaintextInput = "Uncle Ben".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key); + EncryptResult encryptResult = encryptData(plaintextInput, key, true); byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length); badMacCiphertext[badMacCiphertext.length - 1] += 1; cipherFile = writeToFile(badMacCiphertext); - AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest); + AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest); } catch (InvalidMessageException e) { hitCorrectException = true; } finally { @@ -200,7 +238,7 @@ public void sticker_encryptDecrypt() throws IOException, InvalidMessageException byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "Peter Parker".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey); byte[] plaintextOutput = readInputStreamFully(inputStream); @@ -213,7 +251,7 @@ public void sticker_encryptDecryptEmpty() throws IOException, InvalidMessageExce byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey); byte[] plaintextOutput = readInputStreamFully(inputStream); @@ -227,10 +265,10 @@ public void sticker_decryptFailOnBadKey() throws IOException { boolean hitCorrectException = false; try { - byte[] packKey = Util.getSecretBytes(32); - byte[] plaintextInput = "Gwen Stacy".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); - byte[] badPackKey = new byte[32]; + byte[] packKey = Util.getSecretBytes(32); + byte[] plaintextInput = "Gwen Stacy".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); + byte[] badPackKey = new byte[32]; AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey); } catch (InvalidMessageException e) { @@ -249,7 +287,7 @@ public void sticker_decryptFailOnBadMac() throws IOException { try { byte[] packKey = Util.getSecretBytes(32); byte[] plaintextInput = "Uncle Ben".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey)); + EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true); byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length); badMacCiphertext[badMacCiphertext.length - 1] += 1; @@ -262,15 +300,26 @@ public void sticker_decryptFailOnBadMac() throws IOException { assertTrue(hitCorrectException); } - private static EncryptResult encryptData(byte[] data, byte[] keyMaterial) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - AttachmentCipherOutputStream encryptStream = new AttachmentCipherOutputStream(keyMaterial, null, outputStream); + private static EncryptResult encryptData(byte[] data, byte[] keyMaterial, boolean withIncremental) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ByteArrayOutputStream incrementalDigestOut = new ByteArrayOutputStream(); + byte[] iv = Util.getSecretBytes(16); + AttachmentCipherOutputStreamFactory factory = new AttachmentCipherOutputStreamFactory(keyMaterial, iv); + + DigestingOutputStream encryptStream; + if (withIncremental) { + encryptStream = factory.createIncrementalFor(outputStream, data.length, incrementalDigestOut); + } else { + encryptStream = factory.createFor(outputStream); + } + encryptStream.write(data); encryptStream.flush(); encryptStream.close(); + incrementalDigestOut.close(); - return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest()); + return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest(), incrementalDigestOut.toByteArray()); } private static File writeToFile(byte[] data) throws IOException { @@ -296,10 +345,12 @@ private static byte[] expandPackKey(byte[] shortKey) { private static class EncryptResult { final byte[] ciphertext; final byte[] digest; + final byte[] incrementalDigest; - private EncryptResult(byte[] ciphertext, byte[] digest) { + private EncryptResult(byte[] ciphertext, byte[] digest, byte[] incrementalDigest) { this.ciphertext = ciphertext; this.digest = digest; + this.incrementalDigest = incrementalDigest; } } } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java index 189e4e7215..30d8e3a335 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/http/DigestingRequestBodyTest.java @@ -23,7 +23,7 @@ public class DigestingRequestBodyTest { private final OutputStreamFactory outputStreamFactory = new AttachmentCipherOutputStreamFactory(attachmentKey, attachmentIV); @Test - public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameTransmittedDigest() throws Exception { + public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameDigests() throws Exception { DigestingRequestBody fromStart = getBody(0); DigestingRequestBody fromMiddle = getBody(CONTENT_LENGTH / 2); @@ -36,6 +36,7 @@ public void givenSameKeyAndIV_whenIWriteToBuffer_thenIExpectSameTransmittedDiges } assertArrayEquals(fromStart.getTransmittedDigest(), fromMiddle.getTransmittedDigest()); + assertArrayEquals(fromStart.getIncrementalDigest(), fromMiddle.getIncrementalDigest()); } @Test diff --git a/settings.gradle b/settings.gradle index d50017d539..4619d3d821 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,8 +43,6 @@ dependencyResolutionManagement { } } -enableFeaturePreview('VERSION_CATALOGS') - include ':app' include ':libsignal-service' include ':libfakegms'