mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-12 21:13:18 +01:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b7b9e913 | ||
|
|
840a56cbb4 | ||
|
|
aa268fc3ba | ||
|
|
889d1183b2 | ||
|
|
a8706f65d5 | ||
|
|
26bebb9811 | ||
|
|
9331e9ce89 | ||
|
|
6417f5cce0 | ||
|
|
a340ebf74a | ||
|
|
4882a4d11c | ||
|
|
b5300c877c | ||
|
|
c2b94274b0 | ||
|
|
46ec45b985 | ||
|
|
beee3b7dc3 | ||
|
|
e2a7ed86e4 | ||
|
|
95b0639ab4 | ||
|
|
d7f9582bc4 | ||
|
|
176a705079 | ||
|
|
8e9f311fca | ||
|
|
977af2c2f3 | ||
|
|
7e45fc4a3e | ||
|
|
58489bab61 | ||
|
|
0685cf4e51 | ||
|
|
9b9453734c | ||
|
|
ca0e52e141 | ||
|
|
24b7593178 | ||
|
|
993e49db48 | ||
|
|
d458ddba55 | ||
|
|
bd5747b7f6 | ||
|
|
a335130ad4 | ||
|
|
9558513190 | ||
|
|
27a3015d4f | ||
|
|
f751f9afa8 | ||
|
|
2e2b31aa79 | ||
|
|
135d002f02 | ||
|
|
a45ede9348 | ||
|
|
e4b2e5022f | ||
|
|
286010ce90 | ||
|
|
13eb89746b | ||
|
|
d2f639c57f | ||
|
|
ad587606b7 | ||
|
|
9fd5e2057d | ||
|
|
8f63b850fc | ||
|
|
199d04b663 | ||
|
|
658741be52 | ||
|
|
f1bcc756d3 | ||
|
|
cdcb1de3d4 | ||
|
|
7d11a6207a | ||
|
|
e608ad24c2 | ||
|
|
4fe382398e | ||
|
|
b6546f3ae3 | ||
|
|
4620eade58 | ||
|
|
23a328f12d | ||
|
|
83905dd6a6 | ||
|
|
3eb4eb3c09 | ||
|
|
2eba9a8d72 | ||
|
|
9b17e7a7e2 | ||
|
|
3eb9e4a035 | ||
|
|
3edc97eb38 | ||
|
|
cb0208af4d | ||
|
|
cdd311f741 | ||
|
|
8543325d59 | ||
|
|
a1a677a3e2 | ||
|
|
3705465ef2 | ||
|
|
c80999839b | ||
|
|
936212e684 | ||
|
|
1cc39fb89b | ||
|
|
37d3a953c8 | ||
|
|
5a1a23d9ac | ||
|
|
6cb359b2d0 | ||
|
|
8bd89d1e63 | ||
|
|
f111ac7cf2 | ||
|
|
f6e000ab97 | ||
|
|
29869c93b2 | ||
|
|
3aae5ce1de | ||
|
|
e379cf6127 | ||
|
|
0c23cb5ca8 | ||
|
|
d26ba27069 | ||
|
|
e918178694 | ||
|
|
3d075bdd65 | ||
|
|
4a3b8af6af | ||
|
|
2743492076 | ||
|
|
6ebc453e4b | ||
|
|
75bd950b9b | ||
|
|
0b0c4eb8c0 | ||
|
|
e7dbc874bb | ||
|
|
17426f1dbb | ||
|
|
e00ce48517 | ||
|
|
cba1caa5be | ||
|
|
5f6b073cb6 | ||
|
|
51647a5017 | ||
|
|
fae2ceab39 | ||
|
|
553346629a | ||
|
|
726f48bc33 |
@@ -57,8 +57,8 @@ ktlint {
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1117
|
||||
def canonicalVersionName = "5.48.1"
|
||||
def canonicalVersionCode = 1124
|
||||
def canonicalVersionName = "5.50.0"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -417,10 +417,11 @@ dependencies {
|
||||
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.2.0'
|
||||
strictly '1.5.1'
|
||||
}
|
||||
}
|
||||
implementation libs.androidx.window
|
||||
implementation libs.androidx.window.window
|
||||
implementation libs.androidx.window.java
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
@@ -433,7 +434,9 @@ dependencies {
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.lifecycle.extensions
|
||||
implementation libs.androidx.lifecycle.viewmodel.ktx
|
||||
implementation libs.androidx.lifecycle.livedata.ktx
|
||||
implementation libs.androidx.lifecycle.process
|
||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
||||
implementation libs.androidx.lifecycle.common.java8
|
||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
||||
@@ -470,6 +473,7 @@ dependencies {
|
||||
implementation project(':donations')
|
||||
implementation project(':contacts')
|
||||
implementation project(':qr')
|
||||
implementation project(':sms-exporter')
|
||||
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
@@ -560,6 +564,10 @@ dependencies {
|
||||
androidTestImplementation testLibs.mockito.kotlin
|
||||
androidTestImplementation testLibs.square.okhttp.mockserver
|
||||
|
||||
instrumentationImplementation (libs.androidx.fragment.testing) {
|
||||
exclude group: 'androidx.test', module: 'core'
|
||||
}
|
||||
|
||||
testImplementation testLibs.espresso.core
|
||||
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
@@ -570,7 +578,7 @@ dependencies {
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
androidTestUtil testLibs.androidx.test.orchestrator
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.testing.timeout
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import java.util.UUID
|
||||
|
||||
@@ -249,6 +250,109 @@ class ChangeNumberViewModelTest {
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v1/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
||||
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else if (changeNumberRequest.deviceMessages.size == 1) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2, 3)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Get("/v2/keys/$aci/3") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
||||
processor.registrationLock() assertIs true
|
||||
Recipient.self().requirePni() assertIsNot newPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
@@ -410,7 +410,7 @@ class RecipientDatabaseTest_processPnpTuple {
|
||||
fun process(e164: String?, pni: PNI?, aci: ACI?) {
|
||||
SignalDatabase.rawDatabase.beginTransaction()
|
||||
try {
|
||||
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false, pnpEnabled = true).finalId
|
||||
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false).finalId
|
||||
SignalDatabase.rawDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
SignalDatabase.rawDatabase.endTransaction()
|
||||
|
||||
@@ -109,7 +109,7 @@ class MyStoryMigrationTest {
|
||||
}
|
||||
|
||||
private fun runMigration() {
|
||||
MyStoryMigration.migrate(
|
||||
V151_MyStoryMigration.migrate(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
|
||||
SignalDatabase.rawDatabase,
|
||||
0,
|
||||
|
||||
@@ -77,6 +77,7 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
serviceNetworkAccessMock = mock {
|
||||
on { getConfiguration() } doReturn uncensoredConfiguration
|
||||
on { getConfiguration(any()) } doReturn uncensoredConfiguration
|
||||
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
||||
}
|
||||
|
||||
keyBackupService = mock()
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UsernameEditFragmentTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
private val ioScheduler = TestScheduler()
|
||||
private val computationScheduler = TestScheduler()
|
||||
|
||||
@get:Rule
|
||||
val testSchedulerRule = RxTestSchedulerRule(
|
||||
ioTestScheduler = ioScheduler,
|
||||
computationTestScheduler = computationScheduler
|
||||
)
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUsernameCreationInRegistration() {
|
||||
val scenario = createScenario(true)
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
||||
noViewFoundException.assertIsNull()
|
||||
val toolbar = view as Toolbar
|
||||
|
||||
toolbar.navigationIcon.assertIsNull()
|
||||
}
|
||||
|
||||
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUsernameCreationOutsideOfRegistration() {
|
||||
val scenario = createScenario()
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
||||
noViewFoundException.assertIsNull()
|
||||
val toolbar = view as Toolbar
|
||||
|
||||
toolbar.navigationIcon.assertIsNotNull()
|
||||
}
|
||||
|
||||
onView(withText(R.string.UsernameEditFragment_username)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNicknameUpdateHappyPath() {
|
||||
val nickname = "Spiderman"
|
||||
val discriminator = "4578"
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/accounts/username/reserved") {
|
||||
MockResponse().success(ReserveUsernameResponse("$nickname#$discriminator", "reservationToken"))
|
||||
},
|
||||
Put("/v1/accounts/username/confirm") {
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
val scenario = createScenario(isInRegistration = true)
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.username_text)).perform(typeText(nickname))
|
||||
|
||||
computationScheduler.advanceTimeBy(501, TimeUnit.MILLISECONDS)
|
||||
computationScheduler.triggerActions()
|
||||
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
|
||||
|
||||
ioScheduler.triggerActions()
|
||||
computationScheduler.triggerActions()
|
||||
|
||||
onView(withId(R.id.username_text)).perform(closeSoftKeyboard())
|
||||
onView(withId(R.id.username_done_button)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.username_done_button)).check(matches(isEnabled()))
|
||||
onView(withId(R.id.username_done_button)).perform(click())
|
||||
|
||||
computationScheduler.triggerActions()
|
||||
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
|
||||
}
|
||||
|
||||
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
|
||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
|
||||
return launchFragmentInContainer(
|
||||
fragmentArgs = fragmentArgs,
|
||||
themeResId = R.style.Signal_DayNight_NoActionBar
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,14 @@ import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.test.BuildConfig
|
||||
@@ -16,10 +23,14 @@ import org.whispersystems.signalservice.api.kbs.HashedPin
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.DeviceInfoList
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyResponse
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyResponseItem
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.SenderCertificate
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
@@ -83,4 +94,21 @@ object MockProvider {
|
||||
on { newRegistrationSession(any(), any()) } doReturn session
|
||||
}
|
||||
}
|
||||
|
||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
||||
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
||||
|
||||
val device = PreKeyResponseItem().apply {
|
||||
this.deviceId = deviceId
|
||||
registrationId = KeyHelper.generateRegistrationId(false)
|
||||
signedPreKey = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
preKey = PreKeyEntity(oneTimePreKey.id, oneTimePreKey.keyPair.publicKey)
|
||||
}
|
||||
|
||||
return PreKeyResponse().apply {
|
||||
identityKey = identity.publicKey
|
||||
devices = listOf(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.schedulers.TestScheduler
|
||||
import org.junit.rules.ExternalResource
|
||||
|
||||
/**
|
||||
* JUnit Rule which initialises Rx thread schedulers. If a specific
|
||||
* scheduler is not specified, it defaults to the `defaultTestScheduler`
|
||||
*/
|
||||
class RxTestSchedulerRule(
|
||||
val defaultTestScheduler: TestScheduler = TestScheduler(),
|
||||
val ioTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val computationTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val singleTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
val newThreadTestScheduler: TestScheduler = defaultTestScheduler,
|
||||
) : ExternalResource() {
|
||||
|
||||
override fun before() {
|
||||
RxJavaPlugins.setInitIoSchedulerHandler { ioTestScheduler }
|
||||
RxJavaPlugins.setIoSchedulerHandler { ioTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler { computationTestScheduler }
|
||||
RxJavaPlugins.setComputationSchedulerHandler { computationTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitSingleSchedulerHandler { singleTestScheduler }
|
||||
RxJavaPlugins.setSingleSchedulerHandler { singleTestScheduler }
|
||||
|
||||
RxJavaPlugins.setInitNewThreadSchedulerHandler { newThreadTestScheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { newThreadTestScheduler }
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
RxJavaPlugins.reset()
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
|
||||
others += recipientId
|
||||
|
||||
@@ -554,7 +554,7 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".blocked.BlockedUsersActivity"
|
||||
@@ -570,6 +570,10 @@
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".profiles.username.AddAUsernameActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
<activity android:name=".profiles.manage.ManageProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
@@ -659,7 +663,7 @@
|
||||
|
||||
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.FullScreenMedia" />
|
||||
android:theme="@style/TextSecure.DarkNoActionBar" />
|
||||
|
||||
<activity android:name=".wallpaper.crop.WallpaperCropActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -670,6 +674,11 @@
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".exporter.flow.SmsExportActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
|
||||
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package androidx.documentfile.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
/**
|
||||
* Located in androidx package as {@link TreeDocumentFile} is package protected.
|
||||
*/
|
||||
public class DocumentFileHelper {
|
||||
|
||||
private static final String TAG = Log.tag(DocumentFileHelper.class);
|
||||
|
||||
/**
|
||||
* System implementation swallows the exception and we are having problems with the rename. This inlines the
|
||||
* same call and logs the exception. Note this implementation does not update the passed in document file like
|
||||
* the system implementation. Do not use the provided document file after calling this method.
|
||||
*
|
||||
* @return true if rename successful
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
public static boolean renameTo(Context context, DocumentFile documentFile, String displayName) {
|
||||
if (documentFile instanceof TreeDocumentFile) {
|
||||
Log.d(TAG, "Renaming document directly");
|
||||
try {
|
||||
final Uri result = DocumentsContract.renameDocument(context.getContentResolver(), documentFile.getUri(), displayName);
|
||||
return result != null;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Unable to rename document file", e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Letting OS rename document: " + documentFile.getClass().getSimpleName());
|
||||
return documentFile.renameTo(displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadgeReceiveSupport());
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadgeReceiveSupport(), FeatureFlags.phoneNumberPrivacy());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
@@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
@@ -194,6 +196,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
@@ -204,6 +207,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -383,6 +387,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializeTrimThreadsByDateManager() {
|
||||
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
|
||||
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
|
||||
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
|
||||
@@ -44,7 +44,7 @@ import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -168,7 +168,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
|
||||
@@ -20,17 +20,20 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.profiles.username.AddAUsernameActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -52,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
private static final int STATE_CREATE_USERNAME = 11;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -156,6 +160,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
case STATE_CREATE_USERNAME: return getCreateUsernameIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -175,6 +180,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (shouldAskUserToCreateUsername()) {
|
||||
return STATE_CREATE_USERNAME;
|
||||
} else if (userMustCreateSignalPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) {
|
||||
@@ -200,6 +207,13 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||
}
|
||||
|
||||
private boolean shouldAskUserToCreateUsername() {
|
||||
return FeatureFlags.usernames() &&
|
||||
FeatureFlags.phoneNumberPrivacy() &&
|
||||
!SignalStore.uiHints().hasSetOrSkippedUsernameCreation() &&
|
||||
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode() == PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED;
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
|
||||
}
|
||||
@@ -259,6 +273,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getCreateUsernameIntent() {
|
||||
return getRoutedIntent(AddAUsernameActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
|
||||
if (nextIntent != null) destination.putExtra("next_intent", nextIntent);
|
||||
return destination;
|
||||
|
||||
@@ -40,15 +40,18 @@ import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.window.DisplayFeature;
|
||||
import androidx.window.FoldingFeature;
|
||||
import androidx.window.WindowLayoutInfo;
|
||||
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
|
||||
import androidx.window.layout.DisplayFeature;
|
||||
import androidx.window.layout.FoldingFeature;
|
||||
import androidx.window.layout.WindowInfoTracker;
|
||||
import androidx.window.layout.WindowLayoutInfo;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
@@ -113,15 +116,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private WifiToCellularPopupWindow wifiToCellularPopupWindow;
|
||||
private DeviceOrientationMonitor deviceOrientationMonitor;
|
||||
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
private boolean hasWarnedAboutBluetooth;
|
||||
private androidx.window.WindowManager windowManager;
|
||||
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
private WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
private boolean hasWarnedAboutBluetooth;
|
||||
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
|
||||
private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@@ -159,10 +162,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
|
||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||
|
||||
windowManager = new androidx.window.WindowManager(this);
|
||||
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
|
||||
|
||||
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
|
||||
windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
@@ -186,6 +189,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
if (!EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
WebRtcViewModel rtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
||||
if (rtcViewModel == null) {
|
||||
Log.w(TAG, "Activity resumed without service event, perform delay destroy");
|
||||
ThreadUtil.runOnMainDelayed(() -> {
|
||||
WebRtcViewModel delayRtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
|
||||
if (delayRtcViewModel == null) {
|
||||
Log.w(TAG, "Activity still without service event, finishing activity");
|
||||
finish();
|
||||
} else {
|
||||
Log.i(TAG, "Event found after delay");
|
||||
}
|
||||
}, TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -235,7 +252,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
|
||||
windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@@ -258,7 +275,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
|
||||
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
|
||||
viewModel.setIsInPipMode(isInPictureInPictureMode);
|
||||
participantUpdateWindow.setEnabled(!isInPictureInPictureMode);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
@@ -48,7 +48,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
BlockedUsersRepository repository = new BlockedUsersRepository(this);
|
||||
BlockedUsersViewModel.Factory factory = new BlockedUsersViewModel.Factory(repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(BlockedUsersViewModel.class);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
@@ -59,7 +59,7 @@ public class BlockedUsersFragment extends Fragment {
|
||||
}
|
||||
});
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(BlockedUsersViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(BlockedUsersViewModel.class);
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> {
|
||||
if (list.isEmpty()) {
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
@@ -126,6 +127,11 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||
|
||||
int backgroundTintColor = typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.TRANSPARENT);
|
||||
if (getBackground() != null && backgroundTintColor != Color.TRANSPARENT) {
|
||||
DrawableCompat.setTint(getBackground(), backgroundTintColor);
|
||||
}
|
||||
|
||||
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
|
||||
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
|
||||
|
||||
@@ -64,6 +64,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
private @Nullable AvatarColor fallbackPhotoColor;
|
||||
|
||||
public AvatarImageView(Context context) {
|
||||
super(context);
|
||||
@@ -105,6 +106,10 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
this.fallbackPhotoProvider = fallbackPhotoProvider;
|
||||
}
|
||||
|
||||
public void setFallbackPhotoColor(@Nullable AvatarColor fallbackPhotoColor) {
|
||||
this.fallbackPhotoColor = fallbackPhotoColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows self as the actual profile picture.
|
||||
*/
|
||||
@@ -213,7 +218,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
requestManager.clear(this);
|
||||
if (fallbackPhotoProvider != null) {
|
||||
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
|
||||
.asDrawable(getContext(), AvatarColor.UNKNOWN, inverted));
|
||||
.asDrawable(getContext(), Util.firstNonNull(fallbackPhotoColor, AvatarColor.UNKNOWN), inverted));
|
||||
} else {
|
||||
setImageDrawable(unknownRecipientDrawable);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ class Material3SearchToolbar @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun clearText() {
|
||||
input.setText("")
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onSearchTextChange(text: String)
|
||||
fun onSearchClosed()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* A small card with a circular progress indicator in it. Usable in place
|
||||
* of a ProgressDialog, which is deprecated.
|
||||
*
|
||||
* Remember to add this as the last UI element in your XML hierarchy so it'll
|
||||
* draw over top of other elements.
|
||||
*/
|
||||
class ProgressCard @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : MaterialCardView(context, attrs) {
|
||||
init {
|
||||
inflate(context, R.layout.progress_card, this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* ViewBinderDelegate which enforces the "best practices" for maintaining a reference to a view binding given by
|
||||
* Android official documentation.
|
||||
*/
|
||||
class ViewBinderDelegate<T : ViewBinding>(private val bindingFactory: (View) -> T) : DefaultLifecycleObserver {
|
||||
|
||||
private var binding: T? = null
|
||||
|
||||
operator fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||
Preconditions.checkState(thisRef.viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED))
|
||||
|
||||
if (binding == null) {
|
||||
thisRef.viewLifecycleOwner.lifecycle.addObserver(this@ViewBinderDelegate)
|
||||
binding = bindingFactory(thisRef.requireView())
|
||||
}
|
||||
|
||||
return binding!!
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
binding = null
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,9 @@ final class ReminderActionsAdapter extends RecyclerView.Adapter<ReminderActionsA
|
||||
TextView button = ((TextView) LayoutInflater.from(context).inflate(R.layout.reminder_action_button, parent, false));
|
||||
|
||||
if (importance == Reminder.Importance.NORMAL) {
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.signal_accent_primary));
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.signal_colorPrimary));
|
||||
} else if (importance == Reminder.Importance.ERROR || importance == Reminder.Importance.TERMINAL) {
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnSurface));
|
||||
}
|
||||
|
||||
return new ActionViewHolder(button);
|
||||
|
||||
@@ -17,6 +17,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@@ -28,8 +30,7 @@ import java.util.List;
|
||||
public final class ReminderView extends FrameLayout {
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressText;
|
||||
private ViewGroup container;
|
||||
private View background;
|
||||
private MaterialCardView container;
|
||||
private ImageButton closeButton;
|
||||
private TextView title;
|
||||
private TextView text;
|
||||
@@ -58,7 +59,6 @@ public final class ReminderView extends FrameLayout {
|
||||
progressBar = findViewById(R.id.reminder_progress);
|
||||
progressText = findViewById(R.id.reminder_progress_text);
|
||||
container = findViewById(R.id.container);
|
||||
background = findViewById(R.id.background);
|
||||
closeButton = findViewById(R.id.cancel);
|
||||
title = findViewById(R.id.reminder_title);
|
||||
text = findViewById(R.id.reminder_text);
|
||||
@@ -75,6 +75,7 @@ public final class ReminderView extends FrameLayout {
|
||||
title.setText("");
|
||||
title.setVisibility(GONE);
|
||||
space.setVisibility(VISIBLE);
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
|
||||
}
|
||||
|
||||
if (!reminder.isDismissable()) {
|
||||
@@ -82,22 +83,17 @@ public final class ReminderView extends FrameLayout {
|
||||
}
|
||||
|
||||
text.setText(reminder.getText());
|
||||
|
||||
switch (reminder.getImportance()) {
|
||||
case NORMAL:
|
||||
background.setBackgroundResource(R.drawable.reminder_background_normal);
|
||||
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
||||
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurfaceVariant));
|
||||
break;
|
||||
case ERROR:
|
||||
background.setBackgroundResource(R.drawable.reminder_background_error);
|
||||
title.setTextColor(ContextCompat.getColor(getContext(), R.color.core_black));
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.core_black));
|
||||
break;
|
||||
case TERMINAL:
|
||||
background.setBackgroundResource(R.drawable.reminder_background_terminal);
|
||||
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text));
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text));
|
||||
container.setStrokeWidth(0);
|
||||
container.setCardBackgroundColor(ContextCompat.getColor(getContext(), R.color.reminder_background));
|
||||
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_light_colorOnSurface));
|
||||
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_light_colorOnSurface));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
@@ -118,7 +114,7 @@ public final class ReminderView extends FrameLayout {
|
||||
});
|
||||
|
||||
if (reminder.getImportance() == Reminder.Importance.NORMAL) {
|
||||
closeButton.setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
||||
closeButton.setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurfaceVariant));
|
||||
}
|
||||
|
||||
int progress = reminder.getProgress();
|
||||
|
||||
@@ -58,7 +58,10 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
Single
|
||||
.just(true)
|
||||
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) }
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.map { true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.SignalProtocolStore
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
@@ -30,7 +30,6 @@ import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
@@ -41,13 +40,47 @@ import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
class ChangeNumberRepository(private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()) {
|
||||
/**
|
||||
* Provides various change number operations. All operations must run on [Schedulers.single] to support
|
||||
* the global "I am changing the number" lock exclusivity.
|
||||
*/
|
||||
class ChangeNumberRepository(
|
||||
private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(),
|
||||
private val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* This lock should be held by anyone who is performing a change number operation, so that two different parties cannot change the user's number
|
||||
* at the same time.
|
||||
*/
|
||||
val CHANGE_NUMBER_LOCK = ReentrantLock()
|
||||
|
||||
/**
|
||||
* Adds Rx operators to chain to acquire and release the [CHANGE_NUMBER_LOCK] on subscribe and on finish.
|
||||
*/
|
||||
fun <T : Any> acquireReleaseChangeNumberLock(upstream: Single<T>): Single<T> {
|
||||
return upstream.doOnSubscribe {
|
||||
CHANGE_NUMBER_LOCK.lock()
|
||||
SignalStore.misc().lockChangeNumber()
|
||||
}
|
||||
.subscribeOn(Schedulers.single())
|
||||
.observeOn(Schedulers.single())
|
||||
.doFinally {
|
||||
if (CHANGE_NUMBER_LOCK.isHeldByCurrentThread) {
|
||||
CHANGE_NUMBER_LOCK.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return Completable.create { emitter ->
|
||||
@@ -56,15 +89,38 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
.addDecryptionDrainedListener {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}.subscribeOn(Schedulers.single())
|
||||
}
|
||||
|
||||
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
fun changeNumber(code: String, newE164: String, pniUpdateMode: Boolean = false): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return Single.fromCallable {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null)
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
accountManager.changeNumber(request)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
code = code,
|
||||
newE164 = newE164,
|
||||
registrationLock = null,
|
||||
pniUpdateMode = pniUpdateMode
|
||||
)
|
||||
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
changeNumberResponse = accountManager.changeNumber(request)
|
||||
|
||||
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
|
||||
if (possibleError is MismatchedDevicesException) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
|
||||
changeNumberResponse
|
||||
}.subscribeOn(Schedulers.single())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
@@ -75,39 +131,63 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
tokenData: TokenData
|
||||
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
|
||||
return Single.fromCallable {
|
||||
val kbsData: KbsPinData
|
||||
val registrationLock: String
|
||||
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock)
|
||||
kbsData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
registrationLock = kbsData.masterKey.deriveRegistrationLock()
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
} catch (e: IOException) {
|
||||
return@fromCallable ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
|
||||
var completed = false
|
||||
var attempts = 0
|
||||
lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
|
||||
|
||||
while (!completed && attempts < 5) {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
|
||||
code = code,
|
||||
newE164 = newE164,
|
||||
registrationLock = registrationLock,
|
||||
pniUpdateMode = false
|
||||
)
|
||||
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(request)
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
} catch (e: IOException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
changeNumberResponse = accountManager.changeNumber(request)
|
||||
|
||||
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
|
||||
if (possibleError is MismatchedDevicesException) {
|
||||
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
|
||||
attempts++
|
||||
} else {
|
||||
completed = true
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(changeNumberResponse, kbsData)
|
||||
}.subscribeOn(Schedulers.single())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
fun whoAmI(): Single<WhoAmIResponse> {
|
||||
return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeOn(Schedulers.single())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
|
||||
val oldStorageId: ByteArray? = Recipient.self().storageServiceId
|
||||
SignalDatabase.recipients.updateSelfPhone(e164)
|
||||
SignalDatabase.recipients.updateSelfPhone(e164, pni)
|
||||
val newStorageId: ByteArray? = Recipient.self().storageServiceId
|
||||
|
||||
if (MessageDigest.isEqual(oldStorageId, newStorageId)) {
|
||||
if (e164 != SignalStore.account().requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) {
|
||||
Log.w(TAG, "Self storage id was not rotated, attempting to rotate again")
|
||||
SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
@@ -117,7 +197,6 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.setPni(Recipient.self().id, pni)
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
|
||||
SignalStore.account().setE164(e164)
|
||||
@@ -161,6 +240,9 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
|
||||
SignalStore.misc().setPniInitializedDevices(true)
|
||||
ApplicationDependencies.getGroupsV2Authorization().clear()
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh()
|
||||
@@ -190,7 +272,7 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
|
||||
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}.subscribeOn(Schedulers.single())
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@@ -198,51 +280,69 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
|
||||
private fun createChangeNumberRequest(
|
||||
code: String,
|
||||
newE164: String,
|
||||
registrationLock: String?
|
||||
registrationLock: String?,
|
||||
pniUpdateMode: Boolean
|
||||
): ChangeNumberRequestData {
|
||||
val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
|
||||
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore: PreKeyMetadataStore = SignalStore.account().pniPreKeys
|
||||
val selfIdentifier: String = SignalStore.account().requireAci().toString()
|
||||
val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci()
|
||||
|
||||
val devices: List<DeviceInfo> = accountManager.getDevices()
|
||||
|
||||
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
val pniIdentity: IdentityKeyPair = if (pniUpdateMode) SignalStore.account().pniIdentityKey else IdentityKeyUtil.generateIdentityKeyPair()
|
||||
val deviceMessages = mutableListOf<OutgoingPushMessage>()
|
||||
val devicePniSignedPreKeys = mutableMapOf<String, SignedPreKeyEntity>()
|
||||
val pniRegistrationIds = mutableMapOf<String, Int>()
|
||||
val primaryDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID.toString()
|
||||
val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
|
||||
val pniRegistrationIds = mutableMapOf<Int, Int>()
|
||||
val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
|
||||
|
||||
for (device in devices) {
|
||||
val deviceId = device.id.toString()
|
||||
val devices: List<Int> = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier)
|
||||
|
||||
// Signed Prekeys
|
||||
val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
PreKeyUtil.generateAndStoreSignedPreKey(pniProtocolStore, pniMetadataStore, pniIdentity.privateKey)
|
||||
} else {
|
||||
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
devices
|
||||
.filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) }
|
||||
.forEach { deviceId ->
|
||||
// Signed Prekeys
|
||||
val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
if (pniUpdateMode) {
|
||||
ApplicationDependencies.getProtocolStore().pni().loadSignedPreKey(SignalStore.account().pniPreKeys.activeSignedPreKeyId)
|
||||
} else {
|
||||
PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
|
||||
}
|
||||
} else {
|
||||
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
}
|
||||
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
|
||||
// Registration Ids
|
||||
var pniRegistrationId = if (deviceId == primaryDeviceId && pniUpdateMode) {
|
||||
SignalStore.account().pniRegistrationId
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
}
|
||||
pniRegistrationIds[deviceId] = pniRegistrationId
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
|
||||
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
|
||||
.setRegistrationId(pniRegistrationId)
|
||||
.build()
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(deviceId, pniChangeNumber)
|
||||
}
|
||||
}
|
||||
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
|
||||
// Registration Ids
|
||||
var pniRegistrationId = -1
|
||||
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
}
|
||||
pniRegistrationIds[deviceId] = pniRegistrationId
|
||||
val request = ChangePhoneNumberRequest(
|
||||
newE164,
|
||||
code,
|
||||
registrationLock,
|
||||
pniIdentity.publicKey,
|
||||
deviceMessages,
|
||||
devicePniSignedPreKeys.mapKeys { it.key.toString() },
|
||||
pniRegistrationIds.mapKeys { it.key.toString() }
|
||||
)
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
|
||||
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
|
||||
.setRegistrationId(pniRegistrationId)
|
||||
.build()
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(device.id, pniChangeNumber)
|
||||
}
|
||||
}
|
||||
|
||||
val request = ChangePhoneNumberRequest(newE164, code, registrationLock, pniIdentity.publicKey, deviceMessages, devicePniSignedPreKeys, pniRegistrationIds)
|
||||
val metadata = PendingChangeNumberMetadata.newBuilder()
|
||||
.setPreviousPni(SignalStore.account().pni!!.toByteString())
|
||||
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
|
||||
@@ -123,13 +123,13 @@ class ChangeNumberViewModel(
|
||||
|
||||
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
|
||||
return super.verifyCodeWithoutRegistrationLock(code)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
|
||||
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
|
||||
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
|
||||
.flatMap(this::attemptToUnlockChangeNumber)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.SmsUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
@@ -22,6 +30,7 @@ private const val SMS_REQUEST_CODE: Short = 1234
|
||||
class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
|
||||
private lateinit var viewModel: SmsSettingsViewModel
|
||||
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -29,6 +38,12 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
showSmsRemovalDialog()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java]
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
@@ -42,6 +57,32 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
|
||||
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
when (state.smsExportState) {
|
||||
SmsSettingsState.SmsExportState.FETCHING -> Unit
|
||||
SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages),
|
||||
onClick = {
|
||||
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext()))
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
}
|
||||
SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED -> {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages),
|
||||
onClick = {
|
||||
showSmsRemovalDialog()
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
}
|
||||
SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
|
||||
SmsSettingsState.SmsExportState.NOT_AVAILABLE -> Unit
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
|
||||
@@ -96,4 +137,19 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
|
||||
|
||||
startActivityForResult(intent, SMS_REQUEST_CODE.toInt())
|
||||
}
|
||||
|
||||
private fun showSmsRemovalDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.RemoveSmsMessagesDialogFragment__remove_sms_messages)
|
||||
.setMessage(R.string.RemoveSmsMessagesDialogFragment__you_have_changed)
|
||||
.setPositiveButton(R.string.RemoveSmsMessagesDialogFragment__keep_messages) { _, _ -> }
|
||||
.setNegativeButton(R.string.RemoveSmsMessagesDialogFragment__remove_messages) { _, _ ->
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
SignalDatabase.sms.deleteExportedMessages()
|
||||
SignalDatabase.mms.deleteExportedMessages()
|
||||
}
|
||||
Snackbar.make(requireView(), R.string.SmsSettingsFragment__sms_messages_removed, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
class SmsSettingsRepository {
|
||||
fun getSmsExportState(): Single<SmsSettingsState.SmsExportState> {
|
||||
if (!FeatureFlags.smsExporter()) {
|
||||
return Single.just(SmsSettingsState.SmsExportState.NOT_AVAILABLE)
|
||||
}
|
||||
|
||||
return Single.fromCallable {
|
||||
checkInsecureMessageCount() ?: checkUnexportedInsecureMessageCount()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun checkInsecureMessageCount(): SmsSettingsState.SmsExportState? {
|
||||
val smsCount = SignalDatabase.sms.insecureMessageCount
|
||||
val mmsCount = SignalDatabase.mms.insecureMessageCount
|
||||
val totalSmsMmsCount = smsCount + mmsCount
|
||||
|
||||
return if (totalSmsMmsCount == 0) {
|
||||
SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun checkUnexportedInsecureMessageCount(): SmsSettingsState.SmsExportState {
|
||||
val unexportedSmsCount = SignalDatabase.sms.unexportedInsecureMessages.use { it.count }
|
||||
val unexportedMmsCount = SignalDatabase.mms.unexportedInsecureMessages.use { it.count }
|
||||
val totalUnexportedCount = unexportedSmsCount + unexportedMmsCount
|
||||
|
||||
return if (totalUnexportedCount > 0) {
|
||||
SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES
|
||||
} else {
|
||||
SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,14 @@ package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||
data class SmsSettingsState(
|
||||
val useAsDefaultSmsApp: Boolean,
|
||||
val smsDeliveryReportsEnabled: Boolean,
|
||||
val wifiCallingCompatibilityEnabled: Boolean
|
||||
)
|
||||
val wifiCallingCompatibilityEnabled: Boolean,
|
||||
val smsExportState: SmsExportState = SmsExportState.FETCHING
|
||||
) {
|
||||
enum class SmsExportState {
|
||||
FETCHING,
|
||||
HAS_UNEXPORTED_MESSAGES,
|
||||
ALL_MESSAGES_EXPORTED,
|
||||
NO_SMS_MESSAGES_IN_DATABASE,
|
||||
NOT_AVAILABLE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.settings.app.chats.sms
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
@@ -9,6 +11,9 @@ import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class SmsSettingsViewModel : ViewModel() {
|
||||
|
||||
private val repository = SmsSettingsRepository()
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val store = Store(
|
||||
SmsSettingsState(
|
||||
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
|
||||
@@ -19,6 +24,16 @@ class SmsSettingsViewModel : ViewModel() {
|
||||
|
||||
val state: LiveData<SmsSettingsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
disposables += repository.getSmsExportState().subscribe { state ->
|
||||
store.update { it.copy(smsExportState = state) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun setSmsDeliveryReportsEnabled(enabled: Boolean) {
|
||||
store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
|
||||
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled
|
||||
|
||||
@@ -171,15 +171,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v2)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2_description),
|
||||
isChecked = state.gv2doNotCreateGv2Groups,
|
||||
onClick = {
|
||||
viewModel.setGv2DoNotCreateGv2Groups(!state.gv2doNotCreateGv2Groups)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description),
|
||||
@@ -209,28 +200,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v1_migration)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate_description),
|
||||
isChecked = state.disableAutoMigrationInitiation,
|
||||
onClick = {
|
||||
viewModel.setDisableAutoMigrationInitiation(!state.disableAutoMigrationInitiation)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate_description),
|
||||
isChecked = state.disableAutoMigrationNotification,
|
||||
onClick = {
|
||||
viewModel.setDisableAutoMigrationNotification(!state.disableAutoMigrationNotification)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_network)
|
||||
|
||||
switchPref(
|
||||
|
||||
@@ -6,12 +6,9 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles
|
||||
data class InternalSettingsState(
|
||||
val seeMoreUserDetails: Boolean,
|
||||
val shakeToReport: Boolean,
|
||||
val gv2doNotCreateGv2Groups: Boolean,
|
||||
val gv2forceInvites: Boolean,
|
||||
val gv2ignoreServerChanges: Boolean,
|
||||
val gv2ignoreP2PChanges: Boolean,
|
||||
val disableAutoMigrationInitiation: Boolean,
|
||||
val disableAutoMigrationNotification: Boolean,
|
||||
val allowCensorshipSetting: Boolean,
|
||||
val callingServer: String,
|
||||
val callingAudioProcessingMethod: CallManager.AudioProcessingMethod,
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.InternalValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
@@ -38,11 +39,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setGv2ForceInvites(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_FORCE_INVITES, enabled)
|
||||
refresh()
|
||||
@@ -58,16 +54,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setDisableAutoMigrationInitiation(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_INITIATION, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setDisableAutoMigrationNotification(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_NOTIFICATION, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setAllowCensorshipSetting(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.ALLOW_CENSORSHIP_SETTING, enabled)
|
||||
refresh()
|
||||
@@ -125,12 +111,9 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
private fun getState() = InternalSettingsState(
|
||||
seeMoreUserDetails = SignalStore.internalValues().recipientDetails(),
|
||||
shakeToReport = SignalStore.internalValues().shakeToReport(),
|
||||
gv2doNotCreateGv2Groups = SignalStore.internalValues().gv2DoNotCreateGv2Groups(),
|
||||
gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(),
|
||||
gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(),
|
||||
gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(),
|
||||
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
|
||||
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
|
||||
allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(),
|
||||
callingServer = SignalStore.internalValues().groupCallingServer(),
|
||||
callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(),
|
||||
@@ -148,6 +131,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
fun onClearOnboardingState() {
|
||||
SignalStore.storyValues().hasDownloadedOnboardingStory = false
|
||||
SignalStore.storyValues().userHasSeenOnboardingStory = false
|
||||
Stories.onStorySettingsChanged(Recipient.self().id)
|
||||
refresh()
|
||||
StoryOnboardingDownloadJob.enqueueIfNeeded()
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
summary = DSLSettingsText.from(R.string.preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity),
|
||||
isChecked = state.isObsoletePasswordTimeoutEnabled,
|
||||
onClick = {
|
||||
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordEnabled)
|
||||
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordTimeoutEnabled)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -287,7 +287,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
requireContext(),
|
||||
StoryViewerArgs(
|
||||
recipientId = state.recipient.id,
|
||||
isInHiddenStoryMode = state.recipient.shouldHideStory()
|
||||
isInHiddenStoryMode = state.recipient.shouldHideStory(),
|
||||
isFromQuote = true
|
||||
)
|
||||
)
|
||||
StoryDialogs.displayStoryOrProfileImage(
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
@@ -62,27 +61,26 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
if (recipient.isSelf) {
|
||||
val aci: String = SignalStore.account().aci?.toString() ?: "null"
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("ACI"),
|
||||
summary = DSLSettingsText.from(aci),
|
||||
onLongClick = { copyToClipboard(aci) }
|
||||
)
|
||||
val pni: String = SignalStore.account().pni?.toString() ?: "null"
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("PNI"),
|
||||
summary = DSLSettingsText.from(pni),
|
||||
onLongClick = { copyToClipboard(pni) }
|
||||
)
|
||||
} else {
|
||||
val serviceId: String = recipient.serviceId.map(ServiceId::toString).orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("ServiceId"),
|
||||
summary = DSLSettingsText.from(serviceId),
|
||||
onLongClick = { copyToClipboard(serviceId) }
|
||||
)
|
||||
}
|
||||
val e164: String = recipient.e164.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("E164"),
|
||||
summary = DSLSettingsText.from(e164),
|
||||
onLongClick = { copyToClipboard(e164) }
|
||||
)
|
||||
|
||||
val serviceId: String = recipient.serviceId.map { it.toString() }.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("ServiceId"),
|
||||
summary = DSLSettingsText.from(serviceId),
|
||||
onLongClick = { copyToClipboard(serviceId) }
|
||||
)
|
||||
|
||||
val pni: String = recipient.pni.map { it.toString() }.orElse("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("PNI"),
|
||||
summary = DSLSettingsText.from(pni),
|
||||
onLongClick = { copyToClipboard(pni) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.groupId != null) {
|
||||
|
||||
@@ -175,8 +175,8 @@ class VoiceNoteMediaItemFactory {
|
||||
sender.getDisplayName(context),
|
||||
threadRecipient.getDisplayName(context));
|
||||
} else if (preference.isDisplayContact()) {
|
||||
return sender.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: sender.getDisplayName(context);
|
||||
return sender.isSelf() && threadRecipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: sender.getDisplayName(context);
|
||||
} else {
|
||||
return context.getString(R.string.MessageNotifier_signal_message);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class VoiceNotePlaybackController(
|
||||
}
|
||||
|
||||
player.playWhenReady = false
|
||||
player.setAudioAttributes(attributes, false)
|
||||
player.setAudioAttributes(attributes, newStreamType == AudioManager.STREAM_MUSIC)
|
||||
|
||||
if (newStreamType == AudioManager.STREAM_VOICE_CALL) {
|
||||
player.playWhenReady = true
|
||||
|
||||
@@ -39,7 +39,7 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI
|
||||
*/
|
||||
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
|
||||
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
|
||||
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.DefaultLoadControl
|
||||
import com.google.android.exoplayer2.ForwardingPlayer
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import org.thoughtcrime.securesms.video.exo.SignalMediaSourceFactory
|
||||
|
||||
class VoiceNotePlayer @JvmOverloads constructor(
|
||||
@@ -15,7 +16,9 @@ class VoiceNotePlayer @JvmOverloads constructor(
|
||||
DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)
|
||||
.build()
|
||||
).build()
|
||||
).build().apply {
|
||||
setAudioAttributes(AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build(), true)
|
||||
}
|
||||
) : ForwardingPlayer(internalPlayer) {
|
||||
|
||||
override fun seekTo(windowIndex: Int, positionMs: Long) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
|
||||
final WebRtcCallViewModel viewModel = new ViewModelProvider(requireActivity()).get(WebRtcCallViewModel.class);
|
||||
|
||||
initializeList();
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.contacts;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
@@ -10,18 +12,23 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -51,6 +58,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
|
||||
private final UsernameFallbackPhotoProvider usernameFallbackPhotoProvider = new UsernameFallbackPhotoProvider();
|
||||
|
||||
public ContactSelectionListItem(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -104,8 +113,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
this.contactLabel = label;
|
||||
this.contactAbout = about;
|
||||
|
||||
this.contactPhotoImage.setFallbackPhotoProvider(null);
|
||||
if (type == ContactRepository.NEW_PHONE_TYPE || type == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
this.recipient = null;
|
||||
this.contactPhotoImage.setFallbackPhotoProvider(usernameFallbackPhotoProvider);
|
||||
this.contactPhotoImage.setFallbackPhotoColor(AvatarColor.ON_SURFACE_VARIANT);
|
||||
this.contactPhotoImage.setAvatar(glideRequests, null, false);
|
||||
} else if (recipientId != null) {
|
||||
if (this.recipient != null) {
|
||||
@@ -168,6 +180,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label, @Nullable String about) {
|
||||
this.numberView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (number == null || number.isEmpty()) {
|
||||
this.nameView.setEnabled(false);
|
||||
this.numberView.setText("");
|
||||
@@ -181,10 +195,9 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
this.nameView.setEnabled(true);
|
||||
this.labelView.setVisibility(View.GONE);
|
||||
} else if (type == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
this.numberView.setText("@" + number);
|
||||
this.numberView.setVisibility(View.GONE);
|
||||
this.nameView.setEnabled(true);
|
||||
this.labelView.setText(label);
|
||||
this.labelView.setVisibility(View.VISIBLE);
|
||||
this.labelView.setVisibility(View.GONE);
|
||||
} else if (recipient != null && recipient.isDistributionList()) {
|
||||
this.numberView.setText(getViewerCount(number));
|
||||
this.labelView.setVisibility(View.GONE);
|
||||
@@ -198,6 +211,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
if (recipient != null) {
|
||||
this.nameView.setText(recipient);
|
||||
chipName = recipient.getShortDisplayName(getContext());
|
||||
} else if (type == ContactRepository.NEW_USERNAME_TYPE && number != null) {
|
||||
this.nameView.setText(presentUsername(number));
|
||||
} else {
|
||||
this.nameView.setText(name);
|
||||
chipName = name;
|
||||
@@ -224,6 +239,14 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
int viewerCount = Integer.parseInt(number);
|
||||
return getContext().getResources().getQuantityString(R.plurals.contact_selection_list_item__number_of_viewers, viewerCount, viewerCount);
|
||||
}
|
||||
|
||||
private CharSequence presentUsername(@NonNull String username) {
|
||||
if (username.contains("#")) {
|
||||
return username;
|
||||
} else {
|
||||
return new SpannableStringBuilder(username).append(SpanUtil.color(ContextCompat.getColor(getContext(), R.color.signal_colorOutline), "#"));
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable LiveRecipient getRecipient() {
|
||||
return recipient;
|
||||
@@ -264,4 +287,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private static class UsernameFallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_search_24, R.drawable.ic_search_24, R.drawable.ic_search_24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||
}
|
||||
|
||||
private Cursor getUsernameSearchCursor() {
|
||||
return ContactsCursorRows.forUsernameSearch(getUnknownContactTitle(), getFilter());
|
||||
return ContactsCursorRows.forUsernameSearch(getFilter());
|
||||
}
|
||||
|
||||
private String getUnknownContactTitle() {
|
||||
|
||||
@@ -104,11 +104,11 @@ public final class ContactsCursorRows {
|
||||
/**
|
||||
* Create a row for a contacts cursor for a username the user is entering or has entered.
|
||||
*/
|
||||
public static @NonNull MatrixCursor forUsernameSearch(@NonNull String unknownContactTitle, @NonNull String filter) {
|
||||
public static @NonNull MatrixCursor forUsernameSearch(@NonNull String filter) {
|
||||
MatrixCursor matrixCursor = createMatrixCursor(1);
|
||||
|
||||
matrixCursor.addRow(new Object[]{null,
|
||||
unknownContactTitle,
|
||||
null,
|
||||
filter,
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
||||
"\u21e2",
|
||||
@@ -119,7 +119,7 @@ public final class ContactsCursorRows {
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_username_search));
|
||||
return forHeader(context.getString(R.string.ContactsCursorLoader_find_by_username));
|
||||
}
|
||||
|
||||
public static @NonNull MatrixCursor forPhoneNumberSearchHeader(@NonNull Context context) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -63,14 +63,18 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, @NonNull AvatarColor color, boolean inverted) {
|
||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color);
|
||||
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
AvatarColorPair avatarColorPair = AvatarColorPair.create(context, color);
|
||||
|
||||
final int backgroundColor = avatarColorPair.getBackgroundColor();
|
||||
final int foregroundColor = avatarColorPair.getForegroundColor();
|
||||
|
||||
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
|
||||
//noinspection ConstantConditions
|
||||
foreground.setScaleType(scaleType);
|
||||
background.setColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt(), PorterDuff.Mode.SRC_IN);
|
||||
foreground.setColorFilter(inverted ? color.colorInt() : foregroundColor.getColorInt(), PorterDuff.Mode.SRC_ATOP);
|
||||
background.setColorFilter(inverted ? foregroundColor : backgroundColor, PorterDuff.Mode.SRC_IN);
|
||||
foreground.setColorFilter(inverted ? backgroundColor : foregroundColor, PorterDuff.Mode.SRC_ATOP);
|
||||
|
||||
return new ExpandingLayerDrawable(new Drawable[] {background, foreground});
|
||||
}
|
||||
|
||||
@@ -195,8 +195,8 @@ object ContactSearchItems {
|
||||
|
||||
private fun presentPrivacyMode(privacyMode: DistributionListPrivacyMode): String {
|
||||
return when (privacyMode) {
|
||||
DistributionListPrivacyMode.ONLY_WITH -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__only_share_with)
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_signal_connections_except)
|
||||
DistributionListPrivacyMode.ONLY_WITH -> context.getString(R.string.ContactSearchItems__only_share_with)
|
||||
DistributionListPrivacyMode.ALL_EXCEPT -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_except)
|
||||
DistributionListPrivacyMode.ALL -> context.getString(R.string.ChooseInitialMyStoryMembershipFragment__all_signal_connections)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutionException
|
||||
@@ -80,7 +79,9 @@ object ContactDiscovery {
|
||||
descriptor = "refresh-all",
|
||||
refresh = {
|
||||
if (FeatureFlags.phoneNumberPrivacy()) {
|
||||
ContactDiscoveryRefreshV2.refreshAll(context)
|
||||
ContactDiscoveryRefreshV2.refreshAll(context, useCompat = false, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2Compat()) {
|
||||
ContactDiscoveryRefreshV2.refreshAll(context, useCompat = true, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2LoadTesting()) {
|
||||
loadTestRefreshAll(context)
|
||||
} else {
|
||||
@@ -103,7 +104,9 @@ object ContactDiscovery {
|
||||
descriptor = "refresh-multiple",
|
||||
refresh = {
|
||||
if (FeatureFlags.phoneNumberPrivacy()) {
|
||||
ContactDiscoveryRefreshV2.refresh(context, recipients)
|
||||
ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = false, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2Compat()) {
|
||||
ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = true, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2LoadTesting()) {
|
||||
loadTestRefresh(context, recipients)
|
||||
} else {
|
||||
@@ -124,7 +127,9 @@ object ContactDiscovery {
|
||||
descriptor = "refresh-single",
|
||||
refresh = {
|
||||
if (FeatureFlags.phoneNumberPrivacy()) {
|
||||
ContactDiscoveryRefreshV2.refresh(context, listOf(recipient))
|
||||
ContactDiscoveryRefreshV2.refresh(context, listOf(recipient), useCompat = false, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2Compat()) {
|
||||
ContactDiscoveryRefreshV2.refresh(context, listOf(recipient), useCompat = true, ignoreResults = false)
|
||||
} else if (FeatureFlags.cdsV2LoadTesting()) {
|
||||
loadTestRefresh(context, listOf(recipient))
|
||||
} else {
|
||||
@@ -381,14 +386,14 @@ object ContactDiscovery {
|
||||
private fun loadTestRefreshAll(context: Context): RefreshResult {
|
||||
return loadTestOperation(
|
||||
{ ContactDiscoveryRefreshV1.refreshAll(context) },
|
||||
{ ContactDiscoveryRefreshV2.refreshAll(context, ignoreResults = true) }
|
||||
{ ContactDiscoveryRefreshV2.refreshAll(context, useCompat = false, ignoreResults = true) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadTestRefresh(context: Context, recipients: List<Recipient>): RefreshResult {
|
||||
return loadTestOperation(
|
||||
{ ContactDiscoveryRefreshV1.refresh(context, recipients) },
|
||||
{ ContactDiscoveryRefreshV2.refresh(context, recipients, ignoreResults = true) }
|
||||
{ ContactDiscoveryRefreshV2.refresh(context, recipients, useCompat = false, ignoreResults = true) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -398,7 +403,7 @@ object ContactDiscovery {
|
||||
|
||||
try {
|
||||
v2Future.get()
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to complete the V2 fetch!", e)
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class ContactDiscoveryRefreshV1 {
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
recipientDatabase.rewritePhoneNumbers(result.getNumberRewrites());
|
||||
}
|
||||
|
||||
Map<RecipientId, ACI> aciMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
|
||||
@@ -250,8 +250,8 @@ class ContactDiscoveryRefreshV1 {
|
||||
KeyStore iasKeyStore = getIasKeyStore(context);
|
||||
|
||||
try {
|
||||
Map<String, ACI> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
|
||||
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult);
|
||||
Map<String, ACI> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
|
||||
FuzzyPhoneNumberHelper.OutputResult<ACI> outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult);
|
||||
|
||||
return new ContactIntersection(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
|
||||
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException | InvalidKeyException e) {
|
||||
|
||||
@@ -4,20 +4,26 @@ import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.contacts.SystemContactsRepository
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.InputResult
|
||||
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResult
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.CdsV2Result
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs.
|
||||
@@ -39,145 +45,202 @@ object ContactDiscoveryRefreshV2 {
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun refreshAll(context: Context, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult {
|
||||
val stopwatch = Stopwatch("refresh-all")
|
||||
|
||||
val previousE164s: Set<String> = if (SignalStore.misc().cdsToken != null) {
|
||||
SignalDatabase.cds.getAllE164s()
|
||||
} else {
|
||||
Log.w(TAG, "No token set! Cannot provide previousE164s.")
|
||||
emptySet()
|
||||
}
|
||||
stopwatch.split("previous")
|
||||
|
||||
fun refreshAll(context: Context, useCompat: Boolean, ignoreResults: Boolean): ContactDiscovery.RefreshResult {
|
||||
val recipientE164s: Set<String> = SignalDatabase.recipients.getAllE164s().sanitize()
|
||||
val newRecipientE164s: Set<String> = recipientE164s - previousE164s
|
||||
stopwatch.split("recipient")
|
||||
|
||||
val systemE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(context).toE164s(context).sanitize()
|
||||
val newSystemE164s: Set<String> = systemE164s - previousE164s
|
||||
stopwatch.split("system")
|
||||
|
||||
val newE164s: Set<String> = newRecipientE164s + newSystemE164s
|
||||
|
||||
val tokenToUse: ByteArray? = if (previousE164s.isNotEmpty()) {
|
||||
SignalStore.misc().cdsToken
|
||||
} else {
|
||||
if (SignalStore.misc().cdsToken != null) {
|
||||
Log.w(TAG, "We have a token, but our previousE164 list is empty! We cannot provide a token.")
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
val response: CdsiV2Service.Response = makeRequest(
|
||||
previousE164s = previousE164s,
|
||||
newE164s = newE164s,
|
||||
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||
token = tokenToUse,
|
||||
saveToken = true,
|
||||
tag = "refresh-all"
|
||||
return refreshInternal(
|
||||
recipientE164s = recipientE164s,
|
||||
systemE164s = systemE164s,
|
||||
inputPreviousE164s = SignalDatabase.cds.getAllE164s(),
|
||||
isPartialRefresh = false,
|
||||
useCompat = useCompat,
|
||||
ignoreResults = ignoreResults
|
||||
)
|
||||
stopwatch.split("network")
|
||||
|
||||
SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s)
|
||||
stopwatch.split("cds-db")
|
||||
|
||||
var registeredIds: Set<RecipientId> = emptySet()
|
||||
|
||||
if (ignoreResults) {
|
||||
Log.w(TAG, "[refresh-all] Ignoring CDSv2 results.")
|
||||
} else {
|
||||
registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
||||
response.results
|
||||
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
||||
)
|
||||
stopwatch.split("recipient-db")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
||||
stopwatch.split("update-registered")
|
||||
}
|
||||
|
||||
stopwatch.stop(TAG)
|
||||
Log.d(TAG, "[refresh-all] Used ${response.quotaUsedDebugOnly} units of our quota.")
|
||||
|
||||
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
@JvmStatic
|
||||
fun refresh(context: Context, inputRecipients: List<Recipient>, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult {
|
||||
val stopwatch = Stopwatch("refresh-some")
|
||||
|
||||
val recipients = inputRecipients.map { it.resolve() }
|
||||
stopwatch.split("resolve")
|
||||
|
||||
val inputIds: Set<RecipientId> = recipients.map { it.id }.toSet()
|
||||
fun refresh(context: Context, inputRecipients: List<Recipient>, useCompat: Boolean, ignoreResults: Boolean): ContactDiscovery.RefreshResult {
|
||||
val recipients: List<Recipient> = inputRecipients.map { it.resolve() }
|
||||
val inputE164s: Set<String> = recipients.mapNotNull { it.e164.orElse(null) }.toSet()
|
||||
|
||||
if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
|
||||
return if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
|
||||
Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.")
|
||||
val fullResult: ContactDiscovery.RefreshResult = refreshAll(context, ignoreResults)
|
||||
|
||||
return ContactDiscovery.RefreshResult(
|
||||
val fullResult: ContactDiscovery.RefreshResult = refreshAll(context, useCompat = useCompat, ignoreResults = ignoreResults)
|
||||
val inputIds: Set<RecipientId> = recipients.map { it.id }.toSet()
|
||||
|
||||
ContactDiscovery.RefreshResult(
|
||||
registeredIds = fullResult.registeredIds.intersect(inputIds),
|
||||
rewrites = fullResult.rewrites.filterKeys { inputE164s.contains(it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (inputE164s.isEmpty()) {
|
||||
Log.w(TAG, "No numbers to refresh!")
|
||||
return ContactDiscovery.RefreshResult(emptySet(), emptyMap())
|
||||
} else {
|
||||
Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.")
|
||||
}
|
||||
|
||||
val response: CdsiV2Service.Response = makeRequest(
|
||||
previousE164s = emptySet(),
|
||||
newE164s = inputE164s,
|
||||
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||
token = null,
|
||||
saveToken = false,
|
||||
tag = "refresh-some"
|
||||
)
|
||||
stopwatch.split("network")
|
||||
|
||||
var registeredIds: Set<RecipientId> = emptySet()
|
||||
|
||||
if (ignoreResults) {
|
||||
Log.w(TAG, "[refresh-some] Ignoring CDSv2 results.")
|
||||
} else {
|
||||
registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
||||
response.results
|
||||
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
||||
refreshInternal(
|
||||
recipientE164s = inputE164s,
|
||||
systemE164s = inputE164s,
|
||||
inputPreviousE164s = emptySet(),
|
||||
isPartialRefresh = true,
|
||||
useCompat = useCompat,
|
||||
ignoreResults = ignoreResults
|
||||
)
|
||||
stopwatch.split("recipient-db")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
||||
stopwatch.split("update-registered")
|
||||
}
|
||||
|
||||
Log.d(TAG, "[refresh-some] Used ${response.quotaUsedDebugOnly} units of our quota.")
|
||||
stopwatch.stop(TAG)
|
||||
|
||||
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun makeRequest(previousE164s: Set<String>, newE164s: Set<String>, serviceIds: Map<ServiceId, ProfileKey>, token: ByteArray?, saveToken: Boolean, tag: String): CdsiV2Service.Response {
|
||||
return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi(
|
||||
previousE164s,
|
||||
newE164s,
|
||||
serviceIds,
|
||||
Optional.ofNullable(token),
|
||||
BuildConfig.CDSI_MRENCLAVE
|
||||
) { tokenToSave ->
|
||||
if (saveToken) {
|
||||
SignalStore.misc().cdsToken = tokenToSave
|
||||
Log.d(TAG, "[$tag] Token saved!")
|
||||
private fun refreshInternal(
|
||||
recipientE164s: Set<String>,
|
||||
systemE164s: Set<String>,
|
||||
inputPreviousE164s: Set<String>,
|
||||
isPartialRefresh: Boolean,
|
||||
useCompat: Boolean,
|
||||
ignoreResults: Boolean
|
||||
): ContactDiscovery.RefreshResult {
|
||||
val tag = "refreshInternal-${if (useCompat) "compat" else "v2"}"
|
||||
val stopwatch = Stopwatch(tag)
|
||||
|
||||
val previousE164s: Set<String> = if (SignalStore.misc().cdsToken != null && !isPartialRefresh) inputPreviousE164s else emptySet()
|
||||
|
||||
val allE164s: Set<String> = recipientE164s + systemE164s
|
||||
val newRawE164s: Set<String> = allE164s - previousE164s
|
||||
val fuzzyInput: InputResult = FuzzyPhoneNumberHelper.generateInput(newRawE164s, recipientE164s)
|
||||
val newE164s: Set<String> = fuzzyInput.numbers
|
||||
|
||||
if (newE164s.isEmpty() && previousE164s.isEmpty()) {
|
||||
Log.w(TAG, "[$tag] No data to send! Ignoring.")
|
||||
return ContactDiscovery.RefreshResult(emptySet(), emptyMap())
|
||||
}
|
||||
|
||||
val token: ByteArray? = if (previousE164s.isNotEmpty() && !isPartialRefresh) SignalStore.misc().cdsToken else null
|
||||
|
||||
stopwatch.split("preamble")
|
||||
|
||||
val response: CdsiV2Service.Response = try {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi(
|
||||
previousE164s,
|
||||
newE164s,
|
||||
SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||
useCompat,
|
||||
Optional.ofNullable(token),
|
||||
BuildConfig.CDSI_MRENCLAVE
|
||||
) { tokenToSave ->
|
||||
stopwatch.split("network-pre-token")
|
||||
if (!isPartialRefresh) {
|
||||
SignalStore.misc().cdsToken = tokenToSave
|
||||
SignalDatabase.cds.updateAfterFullCdsQuery(previousE164s + newE164s, allE164s + newE164s)
|
||||
Log.d(TAG, "Token saved!")
|
||||
} else {
|
||||
SignalDatabase.cds.updateAfterPartialCdsQuery(newE164s)
|
||||
Log.d(TAG, "Ignoring token.")
|
||||
}
|
||||
stopwatch.split("cds-db")
|
||||
}
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
if (e.code == 4101) {
|
||||
Log.w(TAG, "Our token was invalid! Only thing we can do now is clear our local state :(")
|
||||
SignalStore.misc().cdsToken = null
|
||||
SignalDatabase.cds.clearAll()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
Log.d(TAG, "[$tag] Used ${response.quotaUsedDebugOnly} quota.")
|
||||
stopwatch.split("network-post-token")
|
||||
|
||||
val registeredIds: MutableSet<RecipientId> = mutableSetOf()
|
||||
val rewrites: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
if (ignoreResults) {
|
||||
Log.w(TAG, "[$tag] Ignoring CDSv2 results.")
|
||||
} else {
|
||||
if (useCompat) {
|
||||
val transformed: Map<String, ACI?> = response.results.mapValues { entry -> entry.value.aci.orElse(null) }
|
||||
val fuzzyOutput: OutputResult<ACI> = FuzzyPhoneNumberHelper.generateOutput(transformed, fuzzyInput)
|
||||
|
||||
if (transformed.values.any { it == null }) {
|
||||
throw IOException("Unexpected null ACI!")
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.rewritePhoneNumbers(fuzzyOutput.rewrites)
|
||||
stopwatch.split("rewrite-e164")
|
||||
|
||||
val aciMap: Map<RecipientId, ACI?> = SignalDatabase.recipients.bulkProcessCdsResult(fuzzyOutput.numbers)
|
||||
|
||||
registeredIds += aciMap.keys
|
||||
rewrites += fuzzyOutput.rewrites
|
||||
stopwatch.split("process-result")
|
||||
|
||||
val existingIds: Set<RecipientId> = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values)
|
||||
val inactiveIds: Set<RecipientId> = (existingIds - registeredIds).removeRegisteredButUnlisted()
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(aciMap, inactiveIds)
|
||||
stopwatch.split("update-registered")
|
||||
} else {
|
||||
val transformed: Map<String, CdsV2Result> = response.results.mapValues { entry -> CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
||||
val fuzzyOutput: OutputResult<CdsV2Result> = FuzzyPhoneNumberHelper.generateOutput(transformed, fuzzyInput)
|
||||
|
||||
SignalDatabase.recipients.rewritePhoneNumbers(fuzzyOutput.rewrites)
|
||||
stopwatch.split("rewrite-e164")
|
||||
|
||||
val existingIds: Set<RecipientId> = SignalDatabase.recipients.getAllPossiblyRegisteredByE164(recipientE164s + rewrites.values)
|
||||
val inactiveIds: Set<RecipientId> = (existingIds - registeredIds).removeRegisteredButUnlisted()
|
||||
|
||||
registeredIds += SignalDatabase.recipients.bulkProcessCdsV2Result(fuzzyOutput.numbers)
|
||||
rewrites += fuzzyOutput.rewrites
|
||||
stopwatch.split("process-result")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatusV2(registeredIds, inactiveIds)
|
||||
stopwatch.split("update-registered")
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.stop(TAG)
|
||||
|
||||
return ContactDiscovery.RefreshResult(registeredIds, rewrites)
|
||||
}
|
||||
|
||||
private fun hasCommunicatedWith(recipient: Recipient): Boolean {
|
||||
val localAci = SignalStore.account().requireAci()
|
||||
return SignalDatabase.threads.hasThread(recipient.id) || (recipient.hasServiceId() && SignalDatabase.sessions.hasSessionFor(localAci, recipient.requireServiceId().toString()))
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun Set<RecipientId>.removeRegisteredButUnlisted(): Set<RecipientId> {
|
||||
val futures: List<Future<Pair<RecipientId, Boolean?>>> = Recipient.resolvedList(this)
|
||||
.filter { it.hasServiceId() }
|
||||
.filter { hasCommunicatedWith(it) }
|
||||
.map {
|
||||
SignalExecutors.UNBOUNDED.submit(
|
||||
Callable {
|
||||
try {
|
||||
it.id to ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(it.requireServiceId())
|
||||
} catch (e: IOException) {
|
||||
it.id to null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val registeredIds: MutableSet<RecipientId> = mutableSetOf()
|
||||
val retryIds: MutableSet<RecipientId> = mutableSetOf()
|
||||
|
||||
for (future in futures) {
|
||||
val (id, registered) = future.get()
|
||||
if (registered == null) {
|
||||
retryIds += id
|
||||
registeredIds += id
|
||||
} else if (registered) {
|
||||
registeredIds += id
|
||||
}
|
||||
}
|
||||
|
||||
if (retryIds.isNotEmpty()) {
|
||||
Log.w(TAG, "Failed to determine registered status of ${retryIds.size} recipients. Assuming registered, but enqueuing profile jobs to check later.")
|
||||
RetrieveProfileJob.enqueue(retryIds)
|
||||
}
|
||||
|
||||
return this - registeredIds
|
||||
}
|
||||
|
||||
private fun Set<String>.toE164s(context: Context): Set<String> {
|
||||
|
||||
@@ -51,8 +51,8 @@ class FuzzyPhoneNumberHelper {
|
||||
* these results and our initial input set, we can decide if we need to rewrite which number we
|
||||
* have stored locally.
|
||||
*/
|
||||
static @NonNull OutputResult generateOutput(@NonNull Map<String, ACI> registeredNumbers, @NonNull InputResult inputResult) {
|
||||
Map<String, ACI> allNumbers = new HashMap<>(registeredNumbers);
|
||||
static @NonNull <E> OutputResult<E> generateOutput(@NonNull Map<String, E> registeredNumbers, @NonNull InputResult inputResult) {
|
||||
Map<String, E> allNumbers = new HashMap<>(registeredNumbers);
|
||||
Map<String, String> rewrites = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, String> entry : inputResult.getMapOfOriginalToVariant().entrySet()) {
|
||||
@@ -76,7 +76,7 @@ class FuzzyPhoneNumberHelper {
|
||||
}
|
||||
}
|
||||
|
||||
return new OutputResult(allNumbers, rewrites);
|
||||
return new OutputResult<>(allNumbers, rewrites);
|
||||
}
|
||||
|
||||
private interface FuzzyMatcher {
|
||||
@@ -170,16 +170,16 @@ class FuzzyPhoneNumberHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutputResult {
|
||||
private final Map<String, ACI> numbers;
|
||||
public static class OutputResult<E> {
|
||||
private final Map<String, E> numbers;
|
||||
private final Map<String, String> rewrites;
|
||||
|
||||
private OutputResult(@NonNull Map<String, ACI> numbers, @NonNull Map<String, String> rewrites) {
|
||||
private OutputResult(@NonNull Map<String, E> numbers, @NonNull Map<String, String> rewrites) {
|
||||
this.numbers = numbers;
|
||||
this.rewrites = rewrites;
|
||||
}
|
||||
|
||||
public @NonNull Map<String, ACI> getNumbers() {
|
||||
public @NonNull Map<String, E> getNumbers() {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.Conta
|
||||
field.setSelected(!field.isSelected());
|
||||
checkBox.setChecked(field.isSelected());
|
||||
});
|
||||
super.itemView.setOnClickListener(v -> checkBox.toggle());
|
||||
} else {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
itemView.setOnClickListener(null);
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -94,7 +94,7 @@ public class ContactShareEditActivity extends PassphraseRequiredActivity impleme
|
||||
|
||||
SharedContactRepository contactRepository = new SharedContactRepository(this, AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new Factory(contactUris, contactRepository)).get(ContactShareEditViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, new Factory(contactUris, contactRepository)).get(ContactShareEditViewModel.class);
|
||||
viewModel.getContacts().observe(this, contacts -> {
|
||||
contactAdapter.setContacts(contacts);
|
||||
contactList.post(() -> contactList.scrollToPosition(0));
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.conversation;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Activity which encapsulates a conversation for a Bubble window.
|
||||
*
|
||||
@@ -16,6 +18,12 @@ public class BubbleConversationActivity extends ConversationActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ViewUtil.hideKeyboard(this, getComposeText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeToolbar(@NonNull Toolbar toolbar) {
|
||||
}
|
||||
|
||||
@@ -1250,6 +1250,7 @@ public class ConversationParentFragment extends Fragment
|
||||
startActivity(StoryViewerActivity.createIntent(
|
||||
requireContext(),
|
||||
new StoryViewerArgs.Builder(recipient.getId(), recipient.get().shouldHideStory())
|
||||
.isFromQuote(true)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@@ -1444,9 +1445,9 @@ public class ConversationParentFragment extends Fragment
|
||||
if (recipient == null) return;
|
||||
|
||||
if (isSecure) {
|
||||
CommunicationActions.startVoiceCall(requireActivity(), recipient);
|
||||
CommunicationActions.startVoiceCall(this, recipient);
|
||||
} else {
|
||||
CommunicationActions.startInsecureCall(requireActivity(), recipient);
|
||||
CommunicationActions.startInsecureCall(this, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1459,7 +1460,7 @@ public class ConversationParentFragment extends Fragment
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.show();
|
||||
} else {
|
||||
CommunicationActions.startVideoCall(requireActivity(), recipient);
|
||||
CommunicationActions.startVideoCall(this, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3484,7 +3485,7 @@ public class ConversationParentFragment extends Fragment
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.with(requireActivity())
|
||||
Permissions.with(ConversationParentFragment.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24)
|
||||
|
||||
@@ -23,9 +23,9 @@ public enum AvatarColor {
|
||||
A180("A180", 0xFFFEF5D0),
|
||||
A190("A190", 0xFFEAE6D5),
|
||||
A200("A200", 0xFFD2D2DC),
|
||||
A210("A210", 0xFFD7D7D9);
|
||||
|
||||
public static final AvatarColor UNKNOWN = A210;
|
||||
A210("A210", 0xFFD7D7D9),
|
||||
UNKNOWN("UNKNOWN", 0x00000000),
|
||||
ON_SURFACE_VARIANT("ON_SURFACE_VARIANT", 0x00000000);
|
||||
|
||||
/** Fast map of name to enum, while also giving us a location to map old colors to new ones. */
|
||||
private static final Map<String, AvatarColor> NAME_MAP = new HashMap<>();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
|
||||
class AvatarColorPair private constructor(
|
||||
@ColorInt val foregroundColor: Int,
|
||||
@ColorInt val backgroundColor: Int
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(context: Context, avatarColor: AvatarColor): AvatarColorPair {
|
||||
return when (avatarColor) {
|
||||
AvatarColor.UNKNOWN -> AvatarColorPair(
|
||||
foregroundColor = ContextCompat.getColor(context, R.color.signal_colorOnSurface),
|
||||
backgroundColor = ContextCompat.getColor(context, R.color.signal_colorSurfaceVariant)
|
||||
)
|
||||
AvatarColor.ON_SURFACE_VARIANT -> AvatarColorPair(
|
||||
foregroundColor = ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant),
|
||||
backgroundColor = ContextCompat.getColor(context, R.color.signal_colorSurfaceVariant)
|
||||
)
|
||||
else -> AvatarColorPair(
|
||||
foregroundColor = Avatars.getForegroundColor(avatarColor).colorInt,
|
||||
backgroundColor = avatarColor.colorInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +253,9 @@ class MultiselectForwardFragment :
|
||||
val expiringMessages = args.multiShareArgs.filter { it.expiresAt > 0L }
|
||||
val firstToExpire = expiringMessages.minByOrNull { it.expiresAt }
|
||||
val earliestExpiration = firstToExpire?.expiresAt ?: -1L
|
||||
|
||||
if (viewModel.state.value?.stage is MultiselectForwardState.Stage.SelectionConfirmed && contactSearchMediator.getSelectedContacts().isNotEmpty()) {
|
||||
onCanceled()
|
||||
}
|
||||
if (earliestExpiration > 0) {
|
||||
if (earliestExpiration <= now) {
|
||||
handleMessageExpired()
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(MentionsPickerViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(MentionsPickerViewModel.class);
|
||||
|
||||
initializeList();
|
||||
|
||||
|
||||
@@ -89,7 +89,9 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (getConversationResult(position) != null) {
|
||||
if (position < 0) {
|
||||
return StickyHeaderDecoration.StickyHeaderAdapter.NO_HEADER_ID;
|
||||
} else if (getConversationResult(position) != null) {
|
||||
return TYPE_CONVERSATIONS;
|
||||
} else if (getContactResult(position) != null) {
|
||||
return TYPE_CONTACTS;
|
||||
|
||||
@@ -126,10 +126,9 @@ public class SignalBaseIdentityKeyStore {
|
||||
}
|
||||
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, IdentityKeyStore.Direction direction) {
|
||||
Recipient self = Recipient.self();
|
||||
|
||||
boolean isSelf = address.getName().equals(self.requireServiceId().toString()) ||
|
||||
address.getName().equals(self.requireE164());
|
||||
boolean isSelf = address.getName().equals(SignalStore.account().requireAci().toString()) ||
|
||||
address.getName().equals(SignalStore.account().requirePni().toString()) ||
|
||||
address.getName().equals(SignalStore.account().getE164());
|
||||
|
||||
if (isSelf) {
|
||||
return identityKey.equals(SignalStore.account().getAciIdentityKey().getPublicKey());
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.signal.core.util.delete
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
|
||||
/**
|
||||
* Keeps track of the numbers we've previously queried CDS for.
|
||||
@@ -53,32 +53,58 @@ class CdsDatabase(context: Context, databaseHelper: SignalDatabase) : Database(c
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newE164s The newly-added E164s that we hadn't previously queried for.
|
||||
* @param seenE164s The E164s that were seen in either the system contacts or recipients table.
|
||||
* This should be a superset of [newE164s]
|
||||
*
|
||||
* Saves the set of e164s used after a full refresh.
|
||||
* @param fullE164s All of the e164s used in the last CDS query (previous and new).
|
||||
* @param seenE164s The E164s that were seen in either the system contacts or recipients table. This is different from [fullE164s] in that [fullE164s]
|
||||
* includes every number we've ever seen, even if it's not in our contacts anymore.
|
||||
*/
|
||||
fun updateAfterCdsQuery(newE164s: Set<String>, seenE164s: Set<String>) {
|
||||
fun updateAfterFullCdsQuery(fullE164s: Set<String>, seenE164s: Set<String>) {
|
||||
val lastSeen = System.currentTimeMillis()
|
||||
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
val insertValues: List<ContentValues> = newE164s.map { contentValuesOf(E164 to it) }
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val existingE164s: Set<String> = getAllE164s()
|
||||
val removedE164s: Set<String> = existingE164s - fullE164s
|
||||
val addedE164s: Set<String> = fullE164s - existingE164s
|
||||
|
||||
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(E164), insertValues)
|
||||
.forEach { writableDatabase.execSQL(it.where, it.whereArgs) }
|
||||
if (removedE164s.isNotEmpty()) {
|
||||
SqlUtil.buildCollectionQuery(E164, removedE164s)
|
||||
.forEach { db.delete(TABLE_NAME, it.where, it.whereArgs) }
|
||||
}
|
||||
|
||||
val contentValues = contentValuesOf(LAST_SEEN_AT to lastSeen)
|
||||
if (addedE164s.isNotEmpty()) {
|
||||
val insertValues: List<ContentValues> = addedE164s.map { contentValuesOf(E164 to it) }
|
||||
|
||||
SqlUtil.buildCollectionQuery(E164, seenE164s)
|
||||
.forEach { query -> writableDatabase.update(TABLE_NAME, contentValues, query.where, query.whereArgs) }
|
||||
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(E164), insertValues)
|
||||
.forEach { db.execSQL(it.where, it.whereArgs) }
|
||||
}
|
||||
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
writableDatabase.endTransaction()
|
||||
if (seenE164s.isNotEmpty()) {
|
||||
val contentValues = contentValuesOf(LAST_SEEN_AT to lastSeen)
|
||||
|
||||
SqlUtil.buildCollectionQuery(E164, seenE164s)
|
||||
.forEach { query -> db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates after a partial CDS query. Will not insert new entries. Instead, this will simply update the lastSeen timestamp of any entry we already have.
|
||||
* @param seenE164s The newly-added E164s that we hadn't previously queried for.
|
||||
*/
|
||||
fun updateAfterPartialCdsQuery(seenE164s: Set<String>) {
|
||||
val lastSeen = System.currentTimeMillis()
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val contentValues = contentValuesOf(LAST_SEEN_AT to lastSeen)
|
||||
|
||||
SqlUtil.buildCollectionQuery(E164, seenE164s)
|
||||
.forEach { query -> db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes the entire table.
|
||||
*/
|
||||
fun clearAll() {
|
||||
writableDatabase
|
||||
.delete(TABLE_NAME)
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryResult;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
@@ -94,6 +95,9 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract boolean isSent(long messageId);
|
||||
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
|
||||
public abstract Set<Long> getAllRateLimitedMessageIds();
|
||||
public abstract Cursor getUnexportedInsecureMessages();
|
||||
public abstract int getInsecureMessageCount();
|
||||
public abstract void deleteExportedMessages();
|
||||
|
||||
public abstract void markExpireStarted(long messageId);
|
||||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
@@ -353,6 +357,14 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
return String.format(Locale.ENGLISH, "(%s OR %s) AND %s", isSent, isReceived, isSecure);
|
||||
}
|
||||
|
||||
protected String getInsecureMessageClause() {
|
||||
String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE;
|
||||
String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE;
|
||||
String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
||||
|
||||
return String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s", isSent, isReceived, isSecure);
|
||||
}
|
||||
|
||||
public void setReactionsSeen(long threadId, long sinceTimestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
@@ -803,6 +815,11 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
@Deprecated
|
||||
MessageRecord getCurrent();
|
||||
|
||||
/**
|
||||
* Pulls the export state out of the query, if it is present.
|
||||
*/
|
||||
@NonNull MessageExportState getMessageExportStateForCurrentRecord();
|
||||
|
||||
/**
|
||||
* From the {@link Closeable} interface, removing the IOException requirement.
|
||||
*/
|
||||
|
||||
@@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.database.model.StoryType;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
@@ -189,7 +190,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " +
|
||||
MESSAGE_RANGES + " BLOB DEFAULT NULL, " +
|
||||
STORY_TYPE + " INTEGER DEFAULT 0, " +
|
||||
PARENT_STORY_ID + " INTEGER DEFAULT 0);";
|
||||
PARENT_STORY_ID + " INTEGER DEFAULT 0, " +
|
||||
EXPORT_STATE + " BLOB DEFAULT NULL, " +
|
||||
EXPORTED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");",
|
||||
@@ -1216,8 +1219,12 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) {
|
||||
return rawQuery(MMS_PROJECTION, where, arguments, reverse, limit);
|
||||
}
|
||||
|
||||
private Cursor rawQuery(@NonNull String[] projection, @NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
String rawQueryString = "SELECT " + Util.join(MMS_PROJECTION, ",") +
|
||||
String rawQueryString = "SELECT " + Util.join(projection, ",") +
|
||||
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
|
||||
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" +
|
||||
" WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID;
|
||||
@@ -2416,6 +2423,53 @@ public class MmsDatabase extends MessageDatabase {
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getUnexportedInsecureMessages() {
|
||||
return rawQuery(
|
||||
SqlUtil.appendArg(MMS_PROJECTION, EXPORT_STATE),
|
||||
getInsecureMessageClause() + " AND NOT " + EXPORTED,
|
||||
null,
|
||||
false,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInsecureMessageCount() {
|
||||
try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteExportedMessages() {
|
||||
beginTransaction();
|
||||
try {
|
||||
List<Long> threadsToUpdate = new LinkedList<>();
|
||||
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, EXPORTED + " = ?", SqlUtil.buildArgs(1), THREAD_ID, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
threadsToUpdate.add(CursorUtil.requireLong(cursor, THREAD_ID));
|
||||
}
|
||||
}
|
||||
|
||||
getWritableDatabase().delete(TABLE_NAME, EXPORTED + " = ?", SqlUtil.buildArgs(1));
|
||||
|
||||
for (final long threadId : threadsToUpdate) {
|
||||
SignalDatabase.threads().update(threadId, false);
|
||||
}
|
||||
|
||||
SignalDatabase.attachments().deleteAbandonedAttachmentFiles();
|
||||
|
||||
setTransactionSuccessful();
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void deleteThreads(@NonNull Set<Long> threadIds) {
|
||||
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
|
||||
@@ -2664,6 +2718,25 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageExportState getMessageExportStateForCurrentRecord() {
|
||||
byte[] messageExportState = CursorUtil.requireBlob(cursor, MmsDatabase.EXPORT_STATE);
|
||||
if (messageExportState == null) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
}
|
||||
|
||||
try {
|
||||
return MessageExportState.parseFrom(messageExportState);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
}
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
if (cursor == null) return 0;
|
||||
else return cursor.getCount();
|
||||
}
|
||||
|
||||
private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
|
||||
long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT));
|
||||
|
||||
@@ -29,6 +29,8 @@ public interface MmsSmsColumns {
|
||||
public static final String REMOTE_DELETED = "remote_deleted";
|
||||
public static final String SERVER_GUID = "server_guid";
|
||||
public static final String RECEIPT_TIMESTAMP = "receipt_timestamp";
|
||||
public static final String EXPORT_STATE = "export_state";
|
||||
public static final String EXPORTED = "exported";
|
||||
|
||||
/**
|
||||
* For storage efficiency, all types are stored within a single 64-bit integer column in the
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
@@ -24,21 +25,23 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteQueryBuilder;
|
||||
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MessageUpdate;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -50,6 +53,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS;
|
||||
@@ -613,6 +617,63 @@ public class MmsSmsDatabase extends Database {
|
||||
SignalDatabase.mms().updateViewedStories(syncMessageIds);
|
||||
}
|
||||
|
||||
private @NonNull MessageExportState getMessageExportState(@NonNull MessageId messageId) throws NoSuchMessageException {
|
||||
String table = messageId.isMms() ? MmsDatabase.TABLE_NAME : SmsDatabase.TABLE_NAME;
|
||||
String[] projection = SqlUtil.buildArgs(MmsSmsColumns.EXPORT_STATE);
|
||||
String[] args = SqlUtil.buildArgs(messageId.getId());
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().query(table, projection, ID_WHERE, args, null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
byte[] bytes = CursorUtil.requireBlob(cursor, MmsSmsColumns.EXPORT_STATE);
|
||||
if (bytes == null) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
} else {
|
||||
try {
|
||||
return MessageExportState.parseFrom(bytes);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new NoSuchMessageException("The requested message does not exist.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMessageExportState(@NonNull MessageId messageId, @NonNull Function<MessageExportState, MessageExportState> transform) throws NoSuchMessageException {
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
|
||||
database.beginTransaction();
|
||||
try {
|
||||
MessageExportState oldState = getMessageExportState(messageId);
|
||||
MessageExportState newState = transform.apply(oldState);
|
||||
|
||||
setMessageExportState(messageId, newState);
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void markMessageExported(@NonNull MessageId messageId) {
|
||||
String table = messageId.isMms() ? MmsDatabase.TABLE_NAME : SmsDatabase.TABLE_NAME;
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
||||
contentValues.put(MmsSmsColumns.EXPORTED, 1);
|
||||
|
||||
getWritableDatabase().update(table, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId.getId()));
|
||||
}
|
||||
|
||||
private void setMessageExportState(@NonNull MessageId messageId, @NonNull MessageExportState messageExportState) {
|
||||
String table = messageId.isMms() ? MmsDatabase.TABLE_NAME : SmsDatabase.TABLE_NAME;
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
|
||||
contentValues.put(MmsSmsColumns.EXPORT_STATE, messageExportState.toByteArray());
|
||||
|
||||
getWritableDatabase().update(table, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Unhandled ids
|
||||
*/
|
||||
|
||||
@@ -1141,22 +1141,22 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
Recipient.self().live().refresh()
|
||||
}
|
||||
|
||||
fun updatePhoneNumbers(mapping: Map<String?, String?>) {
|
||||
/**
|
||||
* Takes a mapping of old->new phone numbers and updates the table to match.
|
||||
* Intended to be used to handle changing number formats.
|
||||
*/
|
||||
fun rewritePhoneNumbers(mapping: Map<String, String>) {
|
||||
if (mapping.isEmpty()) return
|
||||
val db = writableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val query = "$PHONE = ?"
|
||||
for ((key, value) in mapping) {
|
||||
val values = ContentValues().apply {
|
||||
put(PHONE, value)
|
||||
}
|
||||
db.updateWithOnConflict(TABLE_NAME, values, query, arrayOf(key), SQLiteDatabase.CONFLICT_IGNORE)
|
||||
Log.i(TAG, "Rewriting ${mapping.size} phone numbers.")
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
for ((originalE164, updatedE164) in mapping) {
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(PHONE to updatedE164)
|
||||
.where("$PHONE = ?", originalE164)
|
||||
.run(SQLiteDatabase.CONFLICT_IGNORE)
|
||||
}
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1607,6 +1607,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGiftBadges).serialize().toLong())
|
||||
value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong())
|
||||
|
||||
val values = ContentValues(1).apply {
|
||||
put(CAPABILITIES, value)
|
||||
@@ -2046,16 +2047,23 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the provided IDs together. The assumption here is that all of the IDs correspond to the local user and have been verified.
|
||||
*/
|
||||
fun linkIdsForSelf(aci: ACI, pni: PNI, e164: String) {
|
||||
getAndPossiblyMergePnp(serviceId = aci, pni = pni, e164 = e164, changeSelf = true, pniVerified = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Does *not* handle clearing the recipient cache. It is assumed the caller handles this.
|
||||
*/
|
||||
fun updateSelfPhone(e164: String) {
|
||||
fun updateSelfPhone(e164: String, pni: PNI) {
|
||||
val db = writableDatabase
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
val id = Recipient.self().id
|
||||
val newId = getAndPossiblyMerge(SignalStore.account().requireAci(), e164, changeSelf = true)
|
||||
val newId = getAndPossiblyMergePnp(serviceId = SignalStore.account().requireAci(), pni = pni, e164 = e164, pniVerified = true, changeSelf = true)
|
||||
|
||||
if (id == newId) {
|
||||
Log.i(TAG, "[updateSelfPhone] Phone updated for self")
|
||||
@@ -2068,7 +2076,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.values(NEEDS_PNI_SIGNATURE to 0)
|
||||
.run()
|
||||
|
||||
SignalDatabase.pendingPniSignatureMessages.deleteAll()
|
||||
pendingPniSignatureMessages.deleteAll()
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
@@ -2123,6 +2131,27 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives you all of the recipientIds of possibly-registered users (i.e. REGISTERED or UNKNOWN) that can be found by the set of
|
||||
* provided E164s.
|
||||
*/
|
||||
fun getAllPossiblyRegisteredByE164(e164s: Set<String>): Set<RecipientId> {
|
||||
val results: MutableSet<RecipientId> = mutableSetOf()
|
||||
val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(PHONE, e164s)
|
||||
|
||||
for (query in queries) {
|
||||
readableDatabase.query(TABLE_NAME, arrayOf(ID, REGISTERED), query.where, query.whereArgs, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
if (RegisteredState.fromId(cursor.requireInt(REGISTERED)) != RegisteredState.NOT_REGISTERED) {
|
||||
results += RecipientId.from(cursor.requireLong(ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun setPni(id: RecipientId, pni: PNI) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
@@ -2288,6 +2317,32 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
return ids
|
||||
}
|
||||
|
||||
fun bulkUpdatedRegisteredStatusV2(registered: Set<RecipientId>, unregistered: Collection<RecipientId>) {
|
||||
writableDatabase.withinTransaction {
|
||||
val registeredValues = contentValuesOf(
|
||||
REGISTERED to RegisteredState.REGISTERED.id
|
||||
)
|
||||
|
||||
for (id in registered) {
|
||||
if (update(id, registeredValues)) {
|
||||
setStorageIdIfNotSet(id)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
|
||||
val unregisteredValues = contentValuesOf(
|
||||
REGISTERED to RegisteredState.NOT_REGISTERED.id,
|
||||
STORAGE_SERVICE_ID to null
|
||||
)
|
||||
|
||||
for (id in unregistered) {
|
||||
if (update(id, unregisteredValues)) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a tuple of (e164, pni, aci) and incorporates it into our database.
|
||||
* It is assumed that we are in a transaction.
|
||||
@@ -2295,7 +2350,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
* @return The [RecipientId] of the resulting recipient.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false, pnpEnabled: Boolean = FeatureFlags.phoneNumberPrivacy()): ProcessPnpTupleResult {
|
||||
fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): ProcessPnpTupleResult {
|
||||
val changeSet: PnpChangeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf)
|
||||
|
||||
val affectedIds: MutableSet<RecipientId> = mutableSetOf()
|
||||
@@ -2321,7 +2376,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pnpEnabled, pni)
|
||||
val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pni)
|
||||
|
||||
return ProcessPnpTupleResult(
|
||||
finalId = finalId,
|
||||
@@ -2334,7 +2389,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, pnpEnabled: Boolean, inputPni: PNI?): RecipientId {
|
||||
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?): RecipientId {
|
||||
for (operation in changeSet.operations) {
|
||||
@Exhaustive
|
||||
when (operation) {
|
||||
@@ -2392,18 +2447,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.run()
|
||||
}
|
||||
is PnpOperation.Merge -> {
|
||||
val primary = getRecord(operation.primaryId)
|
||||
val secondary = getRecord(operation.secondaryId)
|
||||
|
||||
if (primary.serviceId != null && !primary.sidIsPni() && secondary.e164 != null) {
|
||||
merge(operation.primaryId, operation.secondaryId, inputPni)
|
||||
} else {
|
||||
if (!pnpEnabled) {
|
||||
throw AssertionError("This type of merge is not supported in production!")
|
||||
}
|
||||
|
||||
merge(operation.primaryId, operation.secondaryId, inputPni)
|
||||
}
|
||||
merge(operation.primaryId, operation.secondaryId, inputPni)
|
||||
}
|
||||
is PnpOperation.SessionSwitchoverInsert -> {
|
||||
// TODO [pnp]
|
||||
@@ -3674,7 +3718,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
|
||||
* Should only be used for debugging! A very destructive action that clears all known serviceIds from people with phone numbers (so that we could eventually
|
||||
* get them back through CDS).
|
||||
*/
|
||||
fun debugClearServiceIds(recipientId: RecipientId? = null) {
|
||||
writableDatabase
|
||||
@@ -3685,9 +3730,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
)
|
||||
.run {
|
||||
if (recipientId == null) {
|
||||
where("$ID != ?", Recipient.self().id)
|
||||
where("$ID != ? AND $PHONE NOT NULL", Recipient.self().id)
|
||||
} else {
|
||||
where("$ID = ?", recipientId)
|
||||
where("$ID = ? AND $PHONE NOT NULL", recipientId)
|
||||
}
|
||||
}
|
||||
.run()
|
||||
@@ -3785,6 +3830,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
val recipientId = RecipientId.from(cursor.requireLong(idColumnName))
|
||||
val capabilities = cursor.requireLong(CAPABILITIES)
|
||||
val distributionListId: DistributionListId? = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID))
|
||||
val avatarColor: AvatarColor = if (distributionListId != null) AvatarColor.UNKNOWN else AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR))
|
||||
|
||||
return RecipientRecord(
|
||||
id = recipientId,
|
||||
@@ -3794,7 +3841,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
e164 = cursor.requireString(PHONE),
|
||||
email = cursor.requireString(EMAIL),
|
||||
groupId = GroupId.parseNullableOrThrow(cursor.requireString(GROUP_ID)),
|
||||
distributionListId = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID)),
|
||||
distributionListId = distributionListId,
|
||||
groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE)),
|
||||
isBlocked = cursor.requireBoolean(BLOCKED),
|
||||
muteUntil = cursor.requireLong(MUTE_UNTIL),
|
||||
@@ -3827,12 +3874,13 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()),
|
||||
storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()),
|
||||
giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()),
|
||||
pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()),
|
||||
insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)),
|
||||
storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)),
|
||||
mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)),
|
||||
wallpaper = chatWallpaper,
|
||||
chatColors = chatColors,
|
||||
avatarColor = AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR)),
|
||||
avatarColor = avatarColor,
|
||||
about = cursor.requireString(ABOUT),
|
||||
aboutEmoji = cursor.requireString(ABOUT_EMOJI),
|
||||
syncExtras = getSyncExtras(cursor),
|
||||
@@ -4146,6 +4194,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
const val CHANGE_NUMBER = 4
|
||||
const val STORIES = 5
|
||||
const val GIFT_BADGES = 6
|
||||
const val PNP = 7
|
||||
}
|
||||
|
||||
enum class VibrateState(val id: Int) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -221,6 +222,12 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@VisibleForTesting
|
||||
fun setSignalDatabaseInstanceForTesting(signalDatabase: SignalDatabase) {
|
||||
this.instance = signalDatabase
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val rawDatabase: net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
get() = instance!!.rawWritableDatabase
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
@@ -47,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryResult;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
@@ -66,15 +68,16 @@ import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -131,7 +134,9 @@ public class SmsDatabase extends MessageDatabase {
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
|
||||
NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
SERVER_GUID + " TEXT DEFAULT NULL, " +
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1);";
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " +
|
||||
EXPORT_STATE + " BLOB DEFAULT NULL, " +
|
||||
EXPORTED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS sms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");",
|
||||
@@ -901,6 +906,51 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getUnexportedInsecureMessages() {
|
||||
return queryMessages(
|
||||
SqlUtil.appendArg(MESSAGE_PROJECTION, EXPORT_STATE),
|
||||
getInsecureMessageClause() + " AND NOT " + EXPORTED,
|
||||
null,
|
||||
false,
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInsecureMessageCount() {
|
||||
try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteExportedMessages() {
|
||||
beginTransaction();
|
||||
try {
|
||||
List<Long> threadsToUpdate = new LinkedList<>();
|
||||
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, EXPORTED + " = ?", SqlUtil.buildArgs(1), THREAD_ID, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
threadsToUpdate.add(CursorUtil.requireLong(cursor, THREAD_ID));
|
||||
}
|
||||
}
|
||||
|
||||
getWritableDatabase().delete(TABLE_NAME, EXPORTED + " = ?", SqlUtil.buildArgs(1));
|
||||
|
||||
for (final long threadId : threadsToUpdate) {
|
||||
SignalDatabase.threads().update(threadId, false);
|
||||
}
|
||||
|
||||
setTransactionSuccessful();
|
||||
} finally {
|
||||
endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
|
||||
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?";
|
||||
@@ -1541,11 +1591,15 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
private Cursor queryMessages(@NonNull String where, @NonNull String[] args, boolean reverse, long limit) {
|
||||
private Cursor queryMessages(@NonNull String where, @Nullable String[] args, boolean reverse, long limit) {
|
||||
return queryMessages(MESSAGE_PROJECTION, where, args, reverse, limit);
|
||||
}
|
||||
|
||||
private Cursor queryMessages(@NonNull String[] projection, @NonNull String where, @Nullable String[] args, boolean reverse, long limit) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
|
||||
return db.query(TABLE_NAME,
|
||||
MESSAGE_PROJECTION,
|
||||
projection,
|
||||
where,
|
||||
args,
|
||||
null,
|
||||
@@ -1776,7 +1830,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
public static class Reader implements MessageDatabase.Reader {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final Context context;
|
||||
@@ -1798,6 +1852,20 @@ public class SmsDatabase extends MessageDatabase {
|
||||
else return cursor.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageExportState getMessageExportStateForCurrentRecord() {
|
||||
byte[] messageExportState = CursorUtil.requireBlob(cursor, SmsDatabase.EXPORT_STATE);
|
||||
if (messageExportState == null) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
}
|
||||
|
||||
try {
|
||||
return MessageExportState.parseFrom(messageExportState);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return MessageExportState.getDefaultInstance();
|
||||
}
|
||||
}
|
||||
|
||||
public SmsMessageRecord getCurrent() {
|
||||
long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.RECIPIENT_ID));
|
||||
@@ -1853,6 +1921,28 @@ public class SmsDatabase extends MessageDatabase {
|
||||
public void close() {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Iterator<MessageRecord> iterator() {
|
||||
return new ReaderIterator();
|
||||
}
|
||||
|
||||
private class ReaderIterator implements Iterator<MessageRecord> {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return cursor != null && cursor.getCount() != 0 && !cursor.isLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord next() {
|
||||
MessageRecord record = getNext();
|
||||
if (record == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +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.
|
||||
*/
|
||||
object UrgentMslFlagMigration : SignalDatabaseMigration {
|
||||
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")
|
||||
}
|
||||
@@ -13,9 +13,9 @@ import org.thoughtcrime.securesms.util.Base64
|
||||
/**
|
||||
* Performs a check and ensures that MyStory exists at the correct distribution list id and correct distribution id.
|
||||
*/
|
||||
object MyStoryMigration : SignalDatabaseMigration {
|
||||
object V151_MyStoryMigration : SignalDatabaseMigration {
|
||||
|
||||
private val TAG = Log.tag(MyStoryMigration::class.java)
|
||||
private val TAG = Log.tag(V151_MyStoryMigration::class.java)
|
||||
|
||||
private const val TABLE_NAME = "distribution_list"
|
||||
private const val NAME = "name"
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Marks story recipients with a new group type constant.
|
||||
*/
|
||||
object V152_StoryGroupTypesMigration : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
UPDATE recipient
|
||||
SET group_type = 4
|
||||
WHERE distribution_list_id IS NOT NULL
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
/**
|
||||
* Performs a check and ensures that MyStory exists at the correct distribution list id and correct distribution id.
|
||||
*/
|
||||
object V153_MyStoryMigration : SignalDatabaseMigration {
|
||||
|
||||
private val TAG = Log.tag(V153_MyStoryMigration::class.java)
|
||||
|
||||
private const val TABLE_NAME = "distribution_list"
|
||||
private const val NAME = "name"
|
||||
private const val DISTRIBUTION_LIST_ID = "_id"
|
||||
private const val DISTRIBUTION_ID = "distribution_id"
|
||||
private const val RECIPIENT_ID = "recipient_id"
|
||||
private const val PRIVACY_MODE = "privacy_mode"
|
||||
private const val MY_STORY_DISTRIBUTION_LIST_ID = 1
|
||||
private const val MY_STORY_DISTRIBUTION_ID = "00000000-0000-0000-0000-000000000000"
|
||||
private const val MY_STORY_PRIVACY_MODE = 2
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
val result: MyStoryExistsResult = getMyStoryCursor(db).use { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
val distributionId = CursorUtil.requireString(cursor, DISTRIBUTION_ID)
|
||||
if (distributionId != MY_STORY_DISTRIBUTION_ID) {
|
||||
Log.d(TAG, "[migrate] Invalid MyStory DistributionId: $distributionId")
|
||||
MyStoryExistsResult.REQUIRES_DISTRIBUTION_ID_UPDATE
|
||||
} else {
|
||||
Log.d(TAG, "[migrate] MyStory DistributionId matches expected value.")
|
||||
MyStoryExistsResult.MATCHES_EXPECTED_VALUE
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "[migrate] My Story does not exist.")
|
||||
MyStoryExistsResult.DOES_NOT_EXIST
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
MyStoryExistsResult.REQUIRES_DISTRIBUTION_ID_UPDATE -> updateDistributionIdToExpectedValue(db)
|
||||
MyStoryExistsResult.MATCHES_EXPECTED_VALUE -> Unit
|
||||
MyStoryExistsResult.DOES_NOT_EXIST -> createMyStory(db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDistributionIdToExpectedValue(db: SQLiteDatabase) {
|
||||
Log.d(TAG, "[updateDistributionIdToExpectedValue] Overwriting My Story DistributionId with expected value.")
|
||||
db.update(
|
||||
TABLE_NAME,
|
||||
contentValuesOf(DISTRIBUTION_ID to MY_STORY_DISTRIBUTION_ID),
|
||||
"$DISTRIBUTION_LIST_ID = ?",
|
||||
arrayOf(MY_STORY_DISTRIBUTION_LIST_ID.toString())
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMyStory(db: SQLiteDatabase) {
|
||||
Log.d(TAG, "[createMyStory] Attempting to create My Story.")
|
||||
|
||||
val recipientId: Long = getMyStoryRecipientId(db) ?: createMyStoryRecipientId(db)
|
||||
|
||||
db.insert(
|
||||
TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
DISTRIBUTION_LIST_ID to MY_STORY_DISTRIBUTION_LIST_ID,
|
||||
NAME to MY_STORY_DISTRIBUTION_ID,
|
||||
DISTRIBUTION_ID to MY_STORY_DISTRIBUTION_ID,
|
||||
RECIPIENT_ID to recipientId,
|
||||
PRIVACY_MODE to MY_STORY_PRIVACY_MODE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMyStoryRecipientId(db: SQLiteDatabase): Long {
|
||||
return db.insert(
|
||||
"recipient",
|
||||
null,
|
||||
contentValuesOf(
|
||||
"group_type" to 4,
|
||||
"distribution_list_id" to MY_STORY_DISTRIBUTION_LIST_ID,
|
||||
"storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()),
|
||||
"profile_sharing" to 1
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMyStoryRecipientId(db: SQLiteDatabase): Long? {
|
||||
return db.query(
|
||||
"recipient",
|
||||
arrayOf("_id"),
|
||||
"distribution_list_id = ?",
|
||||
SqlUtil.buildArgs(MY_STORY_DISTRIBUTION_LIST_ID),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use {
|
||||
if (it.moveToNext()) {
|
||||
CursorUtil.requireLong(it, "_id")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMyStoryCursor(db: SQLiteDatabase): Cursor {
|
||||
return db.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(DISTRIBUTION_ID),
|
||||
"$DISTRIBUTION_LIST_ID = ?",
|
||||
arrayOf(MY_STORY_DISTRIBUTION_LIST_ID.toString()),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private enum class MyStoryExistsResult {
|
||||
REQUIRES_DISTRIBUTION_ID_UPDATE,
|
||||
MATCHES_EXPECTED_VALUE,
|
||||
DOES_NOT_EXIST
|
||||
}
|
||||
}
|
||||
@@ -6,7 +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.
|
||||
*/
|
||||
object PniSignaturesMigration : SignalDatabaseMigration {
|
||||
object V154_PniSignaturesMigration : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN needs_pni_signature")
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds necessary book-keeping columns to SMS and MMS tables for SMS export.
|
||||
*/
|
||||
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")
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN exported INTEGER DEFAULT 0")
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN export_state BLOB DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN exported INTEGER DEFAULT 0")
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ data class RecipientRecord(
|
||||
val changeNumberCapability: Recipient.Capability,
|
||||
val storiesCapability: Recipient.Capability,
|
||||
val giftBadgesCapability: Recipient.Capability,
|
||||
val pnpCapability: Recipient.Capability,
|
||||
val insightsBannerTier: InsightsBannerTier,
|
||||
val storageId: ByteArray?,
|
||||
val mentionSetting: MentionSetting,
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -49,7 +49,7 @@ public class DeleteAccountCountryPickerFragment extends DialogFragment {
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> dismiss());
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(DeleteAccountViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(DeleteAccountViewModel.class);
|
||||
viewModel.getFilteredCountries().observe(getViewLifecycleOwner(), adapter::submitList);
|
||||
|
||||
searchFilter.addTextChangedListener(new AfterTextChanged(this::onQueryChanged));
|
||||
|
||||
@@ -19,7 +19,7 @@ import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
@@ -83,7 +83,7 @@ public abstract class DeviceTransferSetupFragment extends LoggingFragment {
|
||||
MaterialButton verifyNo = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_no);
|
||||
MaterialButton verifyYes = view.findViewById(R.id.device_transfer_setup_fragment_sas_verify_yes);
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(DeviceTransferSetupViewModel.class);
|
||||
viewModel = new ViewModelProvider(this).get(DeviceTransferSetupViewModel.class);
|
||||
|
||||
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
|
||||
SetupStep step = state.getCurrentSetupStep();
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.thoughtcrime.securesms.exporter
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
import org.signal.smsexporter.SmsExportState
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import java.io.Closeable
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class SignalSmsExportReader(
|
||||
smsCursor: Cursor,
|
||||
mmsCursor: Cursor
|
||||
) : Iterable<ExportableMessage>, Closeable {
|
||||
|
||||
private val smsReader = SmsDatabase.readerFor(smsCursor)
|
||||
private val mmsReader = MmsDatabase.readerFor(mmsCursor)
|
||||
|
||||
override fun iterator(): Iterator<ExportableMessage> {
|
||||
return ExportableMessageIterator()
|
||||
}
|
||||
|
||||
fun getCount(): Int {
|
||||
return smsReader.count + mmsReader.count
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
smsReader.close()
|
||||
mmsReader.close()
|
||||
}
|
||||
|
||||
private inner class ExportableMessageIterator : Iterator<ExportableMessage> {
|
||||
|
||||
private val smsIterator = smsReader.iterator()
|
||||
private val mmsIterator = mmsReader.iterator()
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return smsIterator.hasNext() || mmsIterator.hasNext()
|
||||
}
|
||||
|
||||
override fun next(): ExportableMessage {
|
||||
return if (smsIterator.hasNext()) {
|
||||
readExportableSmsMessageFromRecord(smsIterator.next())
|
||||
} else if (mmsIterator.hasNext()) {
|
||||
readExportableMmsMessageFromRecord(mmsIterator.next())
|
||||
} else {
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readExportableMmsMessageFromRecord(record: MessageRecord): ExportableMessage {
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!!
|
||||
val addresses = if (threadRecipient.isMmsGroup) {
|
||||
Recipient.resolvedList(threadRecipient.participantIds).map { it.requireSmsAddress() }.toSet()
|
||||
} else {
|
||||
setOf(threadRecipient.requireSmsAddress())
|
||||
}
|
||||
|
||||
val parts: MutableList<ExportableMessage.Mms.Part> = mutableListOf()
|
||||
if (record.body.isNotBlank()) {
|
||||
parts.add(ExportableMessage.Mms.Part.Text(record.body))
|
||||
}
|
||||
|
||||
if (record is MmsMessageRecord) {
|
||||
val slideDeck = record.slideDeck
|
||||
slideDeck.slides.forEach {
|
||||
parts.add(
|
||||
ExportableMessage.Mms.Part.Stream(
|
||||
id = JsonUtils.toJson((it.asAttachment() as DatabaseAttachment).attachmentId),
|
||||
contentType = it.contentType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val sender = if (record.isOutgoing) Recipient.self().requireSmsAddress() else record.individualRecipient.requireSmsAddress()
|
||||
|
||||
return ExportableMessage.Mms(
|
||||
id = record.id.toString(),
|
||||
exportState = mapExportState(mmsReader.messageExportStateForCurrentRecord),
|
||||
addresses = addresses,
|
||||
dateReceived = record.dateReceived.milliseconds,
|
||||
dateSent = record.dateSent.milliseconds,
|
||||
isRead = true,
|
||||
isOutgoing = record.isOutgoing,
|
||||
parts = parts,
|
||||
sender = sender
|
||||
)
|
||||
}
|
||||
|
||||
private fun readExportableSmsMessageFromRecord(record: MessageRecord): ExportableMessage {
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(record.threadId)!!
|
||||
|
||||
return if (threadRecipient.isMmsGroup) {
|
||||
readExportableMmsMessageFromRecord(record)
|
||||
} else {
|
||||
ExportableMessage.Sms(
|
||||
id = record.id.toString(),
|
||||
exportState = mapExportState(smsReader.messageExportStateForCurrentRecord),
|
||||
address = record.recipient.requireSmsAddress(),
|
||||
dateReceived = record.dateReceived.milliseconds,
|
||||
dateSent = record.dateSent.milliseconds,
|
||||
isRead = true,
|
||||
isOutgoing = record.isOutgoing,
|
||||
body = record.body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapExportState(messageExportState: MessageExportState): SmsExportState {
|
||||
return SmsExportState(
|
||||
messageId = messageExportState.messageId,
|
||||
startedRecipients = messageExportState.startedRecipientsList.toSet(),
|
||||
completedRecipients = messageExportState.completedRecipientsList.toSet(),
|
||||
startedAttachments = messageExportState.startedAttachmentsList.toSet(),
|
||||
completedAttachments = messageExportState.completedAttachmentsList.toSet(),
|
||||
progress = messageExportState.progress.let {
|
||||
when (it) {
|
||||
MessageExportState.Progress.INIT -> SmsExportState.Progress.INIT
|
||||
MessageExportState.Progress.STARTED -> SmsExportState.Progress.STARTED
|
||||
MessageExportState.Progress.COMPLETED -> SmsExportState.Progress.COMPLETED
|
||||
MessageExportState.Progress.UNRECOGNIZED -> SmsExportState.Progress.INIT
|
||||
null -> SmsExportState.Progress.INIT
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package org.thoughtcrime.securesms.exporter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.signal.smsexporter.ExportableMessage
|
||||
import org.signal.smsexporter.SmsExportService
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Service which integrates the SMS exporter functionality.
|
||||
*/
|
||||
class SignalSmsExportService : SmsExportService() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Launches the export service and immediately begins exporting messages.
|
||||
*/
|
||||
fun start(context: Context) {
|
||||
ContextCompat.startForegroundService(context, Intent(context, SignalSmsExportService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private var reader: SignalSmsExportReader? = null
|
||||
|
||||
override fun getNotification(progress: Int, total: Int): ExportNotification {
|
||||
return ExportNotification(
|
||||
NotificationIds.SMS_EXPORT_SERVICE,
|
||||
NotificationCompat.Builder(this, NotificationChannels.BACKUPS)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(getString(R.string.SignalSmsExportService__exporting_messages))
|
||||
.setProgress(total, progress, false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getUnexportedMessageCount(): Int {
|
||||
ensureReader()
|
||||
return reader!!.getCount()
|
||||
}
|
||||
|
||||
override fun getUnexportedMessages(): Iterable<ExportableMessage> {
|
||||
ensureReader()
|
||||
return reader!!
|
||||
}
|
||||
|
||||
override fun onMessageExportStarted(exportableMessage: ExportableMessage) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().setProgress(MessageExportState.Progress.STARTED).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageExportSucceeded(exportableMessage: ExportableMessage) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().setProgress(MessageExportState.Progress.COMPLETED).build()
|
||||
}
|
||||
|
||||
SignalDatabase.mmsSms.markMessageExported(exportableMessage.getMessageId())
|
||||
}
|
||||
|
||||
override fun onMessageExportFailed(exportableMessage: ExportableMessage) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().setProgress(MessageExportState.Progress.INIT).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().setMessageId(messageId).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().addStartedAttachments(part.contentId).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().addCompletedAttachments(part.contentId).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
val startedAttachments = it.startedAttachmentsList - part.contentId
|
||||
it.toBuilder().clearStartedAttachments().addAllStartedAttachments(startedAttachments).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().addStartedRecipients(recipient).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
it.toBuilder().addCompletedRecipients(recipient).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) {
|
||||
SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) {
|
||||
val startedAttachments = it.startedRecipientsList - recipient
|
||||
it.toBuilder().clearStartedRecipients().addAllStartedRecipients(startedAttachments).build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInputStream(part: ExportableMessage.Mms.Part): InputStream {
|
||||
return SignalDatabase.attachments.getAttachmentStream(JsonUtils.fromJson(part.contentId, AttachmentId::class.java), 0)
|
||||
}
|
||||
|
||||
override fun onExportPassCompleted() {
|
||||
reader?.close()
|
||||
}
|
||||
|
||||
private fun ExportableMessage.getMessageId(): MessageId {
|
||||
return when (this) {
|
||||
is ExportableMessage.Mms -> MessageId(id.toLong(), true)
|
||||
is ExportableMessage.Sms -> MessageId(id.toLong(), false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureReader() {
|
||||
if (reader == null) {
|
||||
reader = SignalSmsExportReader(
|
||||
smsCursor = SignalDatabase.sms.unexportedInsecureMessages,
|
||||
mmsCursor = SignalDatabase.mms.unexportedInsecureMessages
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.exporter.flow
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.smsexporter.DefaultSmsHelper
|
||||
import org.signal.smsexporter.ReleaseSmsAppFailure
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.ChooseANewDefaultSmsAppFragmentBinding
|
||||
|
||||
/**
|
||||
* Fragment which can launch the user into picking an alternative
|
||||
* SMS app, or give them instructions on how to do so manually.
|
||||
*/
|
||||
class ChooseANewDefaultSmsAppFragment : Fragment(R.layout.choose_a_new_default_sms_app_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChooseANewDefaultSmsAppFragment::class.java)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val binding = ChooseANewDefaultSmsAppFragmentBinding.bind(view)
|
||||
|
||||
DefaultSmsHelper.releaseDefaultSms(requireContext()).either(
|
||||
onSuccess = {
|
||||
binding.continueButton.setOnClickListener { _ -> startActivity(it) }
|
||||
},
|
||||
onFailure = {
|
||||
when (it) {
|
||||
ReleaseSmsAppFailure.APP_IS_INELIGIBLE_TO_RELEASE_SMS_SELECTION -> {
|
||||
Log.w(TAG, "App is ineligible to release sms selection")
|
||||
binding.continueButton.setOnClickListener { requireActivity().finish() }
|
||||
}
|
||||
ReleaseSmsAppFailure.NO_METHOD_TO_RELEASE_SMS_AVIALABLE -> {
|
||||
Log.w(TAG, "We can't navigate the user to a specific spot so we should display instructions instead.")
|
||||
binding.continueButton.setOnClickListener { requireActivity().finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!DefaultSmsHelper.isDefaultSms(requireContext())) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.exporter.flow
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.smsexporter.DefaultSmsHelper
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.ExportYourSmsMessagesFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* "Welcome" screen for exporting sms
|
||||
*/
|
||||
class ExportYourSmsMessagesFragment : Fragment(R.layout.export_your_sms_messages_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val binding = ExportYourSmsMessagesFragmentBinding.bind(view)
|
||||
|
||||
binding.toolbar.setOnClickListener {
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
binding.continueButton.setOnClickListener {
|
||||
if (DefaultSmsHelper.isDefaultSms(requireContext())) {
|
||||
findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToExportingSmsMessagesFragment())
|
||||
} else {
|
||||
findNavController().safeNavigate(ExportYourSmsMessagesFragmentDirections.actionExportYourSmsMessagesFragmentToSetSignalAsDefaultSmsAppFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.exporter.flow
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.smsexporter.SmsExportProgress
|
||||
import org.signal.smsexporter.SmsExportService
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.ExportingSmsMessagesFragmentBinding
|
||||
import org.thoughtcrime.securesms.exporter.SignalSmsExportService
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* "Export in progress" fragment which should be displayed
|
||||
* when we start exporting messages.
|
||||
*/
|
||||
class ExportingSmsMessagesFragment : Fragment(R.layout.exporting_sms_messages_fragment) {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val binding = ExportingSmsMessagesFragmentBinding.bind(view)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += SmsExportService.progressState.observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||
when (it) {
|
||||
SmsExportProgress.Done -> {
|
||||
findNavController().safeNavigate(ExportingSmsMessagesFragmentDirections.actionExportingSmsMessagesFragmentToChooseANewDefaultSmsAppFragment())
|
||||
}
|
||||
is SmsExportProgress.InProgress -> {
|
||||
binding.progress.isIndeterminate = false
|
||||
binding.progress.max = it.total
|
||||
binding.progress.progress = it.progress
|
||||
binding.progressLabel.text = getString(R.string.ExportingSmsMessagesFragment__exporting_d_of_d, it.progress, it.total)
|
||||
}
|
||||
SmsExportProgress.Init -> binding.progress.isIndeterminate = true
|
||||
SmsExportProgress.Starting -> binding.progress.isIndeterminate = true
|
||||
}
|
||||
}
|
||||
|
||||
SignalSmsExportService.start(requireContext())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.exporter.flow
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.smsexporter.BecomeSmsAppFailure
|
||||
import org.signal.smsexporter.DefaultSmsHelper
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.SetSignalAsDefaultSmsAppFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class SetSignalAsDefaultSmsAppFragment : Fragment(R.layout.set_signal_as_default_sms_app_fragment) {
|
||||
companion object {
|
||||
private const val REQUEST_CODE = 1
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val binding = SetSignalAsDefaultSmsAppFragmentBinding.bind(view)
|
||||
|
||||
binding.continueButton.setOnClickListener {
|
||||
DefaultSmsHelper.becomeDefaultSms(requireContext()).either(
|
||||
onSuccess = {
|
||||
startActivityForResult(it, REQUEST_CODE)
|
||||
},
|
||||
onFailure = {
|
||||
when (it) {
|
||||
BecomeSmsAppFailure.ALREADY_DEFAULT_SMS -> navigateToExporter()
|
||||
BecomeSmsAppFailure.ROLE_IS_NOT_AVAILABLE -> error("Should never happen")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE && DefaultSmsHelper.isDefaultSms(requireContext())) {
|
||||
navigateToExporter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToExporter() {
|
||||
findNavController().safeNavigate(SetSignalAsDefaultSmsAppFragmentDirections.actionSetSignalAsDefaultSmsAppFragmentToExportingSmsMessagesFragment())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.exporter.flow
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
|
||||
class SmsExportActivity : FragmentWrapperActivity() {
|
||||
override fun getFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.sms_export)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createIntent(context: Context): Intent = Intent(context, SmsExportActivity::class.java)
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.widget.ContentLoadingProgressBar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
@@ -45,7 +45,7 @@ public class GiphyMp4Fragment extends Fragment {
|
||||
RecyclerView recycler = view.findViewById(R.id.giphy_recycler);
|
||||
ContentLoadingProgressBar progressBar = view.findViewById(R.id.content_loading);
|
||||
TextView nothingFound = view.findViewById(R.id.nothing_found);
|
||||
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
|
||||
GiphyMp4ViewModel viewModel = new ViewModelProvider(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
|
||||
GiphyMp4Adapter adapter = new GiphyMp4Adapter(viewModel::saveToBlob);
|
||||
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
|
||||
getViewLifecycleOwner().getLifecycle(),
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -65,7 +65,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
|
||||
sendType = getIntent().getParcelableExtra(EXTRA_TRANSPORT);
|
||||
text = getIntent().getCharSequenceExtra(EXTRA_TEXT);
|
||||
|
||||
giphyMp4ViewModel = ViewModelProviders.of(this, new GiphyMp4ViewModel.Factory(forMms)).get(GiphyMp4ViewModel.class);
|
||||
giphyMp4ViewModel = new ViewModelProvider(this, new GiphyMp4ViewModel.Factory(forMms)).get(GiphyMp4ViewModel.class);
|
||||
giphyMp4ViewModel.getSaveResultEvents().observe(this, this::handleGiphyMp4SaveResult);
|
||||
|
||||
initializeToolbar();
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class GroupManager {
|
||||
int disappearingMessagesTimer)
|
||||
throws GroupChangeBusyException, GroupChangeFailedException, IOException
|
||||
{
|
||||
boolean shouldAttemptToCreateV2 = !mms && !SignalStore.internalValues().gv2DoNotCreateGv2Groups();
|
||||
boolean shouldAttemptToCreateV2 = !mms;
|
||||
Set<RecipientId> memberIds = getMemberIds(members);
|
||||
|
||||
if (shouldAttemptToCreateV2) {
|
||||
|
||||
@@ -81,11 +81,6 @@ public final class GroupsV1MigrationUtil {
|
||||
throw new InvalidMigrationStateException();
|
||||
}
|
||||
|
||||
if (!forced && SignalStore.internalValues().disableGv1AutoMigrateInitiation()) {
|
||||
Log.w(TAG, "Auto migration initiation has been disabled! Skipping.");
|
||||
throw new InvalidMigrationStateException();
|
||||
}
|
||||
|
||||
List<Recipient> registeredMembers = RecipientUtil.getEligibleForSending(Recipient.resolvedList(groupRecipient.getParticipantIds()));
|
||||
|
||||
if (RecipientUtil.ensureUuidsAreAvailable(context, registeredMembers)) {
|
||||
@@ -136,7 +131,7 @@ public final class GroupsV1MigrationUtil {
|
||||
|
||||
DecryptedGroup decryptedGroup = performLocalMigration(context, gv1Id, threadId, groupRecipient);
|
||||
|
||||
if (newlyCreated && decryptedGroup != null && !SignalStore.internalValues().disableGv1AutoMigrateNotification()) {
|
||||
if (newlyCreated && decryptedGroup != null) {
|
||||
Log.i(TAG, "Sending no-op update to notify others.");
|
||||
GroupManager.sendNoopUpdate(context, gv2MasterKey, decryptedGroup);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
@@ -60,8 +60,7 @@ public class AddMembersActivity extends PushContactSelectionActivity {
|
||||
AddMembersViewModel.Factory factory = new AddMembersViewModel.Factory(getGroupId());
|
||||
|
||||
done = findViewById(R.id.done);
|
||||
viewModel = ViewModelProviders.of(this, factory)
|
||||
.get(AddMembersViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(AddMembersViewModel.class);
|
||||
|
||||
done.setOnClickListener(v ->
|
||||
viewModel.getDialogStateForSelectedContacts(contactsFragment.getSelectedContacts(), this::displayAlertMessage)
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
@@ -73,8 +73,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
|
||||
next.setOnClickListener(v -> handleNextPressed());
|
||||
|
||||
AddToGroupViewModel.Factory factory = new AddToGroupViewModel.Factory(getRecipientId());
|
||||
viewModel = ViewModelProviders.of(this, factory)
|
||||
.get(AddToGroupViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(AddToGroupViewModel.class);
|
||||
|
||||
|
||||
viewModel.getEvents().observe(this, event -> {
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -101,7 +101,7 @@ public final class ChooseNewAdminActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this, new ChooseNewAdminViewModel.Factory(groupId)).get(ChooseNewAdminViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, new ChooseNewAdminViewModel.Factory(groupId)).get(ChooseNewAdminViewModel.class);
|
||||
|
||||
viewModel.getNonAdminFullMembers().observe(this, groupList::setMembers);
|
||||
viewModel.getSelection().observe(this, selection -> done.setVisibility(selection.isEmpty() ? View.GONE : View.VISIBLE));
|
||||
|
||||
@@ -20,7 +20,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
@@ -200,7 +200,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
||||
AddGroupDetailsRepository repository = new AddGroupDetailsRepository(requireContext());
|
||||
AddGroupDetailsViewModel.Factory factory = new AddGroupDetailsViewModel.Factory(Arrays.asList(args.getRecipientIds()), repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(AddGroupDetailsViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(AddGroupDetailsViewModel.class);
|
||||
|
||||
viewModel.getGroupCreateResult().observe(getViewLifecycleOwner(), this::handleGroupCreateResult);
|
||||
}
|
||||
|
||||
@@ -50,17 +50,8 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
|
||||
LiveData<Boolean> isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
|
||||
|
||||
members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
|
||||
isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
|
||||
|
||||
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersToCheckGv2CapabilityOf = LiveDataUtil.combineLatest(isMms, members, (forcedMms, memberList) -> {
|
||||
if (SignalStore.internalValues().gv2DoNotCreateGv2Groups() || forcedMms) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
return memberList;
|
||||
}
|
||||
});
|
||||
|
||||
members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
|
||||
isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
|
||||
canSubmitForm = LiveDataUtil.combineLatest(isMms, isValidName, (mms, validName) -> mms || validName);
|
||||
|
||||
repository.resolveMembers(recipientIds, initialMembers::postValue);
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
@@ -85,7 +85,7 @@ public final class GroupLinkInviteFriendsBottomSheetDialogFragment extends Botto
|
||||
GroupId.V2 groupId = getGroupId();
|
||||
|
||||
GroupLinkInviteFriendsViewModel.Factory factory = new GroupLinkInviteFriendsViewModel.Factory(requireContext().getApplicationContext(), groupId);
|
||||
GroupLinkInviteFriendsViewModel viewModel = ViewModelProviders.of(this, factory).get(GroupLinkInviteFriendsViewModel.class);
|
||||
GroupLinkInviteFriendsViewModel viewModel = new ViewModelProvider(this, factory).get(GroupLinkInviteFriendsViewModel.class);
|
||||
|
||||
viewModel.getGroupInviteLinkAndStatus()
|
||||
.observe(getViewLifecycleOwner(), groupLinkUrlAndStatus -> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
@@ -113,7 +113,7 @@ public class PendingMemberInvitesFragment extends Fragment {
|
||||
|
||||
PendingMemberInvitesViewModel.Factory factory = new PendingMemberInvitesViewModel.Factory(requireContext(), groupId);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity(), factory).get(PendingMemberInvitesViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity(), factory).get(PendingMemberInvitesViewModel.class);
|
||||
|
||||
viewModel.getWhoYouInvited().observe(getViewLifecycleOwner(), invitees -> {
|
||||
youInvited.setMembers(invitees);
|
||||
|
||||
@@ -15,7 +15,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
@@ -97,7 +97,7 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
|
||||
GroupJoinViewModel.Factory factory = new GroupJoinViewModel.Factory(requireContext().getApplicationContext(), getGroupInviteLinkUrl());
|
||||
|
||||
GroupJoinViewModel viewModel = ViewModelProviders.of(this, factory).get(GroupJoinViewModel.class);
|
||||
GroupJoinViewModel viewModel = new ViewModelProvider(this, factory).get(GroupJoinViewModel.class);
|
||||
|
||||
viewModel.getGroupDetails().observe(getViewLifecycleOwner(), details -> {
|
||||
groupName.setText(details.getGroupName());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user