Compare commits

..

94 Commits

Author SHA1 Message Date
Greyson Parrelli
f6b7b9e913 Bump version to 5.50.0 2022-09-14 15:31:42 -04:00
Greyson Parrelli
840a56cbb4 Updated language translations. 2022-09-14 15:30:44 -04:00
Nicholas
aa268fc3ba Only show "Note To Self" as Voice Memo author if both sender and receiver are self. 2022-09-14 15:30:44 -04:00
Alex Hart
889d1183b2 Allow the STORIES feature flag to be hot-swappable. 2022-09-14 15:30:44 -04:00
Alex Hart
a8706f65d5 Clean out witness verification metadata. 2022-09-14 15:30:44 -04:00
Alex Hart
26bebb9811 Upgrade several AndroidX Libraries.
AppCompat 1.2.0 to 1.5.1
Lifecycle 2.3.1 to 2.5.1
Navigation 2.3.5 to 2.5.2
Fragment 1.3.5 to 1.5.2
Annotations 1.2.0 to 1.4.0
Window 1.0.0-alpha09 to 1.0.0
AAPT2 to 7.0.4
Fragment-Testing 1.3.5 to 1.5.2 (matching Fragment)
2022-09-14 15:30:43 -04:00
Alex Hart
9331e9ce89 Add deprecation notice to SingleLiveEvent. 2022-09-14 15:30:43 -04:00
Greyson Parrelli
6417f5cce0 Improve logging around attachment compression failures. 2022-09-14 15:30:43 -04:00
Alex Hart
a340ebf74a Add espresso test for usernames. 2022-09-14 15:30:43 -04:00
Alex Hart
4882a4d11c Add new story-based AccountRecord fields and wiring. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
b5300c877c Fix issue with contact share editing.
Fixes #12446
2022-09-13 13:07:42 -04:00
Nicholas Tinsley
c2b94274b0 Cancel Send if we return to fragment.
This plugs a lifecycle hole: previously if you leave this fragment (SelectionConfirmed), you get stuck in that state even if you return.
2022-09-13 13:07:42 -04:00
Nicholas Tinsley
46ec45b985 Update ReminderView to Material Design 3. 2022-09-13 13:07:42 -04:00
Cody Henthorne
beee3b7dc3 Add PNP linked device initialization job.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-09-13 13:07:42 -04:00
Cody Henthorne
e2a7ed86e4 Promote ongoing call notification to high priority. 2022-09-13 13:07:42 -04:00
Alex Hart
95b0639ab4 Fix issue where video player is not released by preview fragment. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
d7f9582bc4 Update Megaphone text styling to match Material Design 3. 2022-09-13 13:07:42 -04:00
Alex Hart
176a705079 Add PNP Listing Wiring. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
8e9f311fca Refresh your own profile when the stories flag changes. 2022-09-13 13:07:42 -04:00
Alex Hart
977af2c2f3 Add capability to request username creation during registration. 2022-09-13 13:07:42 -04:00
Alex Hart
7e45fc4a3e Update URL formatting for username links. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
58489bab61 Update Basic Megaphone to Material Design 3. 2022-09-13 13:07:42 -04:00
Cody Henthorne
0685cf4e51 Add signal.me username support. 2022-09-13 13:07:42 -04:00
Alex Hart
9b9453734c Implement new API endpoints for Usernames. 2022-09-13 13:07:42 -04:00
Cody Henthorne
ca0e52e141 Fix bug with stale linked devices when changing number. 2022-09-13 13:07:42 -04:00
Alex Hart
24b7593178 Update camera layout for better support across different screen sizes. 2022-09-13 13:07:42 -04:00
Alex Hart
993e49db48 Username search UI tweak. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
d458ddba55 Schedule TrimThreadsByDateManager on app startup.
If enabled, this reschedules the alarm on every startup to make sure that the system never loses track of it.
2022-09-13 13:07:42 -04:00
Cody Henthorne
bd5747b7f6 Add more logging around failed backups. 2022-09-13 13:07:42 -04:00
Nicholas
a335130ad4 Clear Selection on ACTION_UP if longClickCopySpan is not found. 2022-09-13 13:07:42 -04:00
Cody Henthorne
9558513190 Prevent empty call screen after missed calls. 2022-09-13 13:07:42 -04:00
Alex Hart
27a3015d4f Set reply icon size to 20dp. 2022-09-13 13:07:42 -04:00
Alex Hart
f751f9afa8 Add support for new story gradient fields and fallback. 2022-09-13 13:07:42 -04:00
Alex Hart
2e2b31aa79 Start call after granting permissions.
Fixes #12419
2022-09-13 13:07:42 -04:00
Greyson Parrelli
135d002f02 Fix possible crash with CDSv2 compat. 2022-09-13 13:07:42 -04:00
Alex Hart
a45ede9348 Update AudioView in Attachment keyboard stub. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
e4b2e5022f Remove some outdated internal settings. 2022-09-13 13:07:42 -04:00
Alex Hart
286010ce90 Fix clickable area around link previews. 2022-09-13 13:07:41 -04:00
Alex Hart
13eb89746b Add unit testing to story download enqueuer. 2022-09-13 13:07:41 -04:00
Cody Henthorne
d2f639c57f Bump version to 5.49.3 2022-09-13 10:52:16 -04:00
Cody Henthorne
ad587606b7 Updated language translations. 2022-09-13 10:42:51 -04:00
Cody Henthorne
9fd5e2057d Fix reply messages for android auto. 2022-09-13 10:38:31 -04:00
Cody Henthorne
8f63b850fc Bump version to 5.49.2 2022-09-07 14:33:54 -04:00
Cody Henthorne
199d04b663 Updated language translations. 2022-09-07 14:28:53 -04:00
Greyson Parrelli
658741be52 Fix token mismatch issues when using CDSv2. 2022-09-07 14:25:03 -04:00
Alex Hart
f1bcc756d3 Remove animation from flash helper. 2022-09-06 10:26:45 -03:00
Alex Hart
cdcb1de3d4 Bump version to 5.49.1 2022-09-01 17:17:03 -03:00
Alex Hart
7d11a6207a Updated language translations. 2022-09-01 17:16:26 -03:00
Alex Hart
e608ad24c2 Hide keyboard when closing the bubble activity. 2022-09-01 17:06:51 -03:00
Alex Hart
4fe382398e Adjust alpha and duration of selfie flash animation. 2022-09-01 17:06:51 -03:00
Alex Hart
b6546f3ae3 Fix single tap on video previews. 2022-09-01 17:06:51 -03:00
Alex Hart
4620eade58 Implement better state management and recoverability for donation badge jobs. 2022-09-01 17:06:51 -03:00
Alex Hart
23a328f12d Add screen to set Signal as default SMS. 2022-09-01 13:17:53 -03:00
Alex Hart
83905dd6a6 Bump version to 5.49.0 2022-08-31 15:58:41 -04:00
Alex Hart
3eb4eb3c09 Updated language translations. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
2eba9a8d72 Add support for doing normal CDS queries on CDSv2. 2022-08-31 15:58:41 -04:00
Alex Hart
9b17e7a7e2 Fix story launching from settings. 2022-08-31 15:58:41 -04:00
Alex Hart
3eb9e4a035 Upgrade Glide to 4.13.2 and upgrade ExifInterface to 1.3.3 2022-08-31 15:58:41 -04:00
Alex Hart
3edc97eb38 Fix NPE when the attachment for a link preview is null. 2022-08-31 15:58:41 -04:00
Jim Gustafson
cb0208af4d Update to RingRTC v2.21.0 2022-08-31 15:58:41 -04:00
Greyson Parrelli
cdd311f741 Fix for possible issue in search. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
8543325d59 Update database migrations to be in their own files. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
a1a677a3e2 Apply network interceptors to CDSv2 websocket client. 2022-08-31 15:58:41 -04:00
Alex Hart
3705465ef2 Update translation strings for story privacy modes. 2022-08-31 15:58:41 -04:00
Alex Voloshyn
c80999839b Use AccountSnapshot to avoid unnecessary network calls. 2022-08-31 15:58:41 -04:00
Alex Hart
936212e684 Add initial sms exporter integration behind a feature flag. 2022-08-31 15:58:41 -04:00
Alex Hart
1cc39fb89b Fix launching of story from chat ring. 2022-08-31 15:58:41 -04:00
Alex Hart
37d3a953c8 Do not display icons in my stories row. 2022-08-31 15:58:41 -04:00
Alex Hart
5a1a23d9ac Fix view-based selfie flash. 2022-08-31 15:58:41 -04:00
Alex Hart
6cb359b2d0 Prevent header decoration from passing NO_POSITION to getHeaderId. 2022-08-31 15:58:41 -04:00
Alex Hart
8bd89d1e63 Fix camera zoom issue on some devices. 2022-08-31 15:58:40 -04:00
gram-signal
f111ac7cf2 Return empty from CDSv2 refresh if current recipient list is empty.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-08-31 15:58:40 -04:00
Greyson Parrelli
f6e000ab97 Fix some PNI-related issues around change number. 2022-08-31 15:58:40 -04:00
Alex Hart
29869c93b2 Adjust placement of elements in first time nav. 2022-08-31 15:58:40 -04:00
Alex Hart
3aae5ce1de Fix hide text header. 2022-08-29 14:23:28 -03:00
Alex Hart
e379cf6127 Bump version to 5.48.3 2022-08-29 14:08:10 -03:00
Alex Hart
0c23cb5ca8 Updated language translations. 2022-08-29 14:07:38 -03:00
Greyson Parrelli
d26ba27069 Look at new v2 remote announcement manifest. 2022-08-29 12:38:38 -04:00
Greyson Parrelli
e918178694 Update launcher assets. 2022-08-29 11:16:49 -04:00
Alex Hart
3d075bdd65 Check for EXTRA_TEXT if we cannot parse EXTRA_STREAM. 2022-08-29 10:47:47 -03:00
Alex Hart
4a3b8af6af Utilize proper control color for text cursor in gift badge message. 2022-08-29 10:35:30 -03:00
Alex Hart
2743492076 Fix ISE when utilizing the ear piece for voice notes. 2022-08-29 10:09:28 -03:00
Alex Hart
6ebc453e4b Bump version to 5.48.2 2022-08-26 15:20:26 -03:00
Alex Hart
75bd950b9b Updated language translations. 2022-08-26 15:20:26 -03:00
Alex Hart
0b0c4eb8c0 Utilize themed colors in fallback resource photos. 2022-08-26 15:20:26 -03:00
Alex Hart
e7dbc874bb Utilize lock icon instead of group icon for distribution lists. 2022-08-26 15:20:26 -03:00
Alex Hart
17426f1dbb Add long-press action to copy sent timestamp to clipboard. 2022-08-26 15:20:26 -03:00
Alex Hart
e00ce48517 Add proper title to text story sender. 2022-08-26 15:20:26 -03:00
Alex Hart
cba1caa5be Add audio focus handling to voice note playback. 2022-08-26 15:20:26 -03:00
Alex Hart
5f6b073cb6 Do not invoke reveal animation when editing a group. 2022-08-26 15:20:26 -03:00
Alex Hart
51647a5017 Enable both use-cases if available. 2022-08-26 15:20:26 -03:00
Johan
fae2ceab39 Set correct variable for password timeout.
Fixes #12415
2022-08-26 15:20:23 -03:00
Greyson Parrelli
553346629a Update libphonenumber to 8.12.54 2022-08-25 16:30:05 -04:00
Greyson Parrelli
726f48bc33 Clear search toolbar upon opening. 2022-08-25 16:27:42 -04:00
412 changed files with 14421 additions and 6110 deletions

View File

@@ -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() {

View File

@@ -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

View File

@@ -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()

View File

@@ -109,7 +109,7 @@ class MyStoryMigrationTest {
}
private fun runMigration() {
MyStoryMigration.migrate(
V151_MyStoryMigration.migrate(
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
SignalDatabase.rawDatabase,
0,

View File

@@ -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()

View File

@@ -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
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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"/>

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -80,6 +80,10 @@ class Material3SearchToolbar @JvmOverloads constructor(
}
}
fun clearText() {
input.setText("")
}
interface Listener {
fun onSearchTextChange(text: String)
fun onSearchClosed()

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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 }
}
}

View File

@@ -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())

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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)
}
)

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -259,7 +259,7 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
}
private Cursor getUsernameSearchCursor() {
return ContactsCursorRows.forUsernameSearch(getUnknownContactTitle(), getFilter());
return ContactsCursorRows.forUsernameSearch(getFilter());
}
private String getUnknownContactTitle() {

View File

@@ -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) {

View File

@@ -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});
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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> {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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));

View File

@@ -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) {
}

View File

@@ -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)

View File

@@ -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<>();

View File

@@ -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()
)
}
}
}
}

View File

@@ -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()

View File

@@ -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();

View File

@@ -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;

View File

@@ -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());

View File

@@ -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)

View File

@@ -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.
*/

View File

@@ -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));

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -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()
)
}
}

View File

@@ -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
}
}

View File

@@ -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")

View File

@@ -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")
}
}

View File

@@ -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,

View File

@@ -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));

View File

@@ -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();

View File

@@ -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
}
}
)
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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())
}
}
}
}

View File

@@ -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())
}
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -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(),

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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 -> {

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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 -> {

View File

@@ -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);

View File

@@ -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