mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-28 17:04:27 +01:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 205549360a | |||
| 09f8f456f6 | |||
| 4d94ad8929 | |||
| e3e86d1b65 | |||
| b535d3f473 | |||
| 0b7eda3e94 | |||
| 2162fccd7d | |||
| 9ca47f5fc5 | |||
| e458b3ff66 | |||
| 7a34150319 | |||
| 71bd8f9ec3 | |||
| ad3d537d92 | |||
| b019aba3fc | |||
| bd5c60d442 | |||
| a57572cf64 | |||
| 59bd04d9b7 | |||
| 7f4829c80c | |||
| 70be67b267 | |||
| 6dc0229e72 | |||
| 338283eff5 | |||
| 9804c6cf5e | |||
| 072185ec36 | |||
| d239f503a8 | |||
| 12785f35c9 | |||
| c8dfc74d41 | |||
| 5b42b115db | |||
| 22f8996d3f | |||
| a5fe925958 | |||
| ba4d92de45 | |||
| 3e8190637c | |||
| 7ea3679a24 | |||
| 5a6c41e166 | |||
| 263d7b7df1 | |||
| be9ae5c8f2 | |||
| 635b5795ea | |||
| cfce0ad8f1 | |||
| d55d1a2acd | |||
| 0451ea188d | |||
| 01a2599c3c | |||
| 682e5e4a01 | |||
| 11ea9cb7b4 | |||
| 687818fbb5 | |||
| 7a2c084b49 | |||
| dfe6ae812d | |||
| 8b1c2bf2af | |||
| ab61b79028 | |||
| 2d04bb32fd | |||
| 836645ed2b | |||
| dd32a92104 | |||
| 4817b5a7ac | |||
| e0603b14bb | |||
| 5f22566e10 | |||
| 6f5d7c4b7b | |||
| 393f58c833 | |||
| d25ac71907 | |||
| 0df6c66d08 | |||
| 9abe34bd69 | |||
| 8e54254ffa | |||
| 0b6cd03115 | |||
| 07500f62ab | |||
| 3cbc61cd11 | |||
| 1be2aeaf2b | |||
| b9012e2820 | |||
| e9249f33a5 | |||
| 7af82fbc87 | |||
| 8b71bde890 | |||
| 1ae5e884ec | |||
| a30cacdb0c | |||
| 0a18d01801 | |||
| 4aa5447967 | |||
| a92079e31b | |||
| 628576817e | |||
| 297b7fedd3 | |||
| c0283fb75f | |||
| 70e768ba05 | |||
| 551a25cfc6 | |||
| 39da95f29b | |||
| 35ca16f294 | |||
| 7e79dfa57f | |||
| abf05293eb | |||
| e81098378f | |||
| 28d2dee617 | |||
| 7ad05dd53c | |||
| 75036acce5 | |||
| 1bebab13db | |||
| ec2a8d37d6 | |||
| ce05c60eb4 | |||
| d818a93928 | |||
| 4af791c903 | |||
| dcef0c9667 | |||
| 974e2efad5 | |||
| 85261afe42 | |||
| 1f7c42c24b | |||
| 4888805873 | |||
| c27dc2a42d | |||
| 17b806ae0e | |||
| 3fbce0c774 | |||
| 7c464a7759 | |||
| 0e1d60bf1d | |||
| 1f5d211b06 | |||
| 81926d1bc4 | |||
| b327d92b82 | |||
| 9a8e0dd64f | |||
| c50757880e | |||
| 99b1fa0ebb | |||
| 74f08927f2 |
+30
-27
@@ -1,27 +1,30 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Automatically keep GitHub Actions SHA-pinned to the latest commit SHAs.
|
||||
# Dependabot will update both the SHA and the inline version comment (e.g. # v6)
|
||||
# while leaving any extra documentation comments intact.
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
labels:
|
||||
- "dependencies"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "actions/*"
|
||||
gradle-actions:
|
||||
patterns:
|
||||
- "gradle/*"
|
||||
peter-evans:
|
||||
patterns:
|
||||
- "peter-evans/*"
|
||||
usefulness:
|
||||
patterns:
|
||||
- "usefulness/*"
|
||||
# Dependabot automated schedule is currently disabled.
|
||||
# To re-enable, uncomment the block below.
|
||||
#
|
||||
# version: 2
|
||||
# updates:
|
||||
# # Automatically keep GitHub Actions SHA-pinned to the latest commit SHAs.
|
||||
# # Dependabot will update both the SHA and the inline version comment (e.g. # v6)
|
||||
# # while leaving any extra documentation comments intact.
|
||||
# - package-ecosystem: "github-actions"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "weekly"
|
||||
# day: "monday"
|
||||
# labels:
|
||||
# - "dependencies"
|
||||
# commit-message:
|
||||
# prefix: "ci"
|
||||
# groups:
|
||||
# actions:
|
||||
# patterns:
|
||||
# - "actions/*"
|
||||
# gradle-actions:
|
||||
# patterns:
|
||||
# - "gradle/*"
|
||||
# peter-evans:
|
||||
# patterns:
|
||||
# - "peter-evans/*"
|
||||
# usefulness:
|
||||
# patterns:
|
||||
# - "usefulness/*"
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
lfs: true
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
|
||||
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
||||
with:
|
||||
distribution: temurin
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: set up JDK 21
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
|
||||
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
||||
with:
|
||||
distribution: temurin
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
||||
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
@@ -11,7 +11,29 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
# The build runs entirely inside the Docker container, which ships its
|
||||
# own JDK, Android SDK, and NDK. None of the host toolchains are needed.
|
||||
sudo rm -rf /usr/lib/jvm
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /usr/local/julia*
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/local/share/chromium
|
||||
sudo rm -rf /opt/microsoft /opt/google
|
||||
sudo rm -rf /opt/az
|
||||
sudo rm -rf /usr/local/share/powershell
|
||||
sudo rm -rf /opt/hostedtoolcache
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
- name: Disk usage after cleanup
|
||||
if: always()
|
||||
run: |
|
||||
df -h
|
||||
docker system df || true
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
@@ -23,5 +45,15 @@ jobs:
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Disk usage after image build
|
||||
if: always()
|
||||
run: |
|
||||
df -h
|
||||
docker system df || true
|
||||
- name: Test build
|
||||
run: docker run --memory=12g --rm -v "$(pwd)":/project -w /project signal-android ./gradlew --no-daemon --max-workers=1 -Dorg.gradle.jvmargs="-Xmx4g -XX:MaxMetaspaceSize=512m" -Dkotlin.compiler.execution.strategy=in-process bundlePlayProdRelease
|
||||
- name: Disk usage after test build
|
||||
if: always()
|
||||
run: |
|
||||
df -h
|
||||
docker system df || true
|
||||
|
||||
+15
-9
@@ -32,8 +32,8 @@ plugins {
|
||||
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
|
||||
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
|
||||
|
||||
val canonicalVersionCode = 1722
|
||||
val canonicalVersionName = "8.20.2"
|
||||
val canonicalVersionCode = 1725
|
||||
val canonicalVersionName = "8.21.0"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
@@ -154,7 +154,11 @@ android {
|
||||
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||
|
||||
buildToolsVersion = libs.versions.buildTools.get()
|
||||
compileSdkVersion(libs.versions.compileSdk.get())
|
||||
|
||||
compileSdk {
|
||||
version = release(libs.versions.compileSdk.get().toInt())
|
||||
}
|
||||
|
||||
ndkVersion = libs.versions.ndk.get()
|
||||
|
||||
flavorDimensions += listOf("distribution", "environment")
|
||||
@@ -191,12 +195,12 @@ android {
|
||||
|
||||
sourceSets {
|
||||
getByName("test") {
|
||||
java.srcDir("$projectDir/src/testShared")
|
||||
java.directories += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
getByName("androidTest") {
|
||||
java.srcDir("$projectDir/src/testShared")
|
||||
java.srcDir("$projectDir/src/benchmarkShared/java")
|
||||
java.directories += "$projectDir/src/testShared"
|
||||
java.directories += "$projectDir/src/benchmarkShared/java"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,6 +415,7 @@ android {
|
||||
isDefault = false
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "true")
|
||||
@@ -521,18 +526,18 @@ android {
|
||||
android.buildTypes.configureEach {
|
||||
val path = if (name == "release") releaseDir else debugDir
|
||||
sourceSets.named(name) {
|
||||
java.srcDir(path)
|
||||
java.directories += path
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("mocked") {
|
||||
java.srcDir("$projectDir/src/benchmarkShared/java")
|
||||
java.directories += "$projectDir/src/benchmarkShared/java"
|
||||
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
|
||||
}
|
||||
|
||||
getByName("benchmark") {
|
||||
java.srcDir("$projectDir/src/benchmarkShared/java")
|
||||
java.directories += "$projectDir/src/benchmarkShared/java"
|
||||
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
|
||||
}
|
||||
}
|
||||
@@ -698,6 +703,7 @@ dependencies {
|
||||
implementation(project(":core:ui"))
|
||||
implementation(project(":core:models"))
|
||||
implementation(project(":core:models-jvm"))
|
||||
implementation(project(":core:serialization"))
|
||||
implementation(project(":feature:camera"))
|
||||
implementation(project(":feature:registration"))
|
||||
implementation(project(":lib:apng"))
|
||||
|
||||
+4
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.permits.DonationPermits
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
|
||||
import org.thoughtcrime.securesms.testing.GooglePayTestRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
@@ -54,6 +55,9 @@ class CheckoutFlowActivityTest__OneTimeDonations {
|
||||
@get:Rule
|
||||
val composeRule = createEmptyComposeRule()
|
||||
|
||||
@get:Rule
|
||||
val animationsRule = DisableAnimationsRule()
|
||||
|
||||
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION)
|
||||
|
||||
@Before
|
||||
|
||||
+4
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
|
||||
import org.thoughtcrime.securesms.testing.GooglePayTestRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
@@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__RecurringDonations {
|
||||
@get:Rule
|
||||
val composeRule = createEmptyComposeRule()
|
||||
|
||||
@get:Rule
|
||||
val animationsRule = DisableAnimationsRule()
|
||||
|
||||
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)
|
||||
|
||||
@Before
|
||||
|
||||
+4
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
|
||||
import org.thoughtcrime.securesms.testing.GooglePayTestRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
|
||||
@@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__Redemption {
|
||||
@get:Rule
|
||||
val composeRule = createEmptyComposeRule()
|
||||
|
||||
@get:Rule
|
||||
val animationsRule = DisableAnimationsRule()
|
||||
|
||||
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION)
|
||||
|
||||
@Before
|
||||
|
||||
+4
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
|
||||
import org.thoughtcrime.securesms.testing.GooglePayTestRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.RawFlag
|
||||
@@ -69,6 +70,9 @@ class DonationsCheckoutSpecTest(private val spec: DonationsCheckoutTestSpec) {
|
||||
@get:Rule
|
||||
val composeRule = createEmptyComposeRule()
|
||||
|
||||
@get:Rule
|
||||
val animationsRule = DisableAnimationsRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
|
||||
|
||||
+1
-1
@@ -9,6 +9,7 @@ import org.signal.core.util.billing.BillingApi
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||
import org.signal.network.api.ArchiveApi
|
||||
import org.signal.network.config.SignalServiceConfiguration
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
|
||||
import org.thoughtcrime.securesms.testing.endpoints.DonationTestServer
|
||||
@@ -21,7 +22,6 @@ import org.whispersystems.signalservice.api.account.AccountApi
|
||||
import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import java.util.function.Supplier
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
+2
-1
@@ -101,7 +101,7 @@ class DataMessageProcessorTest_polls {
|
||||
message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)),
|
||||
senderRecipient = alice,
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId(), CiphertextMessage.WHISPER_TYPE),
|
||||
threadRecipient = bob,
|
||||
threadRecipient = Recipient.resolved(groupRecipientId),
|
||||
groupId = groupId,
|
||||
receivedTime = 200
|
||||
)
|
||||
@@ -310,6 +310,7 @@ class DataMessageProcessorTest_polls {
|
||||
envelope = MessageContentFuzzer.envelope(100),
|
||||
message = DataMessage(pollVote = pollVote),
|
||||
senderRecipient = senderRecipient,
|
||||
threadRecipient = Recipient.resolved(groupRecipientId),
|
||||
earlyMessageCacheEntry = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.provider.Settings
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.rules.ExternalResource
|
||||
import java.io.FileInputStream
|
||||
|
||||
/**
|
||||
* Disables system animation scales for the duration of a test and restores them afterward.
|
||||
*
|
||||
* Espresso and [android.app.Instrumentation.waitForIdleSync] only make progress once the main looper is idle. An
|
||||
* on-screen indeterminate animation (e.g. the checkout's `CircularProgressIndicator`) posts frame callbacks forever, so
|
||||
* the looper never idles and any idle-gated wait hangs indefinitely rather than timing out. Forcing the scales to 0
|
||||
* stops those animations so the looper can idle.
|
||||
*
|
||||
* Writes go through the shell (`UiAutomation`), which holds `WRITE_SECURE_SETTINGS`; the app process does not.
|
||||
*/
|
||||
class DisableAnimationsRule : ExternalResource() {
|
||||
|
||||
private val scales = listOf(
|
||||
Settings.Global.WINDOW_ANIMATION_SCALE,
|
||||
Settings.Global.TRANSITION_ANIMATION_SCALE,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE
|
||||
)
|
||||
|
||||
private lateinit var previous: Map<String, Float>
|
||||
|
||||
override fun before() {
|
||||
val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver
|
||||
previous = scales.associateWith { Settings.Global.getFloat(resolver, it, 1f) }
|
||||
scales.forEach { putScale(it, 0f) }
|
||||
}
|
||||
|
||||
override fun after() {
|
||||
if (::previous.isInitialized) {
|
||||
previous.forEach { (key, value) -> putScale(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun putScale(key: String, value: Float) {
|
||||
val stream = InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("settings put global $key $value")
|
||||
FileInputStream(stream.fileDescriptor).use { it.readBytes() }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,13 +4,13 @@ import android.app.Application
|
||||
import org.signal.benchmark.setup.NoOpJob
|
||||
import org.signal.core.util.UptimeSleepTimer
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.network.config.SignalServiceConfiguration
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection
|
||||
import java.util.function.Supplier
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -9,13 +9,13 @@ import android.app.Application
|
||||
import org.signal.benchmark.setup.NoOpJob
|
||||
import org.signal.core.util.UptimeSleepTimer
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.network.config.SignalServiceConfiguration
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection
|
||||
import java.util.function.Supplier
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -469,18 +469,18 @@
|
||||
<activity
|
||||
android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||
|
||||
<activity
|
||||
android:name=".mediasend.v3.MediaSendV3Activity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||
|
||||
@@ -892,6 +892,14 @@
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".clockskew.ClockSkewActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".ratelimit.RecaptchaProofActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,7 @@ import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
|
||||
import org.thoughtcrime.securesms.clockskew.ClockSkewDetector;
|
||||
import org.thoughtcrime.securesms.preferences.EditProxyActivity;
|
||||
import org.thoughtcrime.securesms.conversation.drafts.DraftBlobs;
|
||||
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
|
||||
@@ -420,6 +421,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
AppDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
}
|
||||
AppForegroundObserver.begin();
|
||||
ClockSkewDetector.beginObserving(this);
|
||||
|
||||
if (Environment.USE_NEW_REGISTRATION) {
|
||||
initializeRegistrationDependencies();
|
||||
@@ -429,7 +431,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
private void initializeRegistrationDependencies() {
|
||||
RegistrationDependencies.provide(
|
||||
new RegistrationDependencies(
|
||||
new AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
|
||||
new AppRegistrationNetworkController(this, AppDependencies.getRegistrationApiV2()),
|
||||
new AppRegistrationStorageController(this),
|
||||
Environment.IS_LINK_AND_SYNC_AVAILABLE,
|
||||
null,
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
/**
|
||||
* Activity for displaying avatars full screen.
|
||||
@@ -77,6 +78,9 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
setTheme(R.style.TextSecure_MediaPreview);
|
||||
setContentView(R.layout.contact_photo_preview_activity);
|
||||
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
WindowUtil.clearLightNavigationBar(getWindow());
|
||||
|
||||
postponeEnterTransition();
|
||||
TransitionInflater inflater = TransitionInflater.from(this);
|
||||
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -34,6 +35,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
logEvent("onCreate()");
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
AppStartup.getInstance().onCriticalRenderEventEnd();
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
@@ -34,6 +36,7 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -110,11 +113,16 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
getSupportActionBar().setIcon(null);
|
||||
getSupportActionBar().setLogo(null);
|
||||
|
||||
toolbar.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
SystemWindowInsetsSetter.attach(toolbar, this, WindowInsetsCompat.Type.statusBars());
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
|
||||
SystemWindowInsetsSetter.attach(contactsFragment.requireView(), this, WindowInsetsCompat.Type.navigationBars());
|
||||
}
|
||||
|
||||
private void initializeSearch() {
|
||||
|
||||
@@ -672,46 +672,54 @@ class MainActivity :
|
||||
}
|
||||
},
|
||||
primaryContent = {
|
||||
when (mainNavigationState.currentListLocation) {
|
||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.chatsBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = contentLayoutData.detailPaddingEnd)
|
||||
.clip(contentLayoutData.shape)
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
when (mainNavigationState.currentListLocation) {
|
||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.chatsBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.callsBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popCallsDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider { callsNavEntries(isSplitPane) }
|
||||
)
|
||||
}
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.callsBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popCallsDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider { callsNavEntries(isSplitPane) }
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.storiesBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popStoriesDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider { storiesNavEntries() }
|
||||
)
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
NavDisplay<NavKey>(
|
||||
backStack = mainNavigationViewModel.storiesBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popStoriesDetailLocation() },
|
||||
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
|
||||
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
|
||||
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider { storiesNavEntries() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -966,7 +974,6 @@ class MainActivity :
|
||||
onSetToolbarColor = {
|
||||
toolbarViewModel.setToolbarColor(it)
|
||||
},
|
||||
setStatusBarColor = {},
|
||||
lifecycleOwner = lifecycleOwner
|
||||
).attach(recyclerView)
|
||||
}
|
||||
@@ -976,7 +983,6 @@ class MainActivity :
|
||||
activity = this,
|
||||
views = listOf(chatFolders),
|
||||
viewStubs = listOf(),
|
||||
setStatusBarColor = {},
|
||||
onSetToolbarColor = {
|
||||
toolbarViewModel.setToolbarColor(it)
|
||||
},
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.signal.registration.RegistrationRoute;
|
||||
import org.thoughtcrime.securesms.clockskew.ClockSkewActivity;
|
||||
import org.thoughtcrime.securesms.clockskew.ClockSkewDetector;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
@@ -59,6 +61,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
private static final int STATE_TRANSFER_OR_RESTORE = 11;
|
||||
private static final int STATE_RESUME_LINKING_REG = 12;
|
||||
private static final int STATE_CLOCK_SKEW = 13;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -158,6 +161,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
|
||||
case STATE_RESUME_LINKING_REG: return getResumeLinkedRegistrationIntent();
|
||||
case STATE_CLOCK_SKEW: return getClockSkewIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -187,6 +191,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else if (ClockSkewDetector.INSTANCE.isDetected() && getClass() != ClockSkewActivity.class) {
|
||||
return STATE_CLOCK_SKEW;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
@@ -288,6 +294,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getClockSkewIntent() {
|
||||
return ClockSkewActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
|
||||
if (nextIntent != null) destination.putExtra(NEXT_INTENT_EXTRA, nextIntent);
|
||||
return destination;
|
||||
|
||||
@@ -63,6 +63,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
|
||||
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
|
||||
val keyboardAwareLayout: KeyboardAwareLinearLayout = view.findViewById(R.id.keyboard_aware_layout)
|
||||
keyboardAwareLayout.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.SAFE_AREA)
|
||||
|
||||
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
|
||||
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
|
||||
|
||||
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.database.SenderKeyTable;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
|
||||
import org.thoughtcrime.securesms.database.SessionTable;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTables;
|
||||
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
@@ -186,7 +186,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringPollMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(PollTables.PollOptionTable.POLL_ID))), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(PollTables.PollVoteTable.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringPollMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(PollTables.PollVoteTable.POLL_ID))), null, count, estimatedCount, cancellationSignal);
|
||||
} else if (table.equals(StickerTable.TABLE_NAME)) {
|
||||
} else if (table.equals(StickerTables.Sticker.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
|
||||
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
|
||||
count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal);
|
||||
@@ -238,7 +238,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
count += getCount(input, BackupCountQueries.getGroupReceiptCount());
|
||||
} else if (table.equals(AttachmentTable.TABLE_NAME)) {
|
||||
count += getCount(input, BackupCountQueries.getAttachmentCount());
|
||||
} else if (table.equals(StickerTable.TABLE_NAME)) {
|
||||
} else if (table.equals(StickerTables.Sticker.TABLE_NAME)) {
|
||||
count += getCount(input, "SELECT COUNT(*) FROM " + table);
|
||||
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
|
||||
count += getCount(input, "SELECT COUNT(*) FROM " + table);
|
||||
@@ -497,11 +497,11 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
long estimatedCount)
|
||||
throws IOException
|
||||
{
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable.ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable.FILE_LENGTH));
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTables.Sticker.ID));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_LENGTH));
|
||||
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerTable.FILE_PATH));
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerTable.FILE_RANDOM));
|
||||
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_PATH));
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_RANDOM));
|
||||
|
||||
if (!TextUtils.isEmpty(data) && size > 0) {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
|
||||
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.LastResortKeyTupleTable;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.SearchTable;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTables;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -244,7 +244,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File stickerDirectory = context.getDir(StickerTable.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File stickerDirectory = context.getDir(StickerTables.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
|
||||
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
@@ -252,12 +252,12 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
inputStream.readAttachmentTo(output.getSecond(), sticker.length);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(StickerTable.FILE_PATH, dataFile.getAbsolutePath());
|
||||
contentValues.put(StickerTable.FILE_LENGTH, sticker.length);
|
||||
contentValues.put(StickerTable.FILE_RANDOM, output.getFirst());
|
||||
contentValues.put(StickerTables.Sticker.FILE_PATH, dataFile.getAbsolutePath());
|
||||
contentValues.put(StickerTables.Sticker.FILE_LENGTH, sticker.length);
|
||||
contentValues.put(StickerTables.Sticker.FILE_RANDOM, output.getFirst());
|
||||
|
||||
db.update(StickerTable.TABLE_NAME, contentValues,
|
||||
StickerTable.ID + " = ?",
|
||||
db.update(StickerTables.Sticker.TABLE_NAME, contentValues,
|
||||
StickerTables.Sticker.ID + " = ?",
|
||||
new String[] {String.valueOf(sticker.rowId)});
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ import org.thoughtcrime.securesms.database.SearchTable
|
||||
import org.thoughtcrime.securesms.database.SessionTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
||||
import org.thoughtcrime.securesms.database.StickerTable
|
||||
import org.thoughtcrime.securesms.database.StickerTables
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -1338,7 +1338,7 @@ object BackupRepository {
|
||||
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
||||
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
||||
|
||||
val importState = ImportState(mediaRootBackupKey)
|
||||
val importState = ImportState(mediaRootBackupKey, backupMode)
|
||||
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
|
||||
|
||||
Log.d(TAG, "[import] Beginning to read frames.")
|
||||
@@ -1487,7 +1487,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
val stickerJobs = SignalDatabase.stickers.getAllStickerPacks().use { cursor ->
|
||||
val reader = StickerTable.StickerPackRecordReader(cursor)
|
||||
val reader = StickerTables.StickerPackRecordReader(cursor)
|
||||
reader
|
||||
.filter { it.isInstalled }
|
||||
.map {
|
||||
@@ -1522,7 +1522,7 @@ object BackupRepository {
|
||||
val jobs = mutableListOf<Job>()
|
||||
groups
|
||||
.asSequence()
|
||||
.filter { it.id.isV2 }
|
||||
.filter { it.id.isV2 && it.hasV2GroupProperties }
|
||||
.forEach { group ->
|
||||
jobs.add(RequestGroupV2InfoJob(group.id as GroupId.V2))
|
||||
val avatarKey = group.requireV2GroupProperties().avatarKey
|
||||
@@ -2530,7 +2530,7 @@ class ExportState(
|
||||
var releaseNoteRecipientId: Long? = null
|
||||
}
|
||||
|
||||
class ImportState(val mediaRootBackupKey: MediaRootBackupKey) {
|
||||
class ImportState(val mediaRootBackupKey: MediaRootBackupKey, val backupMode: BackupMode) {
|
||||
val remoteToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
|
||||
val chatIdToLocalThreadId: MutableMap<Long, Long> = hashMapOf()
|
||||
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
|
||||
|
||||
@@ -104,11 +104,23 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if [dir] appears to be a SignalBackups directory based on its name and
|
||||
* expected internal structure (presence of the "files" subdirectory).
|
||||
* Returns true if [dir] appears to be a SignalBackups directory based on its expected internal
|
||||
* structure. We can't rely on the directory being named [MAIN_DIRECTORY_NAME], since users may
|
||||
* rename it or restore it into a differently-named folder (e.g. when transferring a backup
|
||||
* between devices). A directory is therefore considered a backups directory if it either matches
|
||||
* the expected name or directly contains the archive contents (a "files" subdirectory alongside
|
||||
* at least one snapshot directory).
|
||||
*/
|
||||
private fun looksLikeSignalBackupsDirectory(dir: DocumentFile): Boolean {
|
||||
return dir.name == MAIN_DIRECTORY_NAME && dir.findFile("files") != null
|
||||
if (dir.findFile("files") == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dir.name == MAIN_DIRECTORY_NAME) {
|
||||
return true
|
||||
}
|
||||
|
||||
return dir.listFiles().any { it.isDirectory && it.name?.startsWith(BACKUP_DIRECTORY_PREFIX) == true }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -275,7 +275,7 @@ object AccountDataArchiveProcessor {
|
||||
SignalStore.story.isFeatureDisabled = settings.storiesDisabled
|
||||
SignalStore.story.userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
|
||||
SignalStore.story.viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
|
||||
SignalStore.backup.optimizeStorage = settings.optimizeOnDeviceStorage
|
||||
SignalStore.backup.optimizeStorage = !importState.backupMode.isLinkAndSync && settings.optimizeOnDeviceStorage
|
||||
SignalStore.backup.backupTier = MessageBackupTier.fromBackupLevel(settings.backupTier)
|
||||
SignalStore.settings.sentMediaQuality = settings.defaultSentMediaQuality.toLocalSentMediaQuality()
|
||||
SignalStore.settings.setTheme(settings.appTheme.toLocalTheme())
|
||||
|
||||
+6
-19
@@ -10,13 +10,10 @@ import org.signal.archive.proto.Frame
|
||||
import org.signal.archive.proto.StickerPack
|
||||
import org.signal.archive.stream.BackupFrameEmitter
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ExportSkips
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.StickerTable
|
||||
import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader
|
||||
import org.thoughtcrime.securesms.database.StickerTables.StickerPackRecordReader
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord
|
||||
import java.io.IOException
|
||||
|
||||
@@ -27,7 +24,7 @@ private val TAG = Log.tag(StickerArchiveProcessor::class)
|
||||
*/
|
||||
object StickerArchiveProcessor {
|
||||
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
|
||||
StickerPackRecordReader(db.stickerTable.getAllStickerPacks()).use { reader ->
|
||||
StickerPackRecordReader(db.stickerTables.getAllStickerPacks()).use { reader ->
|
||||
var record: StickerPackRecord? = null
|
||||
while (reader.getNext()?.let { record = it } != null) {
|
||||
if (record!!.isInstalled) {
|
||||
@@ -39,20 +36,10 @@ object StickerArchiveProcessor {
|
||||
}
|
||||
|
||||
fun import(stickerPack: StickerPack) {
|
||||
SignalDatabase.rawDatabase
|
||||
.insertInto(StickerTable.TABLE_NAME)
|
||||
.values(
|
||||
StickerTable.PACK_ID to Hex.toStringCondensed(stickerPack.packId.toByteArray()),
|
||||
StickerTable.PACK_KEY to Hex.toStringCondensed(stickerPack.packKey.toByteArray()),
|
||||
StickerTable.PACK_TITLE to "",
|
||||
StickerTable.PACK_AUTHOR to "",
|
||||
StickerTable.INSTALLED to 1,
|
||||
StickerTable.COVER to 1,
|
||||
StickerTable.EMOJI to "",
|
||||
StickerTable.CONTENT_TYPE to "",
|
||||
StickerTable.FILE_PATH to ""
|
||||
)
|
||||
.run(SQLiteDatabase.CONFLICT_IGNORE)
|
||||
SignalDatabase.stickers.insertPackReference(
|
||||
packId = Hex.toStringCondensed(stickerPack.packId.toByteArray()),
|
||||
packKey = Hex.toStringCondensed(stickerPack.packKey.toByteArray())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-8
@@ -7,8 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.os.bundleOf
|
||||
@@ -42,12 +40,6 @@ class MessageBackupsCheckoutActivity : FragmentWrapperActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
enableEdgeToEdge()
|
||||
}
|
||||
|
||||
override fun getFragment(): Fragment = MessageBackupsFlowFragment.create(
|
||||
IntentCompat.getSerializableExtra(intent, TIER, MessageBackupTier::class.java)
|
||||
)
|
||||
|
||||
+2
-2
@@ -52,14 +52,14 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.ui.fonts.SignalSymbols
|
||||
import org.signal.core.ui.fonts.SignalSymbols.signalSymbolText
|
||||
import org.signal.core.util.ByteUnit
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
-3
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -49,8 +48,6 @@ class VerifyBackupKeyActivity : PassphraseRequiredActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
SignalTheme {
|
||||
val context = LocalContext.current
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
@@ -94,6 +95,7 @@ class GiftFlowConfirmationFragment :
|
||||
.create()
|
||||
|
||||
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
|
||||
inputAwareLayout.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.KEYBOARD_AND_NAVIGATION_BAR)
|
||||
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
|
||||
|
||||
emojiKeyboard.setFragmentManager(childFragmentManager)
|
||||
|
||||
@@ -4,11 +4,13 @@ import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
@@ -28,6 +30,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -63,6 +66,11 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
container = findViewById(R.id.fragment_container);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
|
||||
toolbar.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
SystemWindowInsetsSetter.attach(toolbar, this, WindowInsetsCompat.Type.statusBars());
|
||||
SystemWindowInsetsSetter.attach(container, this, WindowInsetsCompat.Type.navigationBars());
|
||||
|
||||
contactFilterView.setOnFilterChangedListener(query -> {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
|
||||
if (fragment != null) {
|
||||
|
||||
+10
-1
@@ -11,7 +11,9 @@ import android.view.WindowManager
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -43,6 +45,7 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeDialogFragment
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.enableEdgeToEdge
|
||||
import org.signal.core.ui.rememberIsSplitPane
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -68,6 +71,7 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
dialog.window?.enableEdgeToEdge()
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -143,7 +147,12 @@ private fun EditCallLinkNameScreen(
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val breakIterator = remember { BreakIteratorCompat.getInstance() }
|
||||
|
||||
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.imePadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
|
||||
@@ -91,28 +91,24 @@ class CallEventCache(
|
||||
): CallLogRow.Call? {
|
||||
val parent = next()
|
||||
|
||||
if (parent.type == Type.AD_HOC_CALL.code && !callLinksSeen.add(parent.peer)) {
|
||||
return null
|
||||
}
|
||||
val isActiveCallLinkRow = parent.type != Type.AD_HOC_CALL.code || callLinksSeen.add(parent.peer)
|
||||
|
||||
val children = mutableSetOf<Long>()
|
||||
while (hasNext()) {
|
||||
val child = next()
|
||||
|
||||
if (child.type == Type.AD_HOC_CALL.code) {
|
||||
previous()
|
||||
break
|
||||
}
|
||||
if (parent.type != Type.AD_HOC_CALL.code) {
|
||||
while (hasNext()) {
|
||||
val child = next()
|
||||
|
||||
if (parent.peer == child.peer && parent.direction == child.direction && isEventMatch(parent, child) && isWithinTimeout(parent, child)) {
|
||||
children.add(child.rowId)
|
||||
} else {
|
||||
previous()
|
||||
break
|
||||
if (parent.peer == child.peer && parent.direction == child.direction && isEventMatch(parent, child) && isWithinTimeout(parent, child)) {
|
||||
children.add(child.rowId)
|
||||
} else {
|
||||
previous()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createParentCallLogRow(parent, children, filterState, groupCallStateMap, canUserBeginCallMap)
|
||||
return createParentCallLogRow(parent, children, filterState, groupCallStateMap, canUserBeginCallMap, isActiveCallLinkRow)
|
||||
}
|
||||
|
||||
private fun readDataFromDatabase(): List<CacheRecord> {
|
||||
@@ -170,7 +166,8 @@ class CallEventCache(
|
||||
children: Set<Long>,
|
||||
filterState: FilterState,
|
||||
groupCallStateCache: MutableMap<Long, CallLogRow.GroupCallState>,
|
||||
canUserBeginCallMap: MutableMap<Long, CallLogRow.CanStartCall>
|
||||
canUserBeginCallMap: MutableMap<Long, CallLogRow.CanStartCall>,
|
||||
isActiveCallLinkRow: Boolean
|
||||
): CallLogRow.Call {
|
||||
val peer = Recipient.resolved(RecipientId.from(parent.peer))
|
||||
return CallLogRow.Call(
|
||||
@@ -196,7 +193,7 @@ class CallEventCache(
|
||||
},
|
||||
children = setOf(parent.rowId) + children,
|
||||
searchQuery = filterState.query,
|
||||
callLinkPeekInfo = AppDependencies.signalCallManager.peekInfoSnapshot[peer.id],
|
||||
callLinkPeekInfo = if (isActiveCallLinkRow) AppDependencies.signalCallManager.peekInfoSnapshot[peer.id] else null,
|
||||
canUserBeginCall = if (peer.isGroup) {
|
||||
canUserBeginCallMap.getOrPut(parent.peer) { canUserBeginCall(peer, parent.decryptedGroupBytes) }
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -60,7 +59,6 @@ class NewCallActivity : PassphraseRequiredActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
val navigateBack = onBackPressedDispatcher::onBackPressed
|
||||
|
||||
@@ -8,8 +8,6 @@ package org.thoughtcrime.securesms.chats
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -124,8 +122,6 @@ private fun MessageDetailsEntry(route: MainNavigationDetailLocation.Chats.Messag
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -152,8 +148,6 @@ private fun ConversationSettingsEntry(route: MainNavigationDetailLocation.Chats.
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
) { fragment ->
|
||||
backPressedState.attach(fragment)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Hosts [ClockSkewScreen], the full-screen blocking screen shown when the local device clock is too far out of sync
|
||||
* with the server's clock (see [ClockSkewDetector]). The user cannot leave until they fix their clock; the screen
|
||||
* dismisses itself once the skew is resolved (which is re-checked when the app is backgrounded/foregrounded).
|
||||
*/
|
||||
class ClockSkewActivity : PassphraseRequiredActivity() {
|
||||
|
||||
private val theme = DynamicNoActionBarTheme()
|
||||
private val viewModel: ClockSkewViewModel by viewModel { ClockSkewViewModel() }
|
||||
|
||||
override fun onPreCreate() {
|
||||
theme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Disabled: the user must fix their clock.
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.actions.collect { action ->
|
||||
when (action) {
|
||||
ClockSkewScreenAction.OpenDateSettings -> startActivity(Intent(Settings.ACTION_DATE_SETTINGS))
|
||||
ClockSkewScreenAction.Finish -> finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
SignalTheme {
|
||||
ClockSkewScreen(
|
||||
state = state,
|
||||
onEvent = viewModel::onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
theme.onResume(this)
|
||||
viewModel.onEvent(ClockSkewScreenEvent.ScreenResumed)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createIntent(context: Context): Intent {
|
||||
return Intent(context, ClockSkewActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.AppForegroundObserver
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Detects when the local device clock is too far out of sync with the server's clock and holds that state in memory.
|
||||
*
|
||||
* We learn the server's true time from two sources: the websocket ([org.signal.libsignal.net.ChatConnectionListener.onServerTimestamp],
|
||||
* routed through [org.thoughtcrime.securesms.net.SignalWebSocketHealthMonitor]) and the remote config fetch. Because we
|
||||
* hold the websocket open aggressively, a user can change their clock while already connected — in which case we won't
|
||||
* receive a fresh server time. To catch that, we cache the last-known server time alongside a monotonic
|
||||
* [SystemClock.elapsedRealtime] reading and re-check on foreground, estimating the current server time without needing
|
||||
* another network round-trip.
|
||||
*
|
||||
* When skew is detected we stop trying to keep the websocket open (so we don't reconnect in a loop) regardless of
|
||||
* whether the app is foregrounded, and — only while foregrounded — show [ClockSkewActivity]. The detection state is
|
||||
* intentionally not persisted and is re-evaluated via [recheck] whenever the app is backgrounded or foregrounded, so a
|
||||
* skew that has since been corrected clears itself and the user always gets a fresh attempt.
|
||||
*
|
||||
* Note that the monotonic reading is deliberately kept in memory only: it is meaningless across reboots, but a reboot
|
||||
* kills our process anyway, so a live cache is always from the current boot.
|
||||
*/
|
||||
object ClockSkewDetector {
|
||||
|
||||
private val TAG = Log.tag(ClockSkewDetector::class)
|
||||
|
||||
private val _detected = MutableStateFlow(false)
|
||||
val detected: StateFlow<Boolean> = _detected.asStateFlow()
|
||||
|
||||
val isDetected: Boolean
|
||||
get() = _detected.value
|
||||
|
||||
/** The amount our local clock was off from the server's when skew was detected. [Duration.ZERO] when not detected. */
|
||||
@Volatile
|
||||
var skew: Duration = Duration.ZERO
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
private var lastServerTime: Long = 0
|
||||
|
||||
@Volatile
|
||||
private var lastServerTimeElapsedRealtime: Long = 0
|
||||
|
||||
private val allowedSkew: Duration
|
||||
get() = RemoteConfig.maxAllowedClockSkewSeconds.seconds
|
||||
|
||||
/**
|
||||
* Records a freshly-observed server time (both persisting it and caching it for [recheck]) and immediately checks it
|
||||
* against our local clock.
|
||||
*/
|
||||
fun onServerTimeReceived(serverTime: Long) {
|
||||
lastServerTime = serverTime
|
||||
lastServerTimeElapsedRealtime = SystemClock.elapsedRealtime()
|
||||
SignalStore.misc.setLastKnownServerTime(serverTime, System.currentTimeMillis())
|
||||
|
||||
val skew = skewFrom(serverTime)
|
||||
if (skew > allowedSkew) {
|
||||
Log.w(TAG, "Local clock is off from the server by $skew, which exceeds the allowed limit. Blocking.", true)
|
||||
markDetected(skew)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-evaluates clock skew using our cached server time and a monotonic estimate of elapsed time, without needing a
|
||||
* network round-trip. Intended to be called when the app is foregrounded, to catch the case where the user changed
|
||||
* their clock while we were already connected. Clears the detection state if the clock now looks fine.
|
||||
*/
|
||||
fun recheck() {
|
||||
if (lastServerTimeElapsedRealtime == 0L) {
|
||||
reset()
|
||||
return
|
||||
}
|
||||
|
||||
val estimatedServerTime = lastServerTime + (SystemClock.elapsedRealtime() - lastServerTimeElapsedRealtime)
|
||||
val skew = skewFrom(estimatedServerTime)
|
||||
if (skew > allowedSkew) {
|
||||
Log.w(TAG, "Local clock is off from the estimated server time by $skew, which exceeds the allowed limit. Blocking.", true)
|
||||
markDetected(skew)
|
||||
} else {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears any detected skew, allowing the websocket to reconnect and re-check. */
|
||||
private fun reset() {
|
||||
skew = Duration.ZERO
|
||||
_detected.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins observing the detection state and launches [ClockSkewActivity] whenever clock skew is detected while the app
|
||||
* is foregrounded. Skew can also be detected while backgrounded (which still blocks the websocket), but we only bring
|
||||
* up the blocking screen when foregrounded, to avoid a background activity launch. Should be called once during app
|
||||
* startup.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun beginObserving(application: Application) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
detected.collect { detected ->
|
||||
if (detected && AppForegroundObserver.isForegrounded()) {
|
||||
application.startActivity(ClockSkewActivity.createIntent(application).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markDetected(skew: Duration) {
|
||||
this.skew = skew
|
||||
_detected.value = true
|
||||
}
|
||||
|
||||
private fun skewFrom(serverTime: Long): Duration {
|
||||
return abs(System.currentTimeMillis() - serverTime).milliseconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Full-screen blocking screen shown when we've detected that the local device clock is too far out of sync with the
|
||||
* server's clock (see [ClockSkewDetector]).
|
||||
*/
|
||||
@Composable
|
||||
fun ClockSkewScreen(
|
||||
state: ClockSkewState,
|
||||
onEvent: (ClockSkewScreenEvent) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(modifier = modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.displayCutoutPadding()
|
||||
.padding(horizontal = 32.dp, vertical = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ClockSkewActivity__title),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = maxHeight)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(150.dp)
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_recent_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ClockSkewActivity__date_inaccurate_description),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.ClockSkewActivity__time_prefix, state.deviceDateTime),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = { onEvent(ClockSkewScreenEvent.AdjustDateSelected) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(R.string.ClockSkewActivity__adjust_date_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun ClockSkewScreenPreview() {
|
||||
Previews.Preview {
|
||||
ClockSkewScreen(
|
||||
state = ClockSkewState(deviceDateTime = "12/17/25, 11:26 PM\n(Greenwich Mean Time)"),
|
||||
onEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
/**
|
||||
* One-shot side effects emitted by [ClockSkewViewModel] for the host [ClockSkewActivity] to perform, since they require
|
||||
* an Activity context.
|
||||
*/
|
||||
sealed interface ClockSkewScreenAction {
|
||||
/** Open the system date and time settings. */
|
||||
data object OpenDateSettings : ClockSkewScreenAction
|
||||
|
||||
/** Skew has been resolved; the blocking screen should close. */
|
||||
data object Finish : ClockSkewScreenAction
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
/**
|
||||
* Events emitted by [ClockSkewScreen] and handled by [ClockSkewViewModel].
|
||||
*/
|
||||
sealed interface ClockSkewScreenEvent {
|
||||
/** The screen became visible; the displayed device time should be recomputed. */
|
||||
data object ScreenResumed : ClockSkewScreenEvent
|
||||
|
||||
/** The user tapped the button to adjust their device date. */
|
||||
data object AdjustDateSelected : ClockSkewScreenEvent
|
||||
|
||||
/** Internal: the [ClockSkewDetector]'s detection state changed. */
|
||||
data class SkewStateChanged(val detected: Boolean) : ClockSkewScreenEvent
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
/**
|
||||
* View state for [ClockSkewScreen].
|
||||
*/
|
||||
data class ClockSkewState(
|
||||
/** The current device date and time, formatted for display (including the time zone). */
|
||||
val deviceDateTime: String = ""
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.clockskew
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import org.signal.core.ui.compose.EventDrivenViewModel
|
||||
import org.signal.core.util.logging.Log
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
class ClockSkewViewModel(
|
||||
private val clock: () -> Long = { System.currentTimeMillis() },
|
||||
detected: StateFlow<Boolean> = ClockSkewDetector.detected
|
||||
) : EventDrivenViewModel<ClockSkewScreenEvent>(TAG) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ClockSkewViewModel::class)
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(ClockSkewState(deviceDateTime = formatDeviceDateTime()))
|
||||
val state: StateFlow<ClockSkewState> = _state.asStateFlow()
|
||||
|
||||
private val _actions = Channel<ClockSkewScreenAction>(Channel.UNLIMITED)
|
||||
val actions: Flow<ClockSkewScreenAction> = _actions.receiveAsFlow()
|
||||
|
||||
init {
|
||||
detected
|
||||
.onEach { onEvent(ClockSkewScreenEvent.SkewStateChanged(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override suspend fun processEvent(event: ClockSkewScreenEvent) {
|
||||
applyEvent(_state.value, event, { _actions.trySend(it) }) { _state.value = it }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun applyEvent(
|
||||
state: ClockSkewState,
|
||||
event: ClockSkewScreenEvent,
|
||||
actionEmitter: (ClockSkewScreenAction) -> Unit,
|
||||
stateEmitter: (ClockSkewState) -> Unit
|
||||
) {
|
||||
when (event) {
|
||||
ClockSkewScreenEvent.ScreenResumed -> stateEmitter(state.copy(deviceDateTime = formatDeviceDateTime()))
|
||||
ClockSkewScreenEvent.AdjustDateSelected -> actionEmitter(ClockSkewScreenAction.OpenDateSettings)
|
||||
is ClockSkewScreenEvent.SkewStateChanged -> if (!event.detected) actionEmitter(ClockSkewScreenAction.Finish)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDeviceDateTime(): String {
|
||||
val dateTime = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(Date(clock()))
|
||||
val timeZone = TimeZone.getDefault().getDisplayName(false, TimeZone.LONG)
|
||||
return "$dateTime\n($timeZone)"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -12,7 +13,9 @@ import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.signal.core.ui.WindowExtensionsKt;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
/**
|
||||
@@ -30,6 +33,15 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
if (dialog.getWindow() != null) {
|
||||
WindowExtensionsKt.enableEdgeToEdge(dialog.getWindow());
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
|
||||
@@ -41,6 +53,8 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
|
||||
|
||||
SystemWindowInsetsSetter.attach(view, getViewLifecycleOwner());
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
+65
-105
@@ -17,40 +17,37 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.content.res.Configuration;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.signal.core.util.ServiceUtil;
|
||||
import org.signal.core.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* LinearLayout that, when a view container, will report back when it thinks a soft keyboard
|
||||
* has been opened and what its height would be.
|
||||
* has been opened and what its height would be. Driven by {@link WindowInsetsCompat.Type#ime()}
|
||||
* insets, so it requires an edge-to-edge host window (all activity and dialog windows in the app
|
||||
* are edge-to-edge). The reported height excludes the navigation bar, matching what a custom
|
||||
* keyboard sitting above a navigation-bar-padded container should be sized to.
|
||||
*/
|
||||
public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
private static final String TAG = Log.tag(KeyboardAwareLinearLayout.class);
|
||||
|
||||
private static final long KEYBOARD_DEBOUNCE = 150;
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
|
||||
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
|
||||
private final DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
|
||||
private final int minKeyboardSize;
|
||||
private final int minCustomKeyboardSize;
|
||||
@@ -58,14 +55,14 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
private final int minCustomKeyboardTopMarginPortrait;
|
||||
private final int minCustomKeyboardTopMarginLandscape;
|
||||
private final int minCustomKeyboardTopMarginLandscapeBubble;
|
||||
private final int statusBarHeight;
|
||||
|
||||
private int viewInset;
|
||||
|
||||
private boolean keyboardOpen = false;
|
||||
private int rotation = 0;
|
||||
private boolean isBubble = false;
|
||||
private long openedAt = 0;
|
||||
private int lastKeyboardHeight;
|
||||
private boolean lastKeyboardVisible;
|
||||
|
||||
private InsetPaddingMode insetPaddingMode = InsetPaddingMode.NONE;
|
||||
|
||||
public KeyboardAwareLinearLayout(Context context) {
|
||||
this(context, null);
|
||||
@@ -83,48 +80,71 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
|
||||
minCustomKeyboardTopMarginLandscapeBubble = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_landscape_bubble);
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
viewInset = getViewInset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
updateRotation();
|
||||
updateKeyboardState();
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this, (view, insets) -> {
|
||||
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
|
||||
Insets nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
|
||||
Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
lastKeyboardHeight = Math.max(0, ime.bottom - nav.bottom);
|
||||
lastKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
|
||||
|
||||
switch (insetPaddingMode) {
|
||||
case NONE:
|
||||
break;
|
||||
case KEYBOARD:
|
||||
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), lastKeyboardVisible ? lastKeyboardHeight : 0);
|
||||
break;
|
||||
case KEYBOARD_AND_NAVIGATION_BAR:
|
||||
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), Math.max(nav.bottom, ime.bottom));
|
||||
break;
|
||||
case SAFE_AREA:
|
||||
setPadding(bars.left, bars.top, bars.right, Math.max(bars.bottom, ime.bottom));
|
||||
break;
|
||||
}
|
||||
|
||||
updateKeyboardState();
|
||||
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
|
||||
public void setIsBubble(boolean isBubble) {
|
||||
this.isBubble = isBubble;
|
||||
}
|
||||
|
||||
private void updateRotation() {
|
||||
int oldRotation = rotation;
|
||||
rotation = getDeviceRotation();
|
||||
if (oldRotation != rotation) {
|
||||
Log.i(TAG, "rotation changed");
|
||||
onKeyboardClose();
|
||||
}
|
||||
/**
|
||||
* Opt-in self-padding, replacing the window resize behavior these layouts relied on before the
|
||||
* host windows went edge-to-edge. [InsetPaddingMode.NONE] keeps the layout as a pure keyboard
|
||||
* event source (e.g. when a parent container already manages insets).
|
||||
*/
|
||||
public void setInsetPaddingMode(@NonNull InsetPaddingMode mode) {
|
||||
this.insetPaddingMode = mode;
|
||||
ViewCompat.requestApplyInsets(this);
|
||||
}
|
||||
|
||||
public enum InsetPaddingMode {
|
||||
/** Apply no padding; the layout only reports keyboard events. */
|
||||
NONE,
|
||||
/** Pad the bottom by the keyboard height above the navigation bar (use inside a container that already clears the navigation bar). */
|
||||
KEYBOARD,
|
||||
/** Pad the bottom by the keyboard or navigation bar, whichever is larger. */
|
||||
KEYBOARD_AND_NAVIGATION_BAR,
|
||||
/** Pad all sides by the system bars, and the bottom by the keyboard when it is taller. */
|
||||
SAFE_AREA
|
||||
}
|
||||
|
||||
private void updateKeyboardState() {
|
||||
if (viewInset == 0) viewInset = getViewInset();
|
||||
|
||||
getWindowVisibleDisplayFrame(rect);
|
||||
|
||||
final int availableHeight = getAvailableHeight();
|
||||
final int keyboardHeight = availableHeight - rect.bottom;
|
||||
|
||||
if (keyboardHeight > minKeyboardSize) {
|
||||
if (getKeyboardHeight() != keyboardHeight) {
|
||||
if (lastKeyboardVisible && lastKeyboardHeight > minKeyboardSize) {
|
||||
if (getKeyboardHeight() != lastKeyboardHeight) {
|
||||
if (isLandscape()) {
|
||||
setKeyboardLandscapeHeight(keyboardHeight);
|
||||
setKeyboardLandscapeHeight(lastKeyboardHeight);
|
||||
} else {
|
||||
setKeyboardPortraitHeight(keyboardHeight);
|
||||
setKeyboardPortraitHeight(lastKeyboardHeight);
|
||||
}
|
||||
}
|
||||
if (!keyboardOpen) {
|
||||
onKeyboardOpen(keyboardHeight);
|
||||
onKeyboardOpen(lastKeyboardHeight);
|
||||
}
|
||||
} else if (keyboardOpen) {
|
||||
onKeyboardClose();
|
||||
@@ -134,53 +154,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
rotation = getDeviceRotation();
|
||||
if (getRootWindowInsets() != null) {
|
||||
int bottomInset;
|
||||
WindowInsets windowInsets = getRootWindowInsets();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
bottomInset = windowInsets.getInsets(WindowInsets.Type.navigationBars()).bottom;
|
||||
} else {
|
||||
bottomInset = windowInsets.getStableInsetBottom();
|
||||
}
|
||||
|
||||
if (bottomInset != 0 && (viewInset == 0 || viewInset == statusBarHeight)) {
|
||||
Log.i(TAG, "Updating view inset based on WindowInsets. viewInset: " + viewInset + " windowInset: " + bottomInset);
|
||||
viewInset = bottomInset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getViewInset() {
|
||||
try {
|
||||
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
|
||||
attachInfoField.setAccessible(true);
|
||||
Object attachInfo = attachInfoField.get(this);
|
||||
if (attachInfo != null) {
|
||||
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
|
||||
stableInsetsField.setAccessible(true);
|
||||
Rect insets = (Rect) stableInsetsField.get(attachInfo);
|
||||
if (insets != null) {
|
||||
return insets.bottom;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
// Do nothing
|
||||
}
|
||||
return statusBarHeight;
|
||||
}
|
||||
|
||||
private int getAvailableHeight() {
|
||||
final int availableHeight = this.getRootView().getHeight() - viewInset;
|
||||
final int availableWidth = this.getRootView().getWidth();
|
||||
|
||||
if (isLandscape() && availableHeight > availableWidth) {
|
||||
//noinspection SuspiciousNameCombination
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
return availableHeight;
|
||||
ViewCompat.requestApplyInsets(this);
|
||||
}
|
||||
|
||||
protected void onKeyboardOpen(int keyboardHeight) {
|
||||
@@ -213,21 +187,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
||||
}
|
||||
|
||||
public boolean isLandscape() {
|
||||
int rotation = getDeviceRotation();
|
||||
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
|
||||
}
|
||||
|
||||
private int getDeviceRotation() {
|
||||
if (isInEditMode()) {
|
||||
return Surface.ROTATION_0;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
getContext().getDisplay().getRealMetrics(displayMetrics);
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics);
|
||||
}
|
||||
return displayMetrics.widthPixels > displayMetrics.heightPixels ? Surface.ROTATION_90 : Surface.ROTATION_0;
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
private int getKeyboardLandscapeHeight() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.enableEdgeToEdge
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,7 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
}
|
||||
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
dialog.window?.enableEdgeToEdge()
|
||||
|
||||
return dialog
|
||||
}
|
||||
@@ -47,6 +49,7 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
return if (view is KeyboardAwareLinearLayout) {
|
||||
view.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.SAFE_AREA)
|
||||
view.addOnKeyboardShownListener(this)
|
||||
view.addOnKeyboardHiddenListener(this)
|
||||
view
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.bumptech.glide.RequestManager;
|
||||
|
||||
@@ -207,6 +208,27 @@ public class LinkPreviewView extends FrameLayout {
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
}
|
||||
|
||||
boolean thumbnailVisible = (showThumbnail && linkPreview.getThumbnail().isPresent()) || callLinkRootKey != null;
|
||||
alignTitleWithThumbnail(thumbnailVisible);
|
||||
}
|
||||
|
||||
private void alignTitleWithThumbnail(boolean thumbnailVisible) {
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) title.getLayoutParams();
|
||||
if (thumbnailVisible) {
|
||||
params.topToTop = R.id.linkpreview_thumbnail;
|
||||
params.bottomToBottom = R.id.linkpreview_thumbnail;
|
||||
params.startToEnd = R.id.linkpreview_thumbnail;
|
||||
params.startToStart = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.setMarginStart(ViewUtil.dpToPx(8));
|
||||
} else {
|
||||
params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
|
||||
params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
||||
params.setMarginStart(0);
|
||||
}
|
||||
title.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public void setCorners(int topStart, int topEnd) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.android.material.shape.CornerFamily;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
|
||||
import org.signal.core.ui.fonts.SignalSymbols;
|
||||
import org.signal.core.ui.view.Stub;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.Util;
|
||||
@@ -37,7 +38,6 @@ import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.SenderNameWithLabelView;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols;
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.signal.core.ui.enableEdgeToEdge
|
||||
import org.signal.core.ui.initializeScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
@@ -41,6 +43,12 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
window?.enableEdgeToEdge()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class EmojiUtil {
|
||||
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
|
||||
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]++)+$");
|
||||
private static final String EMOJI_REGEX = "[^\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\p{Cf}\\p{Cs}\\s]";
|
||||
|
||||
private EmojiUtil() {}
|
||||
|
||||
+25
@@ -1,7 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
@@ -9,6 +14,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* The DSL API can be completely replaced by compose.
|
||||
@@ -22,6 +28,8 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
enableSettingsEdgeToEdge()
|
||||
|
||||
setContentView(R.layout.dsl_settings_activity)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
@@ -62,6 +70,23 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableSettingsEdgeToEdge() {
|
||||
val navBarColor = ContextCompat.getColor(this, CoreUiR.color.signal_colorSurface2)
|
||||
val isDark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
enableEdgeToEdge(
|
||||
navigationBarStyle = if (isDark) {
|
||||
SystemBarStyle.dark(navBarColor)
|
||||
} else {
|
||||
SystemBarStyle.light(navBarColor, navBarColor)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
enableEdgeToEdge()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onWillFinish() {}
|
||||
|
||||
protected open fun resolveNavGraphId(): Int = intent.getIntExtra(ARG_NAV_GRAPH, -1)
|
||||
|
||||
+41
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
@@ -11,12 +12,17 @@ import androidx.annotation.MenuRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.ui.logging.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavHostFragment
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
/**
|
||||
@@ -70,6 +76,8 @@ abstract class DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
applyEdgeToEdgeInsets()
|
||||
|
||||
when (settingsAdapter) {
|
||||
is ConcatAdapter -> bindAdapters(settingsAdapter)
|
||||
is MappingAdapter -> bindAdapter(settingsAdapter)
|
||||
@@ -77,6 +85,39 @@ abstract class DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All host windows are edge-to-edge — activities centrally via [org.thoughtcrime.securesms.BaseActivity],
|
||||
* and the full-screen dialog hosts ([org.thoughtcrime.securesms.components.WrapperDialogFragment],
|
||||
* [org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment]) via their own
|
||||
* `enableEdgeToEdge` calls — so the toolbar tint must extend behind the (transparent) status bar and the
|
||||
* list must scroll clear of the navigation bar. Skipped only for fragments embedded directly in
|
||||
* [MainActivity]'s Compose scaffolding, which insets those containers itself — except the
|
||||
* conversation-settings detail entry ([ConversationSettingsNavHostFragment]), which leaves insets to these
|
||||
* fragments so the toolbar scroll tint can reach behind the status bar.
|
||||
*/
|
||||
private fun applyEdgeToEdgeInsets() {
|
||||
if (isInsetByMainActivityHost()) {
|
||||
return
|
||||
}
|
||||
|
||||
toolbar?.let { toolbar ->
|
||||
toolbar.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
|
||||
SystemWindowInsetsSetter.attach(toolbar, viewLifecycleOwner, WindowInsetsCompat.Type.statusBars())
|
||||
}
|
||||
|
||||
recyclerView?.let { recycler ->
|
||||
recycler.clipToPadding = false
|
||||
SystemWindowInsetsSetter.attach(recycler, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInsetByMainActivityHost(): Boolean {
|
||||
if (activity !is MainActivity) {
|
||||
return false
|
||||
}
|
||||
return generateSequence(parentFragment) { it.parentFragment }.none { it is ConversationSettingsNavHostFragment }
|
||||
}
|
||||
|
||||
open fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
|
||||
if (toolbar == null) {
|
||||
return null
|
||||
|
||||
+1
-4
@@ -74,7 +74,6 @@ import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRout
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
@@ -219,9 +218,7 @@ private fun AppSettingsContent(
|
||||
) {
|
||||
bannerManager.Banner()
|
||||
|
||||
LazyColumn(
|
||||
modifier = rememberStatusBarColorNestedScrollModifier()
|
||||
) {
|
||||
LazyColumn {
|
||||
item {
|
||||
BioRow(
|
||||
self = self,
|
||||
|
||||
+1
-4
@@ -47,7 +47,6 @@ import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -265,9 +264,7 @@ fun AccountSettingsScreen(
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
.testTag(AccountSettingsTestTags.SCROLLER)
|
||||
.padding(contentPadding).testTag(AccountSettingsTestTags.SCROLLER)
|
||||
) {
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
|
||||
-2
@@ -41,7 +41,6 @@ import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.account.LinkedDeviceAccountSettingsState.OneTimeEvent
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
|
||||
/**
|
||||
@@ -92,7 +91,6 @@ private fun LinkedDeviceAccountSettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
LinkedDeviceCallout(onLearnMoreClick = { onEvent(LinkedDeviceAccountSettingsEvent.LearnMoreClicked) })
|
||||
|
||||
-2
@@ -24,7 +24,6 @@ import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.appearance.navbar.ChooseNavigationBarStyleFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -114,7 +113,6 @@ private fun AppearanceSettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.RadioListRow(
|
||||
|
||||
+2
-16
@@ -106,7 +106,6 @@ import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentic
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
@@ -151,8 +150,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
backupRestoreState = restoreState,
|
||||
backupDeleteState = deleteState,
|
||||
contentCallbacks = callbacks,
|
||||
backupProgress = backupProgress,
|
||||
statusBarColorNestedScrollConnection = remember { StatusBarColorNestedScrollConnection(requireActivity()) }
|
||||
backupProgress = backupProgress
|
||||
)
|
||||
}
|
||||
|
||||
@@ -360,8 +358,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
backupRestoreState: BackupRestoreState,
|
||||
backupDeleteState: DeletionState,
|
||||
contentCallbacks: ContentCallbacks,
|
||||
backupProgress: ArchiveUploadProgressState?,
|
||||
statusBarColorNestedScrollConnection: StatusBarColorNestedScrollConnection?
|
||||
backupProgress: ArchiveUploadProgressState?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val biometrics = rememberBiometricsAuthentication(
|
||||
@@ -402,13 +399,6 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (statusBarColorNestedScrollConnection != null) {
|
||||
Modifier.nestedScroll(statusBarColorNestedScrollConnection)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
snackbarHost = {
|
||||
Snackbars.Host(snackbarHostState = snackbarHostState)
|
||||
@@ -1885,7 +1875,6 @@ private fun RemoteBackupsSettingsContentPreview() {
|
||||
hasRedemptionError = true,
|
||||
isOutOfStorageSpace = true
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
@@ -1915,7 +1904,6 @@ private fun RemoteBackupsSettingsInternalUserContentPreview() {
|
||||
internalUser = true,
|
||||
includeDebuglog = true
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
@@ -1948,7 +1936,6 @@ private fun RemoteBackupsSettingsLinkedDevicePaidContentPreview() {
|
||||
renewalTime = 1_752_710_400.seconds
|
||||
)
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
@@ -1973,7 +1960,6 @@ private fun RemoteBackupsSettingsLinkedDeviceNotFoundContentPreview() {
|
||||
backupMediaSize = 2300000,
|
||||
backupState = BackupState.NotFound
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.SignalE164Util
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
|
||||
|
||||
/**
|
||||
* A captive activity that can determine if an interrupted/erred change number request
|
||||
@@ -58,6 +60,8 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
SystemWindowInsetsSetter.attach(findViewById(R.id.change_number_lock_scroll), this, WindowInsetsCompat.Type.systemBars())
|
||||
|
||||
reattemptChange()
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.isIdle
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreationProgressRow
|
||||
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -162,7 +161,6 @@ private fun ChatsSettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
|
||||
-2
@@ -27,7 +27,6 @@ import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.mediasend.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||
@@ -135,7 +134,6 @@ private fun DataAndStorageSettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
|
||||
+54
-13
@@ -25,7 +25,6 @@ import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.permissions.PermissionDeniedBottomSheet
|
||||
import org.signal.core.ui.permissions.RationaleDialog
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.Util
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
@@ -777,6 +776,50 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Set Video Config:"),
|
||||
isChecked = state.callingSetVideoConfig,
|
||||
onClick = {
|
||||
viewModel.setInternalCallingSetVideoConfig(!state.callingSetVideoConfig)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(" Use Hardware Vp9 Encode"),
|
||||
isChecked = state.callingUseHardwareVp9Encode,
|
||||
isEnabled = state.callingSetVideoConfig,
|
||||
onClick = {
|
||||
viewModel.setInternalCallingUseHardwareVp9Encode(!state.callingUseHardwareVp9Encode)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(" Use Hardware Vp9 Decode"),
|
||||
isChecked = state.callingUseHardwareVp9Decode,
|
||||
isEnabled = state.callingSetVideoConfig,
|
||||
onClick = {
|
||||
viewModel.setInternalCallingUseHardwareVp9Decode(!state.callingUseHardwareVp9Decode)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(" Use Software Vp9 Encode"),
|
||||
isChecked = state.callingUseSoftwareVp9Encode,
|
||||
isEnabled = state.callingSetVideoConfig,
|
||||
onClick = {
|
||||
viewModel.setInternalCallingUseSoftwareVp9Encode(!state.callingUseSoftwareVp9Encode)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(" Use Software Vp9 Decode"),
|
||||
isChecked = state.callingUseSoftwareVp9Decode,
|
||||
isEnabled = state.callingSetVideoConfig,
|
||||
onClick = {
|
||||
viewModel.setInternalCallingUseSoftwareVp9Decode(!state.callingUseSoftwareVp9Decode)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
// TODO [alex] -- db access on main thread!
|
||||
@@ -1055,19 +1098,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
.setTitle("Unregister?")
|
||||
.setMessage("Are you sure? You'll have to re-register to use Signal again -- no promises that the process will go smoothly.")
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages {
|
||||
ThreadUtil.runOnMain {
|
||||
when (it) {
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||
SignalStore.account.setRegistered(false)
|
||||
SignalStore.registration.clearRegistrationComplete()
|
||||
SignalStore.registration.hasUploadedProfile = false
|
||||
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
when (AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages()) {
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||
SignalStore.account.setRegistered(false)
|
||||
SignalStore.registration.clearRegistrationComplete()
|
||||
SignalStore.registration.hasUploadedProfile = false
|
||||
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
|
||||
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
|
||||
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -19,6 +19,11 @@ data class InternalSettingsState(
|
||||
val callingUseSoftwareNs: Boolean,
|
||||
val callingUseInputLowLatency: Boolean,
|
||||
val callingUseInputVoiceComm: Boolean,
|
||||
val callingSetVideoConfig: Boolean,
|
||||
val callingUseHardwareVp9Encode: Boolean,
|
||||
val callingUseHardwareVp9Decode: Boolean,
|
||||
val callingUseSoftwareVp9Encode: Boolean,
|
||||
val callingUseSoftwareVp9Decode: Boolean,
|
||||
val useBuiltInEmojiSet: Boolean,
|
||||
val emojiVersion: EmojiFiles.Version?,
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
|
||||
+30
@@ -142,6 +142,31 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalCallingSetVideoConfig(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.CALLING_SET_VIDEO_CONFIG, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalCallingUseHardwareVp9Encode(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_HARDWARE_VP9_ENCODE, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalCallingUseHardwareVp9Decode(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_HARDWARE_VP9_DECODE, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalCallingUseSoftwareVp9Encode(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_VP9_ENCODE, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setInternalCallingUseSoftwareVp9Decode(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_VP9_DECODE, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setUseConversationItemV2Media(enabled: Boolean) {
|
||||
SignalStore.internal.useConversationItemV2Media = enabled
|
||||
refresh()
|
||||
@@ -228,6 +253,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
callingUseSoftwareNs = SignalStore.internal.callingUseSoftwareNs,
|
||||
callingUseInputLowLatency = SignalStore.internal.callingUseInputLowLatency,
|
||||
callingUseInputVoiceComm = SignalStore.internal.callingUseInputVoiceComm,
|
||||
callingSetVideoConfig = SignalStore.internal.callingSetVideoConfig,
|
||||
callingUseHardwareVp9Encode = SignalStore.internal.callingUseHardwareVp9Encode,
|
||||
callingUseHardwareVp9Decode = SignalStore.internal.callingUseHardwareVp9Decode,
|
||||
callingUseSoftwareVp9Encode = SignalStore.internal.callingUseSoftwareVp9Encode,
|
||||
callingUseSoftwareVp9Decode = SignalStore.internal.callingUseSoftwareVp9Decode,
|
||||
useBuiltInEmojiSet = SignalStore.internal.forceBuiltInEmoji,
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internal.removeSenderKeyMinimum,
|
||||
|
||||
+6
@@ -324,6 +324,12 @@ private fun StorageRecordRow(record: SignalStorageRecord) {
|
||||
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||
}
|
||||
}
|
||||
record.proto.stickerPack != null -> {
|
||||
Column {
|
||||
Text("Sticker Pack", fontWeight = FontWeight.Bold)
|
||||
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Column {
|
||||
Text("Unknown!")
|
||||
|
||||
+1
@@ -13,4 +13,5 @@ sealed interface LabsSettingsEvents {
|
||||
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleStickerReplies(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleMuteBreakthroughNotifications(val enabled: Boolean) : LabsSettingsEvents
|
||||
data class ToggleImprovedMessageDeletion(val enabled: Boolean) : LabsSettingsEvents
|
||||
}
|
||||
|
||||
+9
@@ -151,6 +151,15 @@ private fun LabsSettingsContent(
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleMuteBreakthroughNotifications(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = state.improvedMessageDeletion,
|
||||
text = "Improved message deletion",
|
||||
label = "Show a temporary tombstone when you delete a message, allowing you to promote it to a remote deletion.",
|
||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleImprovedMessageDeletion(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,5 +15,6 @@ data class LabsSettingsState(
|
||||
val betterSearch: Boolean = false,
|
||||
val starredMessages: Boolean = false,
|
||||
val stickerReplies: Boolean = false,
|
||||
val muteBreakthroughNotifications: Boolean = false
|
||||
val muteBreakthroughNotifications: Boolean = false,
|
||||
val improvedMessageDeletion: Boolean = false
|
||||
)
|
||||
|
||||
+6
-1
@@ -45,6 +45,10 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
SignalStore.labs.muteBreakthroughNotifications = event.enabled
|
||||
_state.value = _state.value.copy(muteBreakthroughNotifications = event.enabled)
|
||||
}
|
||||
is LabsSettingsEvents.ToggleImprovedMessageDeletion -> {
|
||||
SignalStore.labs.improvedMessageDeletion = event.enabled
|
||||
_state.value = _state.value.copy(improvedMessageDeletion = event.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +60,8 @@ class LabsSettingsViewModel : ViewModel() {
|
||||
betterSearch = SignalStore.labs.betterSearch,
|
||||
starredMessages = SignalStore.labs.starredMessages,
|
||||
stickerReplies = SignalStore.labs.stickerReplies,
|
||||
muteBreakthroughNotifications = SignalStore.labs.muteBreakthroughNotifications
|
||||
muteBreakthroughNotifications = SignalStore.labs.muteBreakthroughNotifications,
|
||||
improvedMessageDeletion = SignalStore.labs.improvedMessageDeletion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -39,7 +39,6 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersViewModel.NotificationProfileAndRecipients
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||
@@ -168,7 +167,6 @@ private fun AddAllowedMembersContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
|
||||
-2
@@ -43,7 +43,6 @@ import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
@@ -193,7 +192,6 @@ private fun AdvancedPrivacySettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
|
||||
+31
-25
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
|
||||
import android.content.Context
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.net.DeviceDeregisteredException
|
||||
import org.signal.libsignal.net.RequestResult
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
@@ -13,8 +17,6 @@ import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
@@ -22,30 +24,34 @@ private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
|
||||
|
||||
class AdvancedPrivacySettingsRepository(private val context: Context) {
|
||||
|
||||
fun disablePushMessages(consumer: (DisablePushMessagesResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val result = try {
|
||||
try {
|
||||
NetworkResultUtil.toBasicLegacy(SignalNetwork.account.clearFcmToken())
|
||||
} catch (e: AuthorizationFailedException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
if (SignalStore.account.fcmEnabled) {
|
||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||
}
|
||||
DisablePushMessagesResult.SUCCESS
|
||||
} catch (ioe: IOException) {
|
||||
Log.w(TAG, ioe)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted while deleting", e)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: ExecutionException) {
|
||||
Log.w(TAG, "Error deleting", e.cause)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
}
|
||||
suspend fun disablePushMessages(): DisablePushMessagesResult = withContext(Dispatchers.IO) {
|
||||
val clearTokenError: Throwable? = when (val result = SignalNetwork.account.clearFcmToken()) {
|
||||
is RequestResult.Success, is RequestResult.NonSuccess -> null
|
||||
is RequestResult.RetryableNetworkError -> result.networkError
|
||||
is RequestResult.ApplicationError -> result.cause
|
||||
}
|
||||
|
||||
consumer(result)
|
||||
if (clearTokenError != null) {
|
||||
Log.w(TAG, clearTokenError)
|
||||
if (clearTokenError !is DeviceDeregisteredException) {
|
||||
return@withContext DisablePushMessagesResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (SignalStore.account.fcmEnabled) {
|
||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||
}
|
||||
DisablePushMessagesResult.SUCCESS
|
||||
} catch (ioe: IOException) {
|
||||
Log.w(TAG, ioe)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Interrupted while deleting", e)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
} catch (e: ExecutionException) {
|
||||
Log.w(TAG, "Error deleting", e.cause)
|
||||
DisablePushMessagesResult.NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-18
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.pnp
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -14,7 +13,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -34,23 +32,11 @@ import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.ui.compose.showSnackbar
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: PhoneNumberPrivacySettingsViewModel by viewModels()
|
||||
private lateinit var statusBarNestedScrollConnection: StatusBarColorNestedScrollConnection
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
statusBarNestedScrollConnection = StatusBarColorNestedScrollConnection(requireActivity())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
statusBarNestedScrollConnection.setColorImmediate()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
@@ -62,7 +48,6 @@ class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
statusBarNestedScrollConnection = statusBarNestedScrollConnection,
|
||||
onEveryoneCanSeeMyNumberClicked = viewModel::setEveryoneCanSeeMyNumber,
|
||||
onNobodyCanSeeMyNumberClicked = viewModel::setNobodyCanSeeMyNumber,
|
||||
onEveryoneCanFindMeByNumberClicked = viewModel::setEveryoneCanFindMeByMyNumber,
|
||||
@@ -96,7 +81,6 @@ private fun Screen(
|
||||
state: PhoneNumberPrivacySettingsState,
|
||||
snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
onNavigationClick: () -> Unit = {},
|
||||
statusBarNestedScrollConnection: StatusBarColorNestedScrollConnection? = null,
|
||||
onEveryoneCanSeeMyNumberClicked: () -> Unit = {},
|
||||
onNobodyCanSeeMyNumberClicked: () -> Unit = {},
|
||||
onEveryoneCanFindMeByNumberClicked: () -> Unit = {},
|
||||
@@ -109,8 +93,7 @@ private fun Screen(
|
||||
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState)
|
||||
},
|
||||
modifier = statusBarNestedScrollConnection?.let { Modifier.nestedScroll(it) } ?: Modifier
|
||||
}
|
||||
) { contentPadding ->
|
||||
Box(modifier = Modifier.padding(contentPadding)) {
|
||||
LazyColumn {
|
||||
|
||||
+8
-6
@@ -334,12 +334,14 @@ private fun ManageStorageSettingsScreen(
|
||||
onClick = onSetChatLengthLimit
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_title),
|
||||
label = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_description),
|
||||
checked = state.syncTrimDeletes,
|
||||
onCheckChanged = onSyncTrimThreadDeletes
|
||||
)
|
||||
if (state.isPrimary) {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_title),
|
||||
label = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_description),
|
||||
checked = state.syncTrimDeletes,
|
||||
onCheckChanged = onSyncTrimThreadDeletes
|
||||
)
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
|
||||
+4
-2
@@ -36,7 +36,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
|
||||
keepMessagesDuration = SignalStore.settings.keepMessagesDuration,
|
||||
lengthLimit = if (SignalStore.settings.isTrimByLengthEnabled) SignalStore.settings.threadTrimLength else ManageStorageState.NO_LIMIT,
|
||||
syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes(),
|
||||
localBackupsEnabled = SignalStore.backup.newLocalBackupsEnabled
|
||||
localBackupsEnabled = SignalStore.backup.newLocalBackupsEnabled,
|
||||
isPrimary = SignalStore.account.isPrimaryDevice
|
||||
)
|
||||
)
|
||||
val state = store.asStateFlow()
|
||||
@@ -182,7 +183,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
|
||||
val onDeviceStorageOptimizationState: OnDeviceStorageOptimizationState = OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE,
|
||||
val storageOptimizationStateChanged: Boolean = false,
|
||||
val isPaidTierPending: Boolean = false,
|
||||
val localBackupsEnabled: Boolean = false
|
||||
val localBackupsEnabled: Boolean = false,
|
||||
val isPrimary: Boolean = true
|
||||
) {
|
||||
companion object {
|
||||
const val NO_LIMIT = 0
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ object RecurringInAppPaymentRepository {
|
||||
} ?: return NetworkResult.Success(ActiveSubscription.EMPTY)
|
||||
|
||||
response.result.ifPresent { result ->
|
||||
if (result.isActive && result.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) {
|
||||
val lastEndOfPeriod = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(type.inAppPaymentType)?.endOfPeriodSeconds ?: 0L
|
||||
if (result.isActive && result.activeSubscription.endOfCurrentPeriod > lastEndOfPeriod) {
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -115,8 +115,8 @@ class DonateToSignalFragment :
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(activity = requireActivity(), views = listOf(toolbar!!), lifecycleOwner = viewLifecycleOwner) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.c
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
@@ -33,6 +36,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.st
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
@@ -55,6 +59,10 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment), InAppPayment
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPaymentId)
|
||||
|
||||
binding.toolbar.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
|
||||
SystemWindowInsetsSetter.attach(binding.toolbar, viewLifecycleOwner, WindowInsetsCompat.Type.statusBars())
|
||||
SystemWindowInsetsSetter.attach(binding.continueButton, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars(), SystemWindowInsetsSetter.ApplyMode.MARGIN)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
if (result.status == InAppPaymentProcessorActionResult.Status.SUCCESS) {
|
||||
|
||||
+11
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
@@ -15,12 +16,14 @@ import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.enableEdgeToEdge
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationWebViewOnBackPressedCallback
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
@@ -49,8 +52,16 @@ class PayPalConfirmationDialogFragment : DialogFragment(R.layout.donation_webvie
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
window?.enableEdgeToEdge()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
SystemWindowInsetsSetter.attach(view, viewLifecycleOwner)
|
||||
|
||||
val client = PayPalWebClient()
|
||||
viewLifecycleOwner.lifecycle.addObserver(client)
|
||||
binding.webView.webViewClient = client
|
||||
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
@@ -24,6 +25,7 @@ import com.google.android.material.button.MaterialButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.enableEdgeToEdge
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
@@ -64,9 +67,16 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
window?.enableEdgeToEdge()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
SystemWindowInsetsSetter.attach(view, viewLifecycleOwner)
|
||||
|
||||
dialog!!.window!!.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
|
||||
|
||||
+4
@@ -9,9 +9,11 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -231,6 +233,8 @@ fun BankTransferDetailsContent(
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it)
|
||||
.imePadding()
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
+6
-1
@@ -9,8 +9,10 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -237,7 +239,10 @@ private fun IdealTransferDetailsContent(
|
||||
|
||||
Column(
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
modifier = Modifier.padding(it)
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it)
|
||||
.imePadding()
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
|
||||
+10
-27
@@ -5,9 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.mandate
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -60,7 +57,6 @@ import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.DonationTransferTestTags
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorAnimator
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -76,17 +72,6 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
BankTransferMandateViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private lateinit var statusBarColorAnimator: StatusBarColorAnimator
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
statusBarColorAnimator = StatusBarColorAnimator(requireActivity())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
statusBarColorAnimator.setColorImmediate()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val mandate by viewModel.mandate
|
||||
@@ -97,8 +82,7 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
failedToLoadMandate = failedToLoadMandate,
|
||||
onNavigationClick = this::onNavigationClick,
|
||||
onContinueClick = this::onContinueClick,
|
||||
onLearnMoreClick = this::onLearnMoreClick,
|
||||
onCanScrollUp = statusBarColorAnimator::setCanScrollUp
|
||||
onLearnMoreClick = this::onLearnMoreClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,22 +120,19 @@ fun BankTransferScreenPreview() {
|
||||
failedToLoadMandate = false,
|
||||
onNavigationClick = {},
|
||||
onContinueClick = {},
|
||||
onLearnMoreClick = {},
|
||||
onCanScrollUp = {}
|
||||
onLearnMoreClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun BankTransferScreen(
|
||||
bankMandate: String,
|
||||
failedToLoadMandate: Boolean,
|
||||
onNavigationClick: () -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onCanScrollUp: (Boolean) -> Unit
|
||||
onLearnMoreClick: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -182,17 +163,19 @@ fun BankTransferScreen(
|
||||
colors = if (listState.canScrollBackward) TopAppBarDefaults.topAppBarColors(containerColor = SignalTheme.colors.colorSurface2) else TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
}
|
||||
) {
|
||||
onCanScrollUp(listState.canScrollBackward)
|
||||
|
||||
Column(horizontalAlignment = CenterHorizontally, modifier = Modifier.fillMaxSize()) {
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, true)
|
||||
.padding(top = 64.dp)
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
|
||||
+2
-2
@@ -138,8 +138,8 @@ class ManageDonationsFragment :
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(activity = requireActivity(), views = listOf(toolbar!!), lifecycleOwner = viewLifecycleOwner) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -22,7 +22,6 @@ import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -92,7 +91,6 @@ private fun AppUpdatesSettingsScreen(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
item {
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@@ -29,7 +29,7 @@ class QrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragment.Callb
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.username_qr_image_selection_activity)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -67,6 +68,7 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
|
||||
|
||||
+2
-10
@@ -29,7 +29,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.isSplitPane
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.Result
|
||||
@@ -117,7 +116,6 @@ import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
@@ -274,16 +272,10 @@ class ConversationSettingsFragment :
|
||||
return object : Material3OnScrollHelper(
|
||||
activity = requireActivity(),
|
||||
views = listOf(toolbar!!),
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
setStatusBarColor = { color ->
|
||||
if (!resources.isSplitPane() || activity is ConversationSettingsActivity) {
|
||||
WindowUtil.setStatusBarColor(requireActivity().window, color)
|
||||
}
|
||||
}
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
) {
|
||||
override val inactiveColorSet = ColorSet(
|
||||
toolbarColorRes = CoreUiR.color.signal_colorBackground_0,
|
||||
statusBarColorRes = CoreUiR.color.signal_colorBackground
|
||||
toolbarColorRes = CoreUiR.color.signal_colorBackground_0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ class ConversationSettingsRepository(
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupRecord: GroupRecord = SignalDatabase.groups.getGroup(groupId).get()
|
||||
consumer(
|
||||
if (groupRecord.isV2Group) {
|
||||
if (groupRecord.hasV2GroupProperties) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembers
|
||||
.map { m -> m.serviceIdBytes }
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class PermissionsSettingsRepository(
|
||||
|
||||
fun hasNonAdminMembersWithLabels(groupId: GroupId): Boolean {
|
||||
val v2GroupId = groupId.v2OrNull() ?: return false
|
||||
val group = groupTable.getGroup(v2GroupId).orNull() ?: return false
|
||||
val group = groupTable.getGroup(v2GroupId).filter { it.hasV2GroupProperties }.orNull() ?: return false
|
||||
return group.requireV2GroupProperties().nonAdminMembersWithLabels().isNotEmpty()
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -6,10 +6,10 @@ import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import org.signal.core.ui.fonts.SignalSymbols
|
||||
import org.signal.core.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
||||
+1
-1
@@ -12,10 +12,10 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import org.signal.core.ui.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.PendingParticipantsListener
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
@@ -134,6 +134,7 @@ fun CallScreen(
|
||||
onRemoteMuteToastDismissed: () -> Unit = {},
|
||||
isSelfAdmin: Boolean = false,
|
||||
isCallLink: Boolean = false,
|
||||
canRemoteMute: Boolean = false,
|
||||
onMuteAudio: (CallParticipant) -> Unit = {},
|
||||
onRemoveFromCall: (CallParticipant) -> Unit = {},
|
||||
onContactDetails: (CallParticipant) -> Unit = {},
|
||||
@@ -383,6 +384,7 @@ fun CallScreen(
|
||||
participant = longPressedParticipant,
|
||||
isSelfAdmin = isSelfAdmin,
|
||||
isCallLink = isCallLink,
|
||||
canRemoteMute = canRemoteMute,
|
||||
onDismiss = { longPressedParticipantId = null },
|
||||
onMuteAudio = onMuteAudio,
|
||||
onRemoveFromCall = onRemoveFromCall,
|
||||
@@ -587,6 +589,7 @@ private fun ParticipantContextMenu(
|
||||
participant: CallParticipant?,
|
||||
isSelfAdmin: Boolean,
|
||||
isCallLink: Boolean,
|
||||
canRemoteMute: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onMuteAudio: (CallParticipant) -> Unit,
|
||||
onRemoveFromCall: (CallParticipant) -> Unit,
|
||||
@@ -620,7 +623,7 @@ private fun ParticipantContextMenu(
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
if (resolved.isMicrophoneEnabled) {
|
||||
if (canRemoteMute && resolved.isMicrophoneEnabled) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.CallParticipantSheet__mute_audio)) },
|
||||
leadingIcon = { Icon(painter = painterResource(R.drawable.symbol_mic_slash_24), contentDescription = null) },
|
||||
@@ -683,6 +686,7 @@ private fun ParticipantContextMenuAdminPreview() {
|
||||
),
|
||||
isSelfAdmin = true,
|
||||
isCallLink = true,
|
||||
canRemoteMute = true,
|
||||
onDismiss = {},
|
||||
onMuteAudio = {},
|
||||
onRemoveFromCall = {},
|
||||
@@ -705,6 +709,7 @@ private fun ParticipantContextMenuNonAdminPreview() {
|
||||
),
|
||||
isSelfAdmin = false,
|
||||
isCallLink = false,
|
||||
canRemoteMute = true,
|
||||
onDismiss = {},
|
||||
onMuteAudio = {},
|
||||
onRemoveFromCall = {},
|
||||
|
||||
+1
-4
@@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -76,9 +75,6 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
init {
|
||||
WindowUtil.clearTranslucentNavigationBar(activity.window)
|
||||
WindowUtil.clearTranslucentStatusBar(activity.window)
|
||||
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
|
||||
navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)
|
||||
@@ -241,6 +237,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
|
||||
callParticipantUpdatePopupController = callParticipantUpdatePopupController,
|
||||
isSelfAdmin = controlAndInfoState.isSelfAdmin(),
|
||||
isCallLink = controlAndInfoState.callLink != null,
|
||||
canRemoteMute = callParticipantsState.groupCallState.isNotIdle,
|
||||
onMuteAudio = callInfoCallbacks::onMuteAudio,
|
||||
onRemoveFromCall = callInfoCallbacks::onRemoveFromCall,
|
||||
onContactDetails = callInfoCallbacks::onContactDetails,
|
||||
|
||||
+1
-1
@@ -298,7 +298,7 @@ class WebRtcCallViewModel : ViewModel() {
|
||||
|
||||
@MainThread
|
||||
fun updateFromWebRtcViewModel(webRtcViewModel: WebRtcViewModel, enableVideo: Boolean) {
|
||||
canEnterPipMode.value = !webRtcViewModel.state.isPreJoinOrNetworkUnavailable
|
||||
canEnterPipMode.value = !webRtcViewModel.state.isPreJoinOrNetworkUnavailable && !webRtcViewModel.isLocalScreenSharing
|
||||
if (isCallStarting && webRtcViewModel.state.isPassedPreJoin) {
|
||||
isCallStarting = false
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.thoughtcrime.securesms.compose
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
/**
|
||||
* Controls status-bar color based off ability to scroll up
|
||||
*/
|
||||
class StatusBarColorAnimator(
|
||||
private val activity: Activity
|
||||
) {
|
||||
private var animator: ValueAnimator? = null
|
||||
private var previousCanScrollUp: Boolean = false
|
||||
|
||||
private val normalColor = ContextCompat.getColor(activity, CoreUiR.color.signal_colorBackground)
|
||||
private val scrollColor = ContextCompat.getColor(activity, CoreUiR.color.signal_colorSurface2)
|
||||
|
||||
fun setCanScrollUp(canScrollUp: Boolean) {
|
||||
if (previousCanScrollUp == canScrollUp) {
|
||||
return
|
||||
}
|
||||
|
||||
previousCanScrollUp = canScrollUp
|
||||
applyState(canScrollUp)
|
||||
}
|
||||
|
||||
fun setColorImmediate() {
|
||||
val end = when {
|
||||
previousCanScrollUp -> scrollColor
|
||||
else -> normalColor
|
||||
}
|
||||
|
||||
animator?.cancel()
|
||||
WindowUtil.setStatusBarColor(
|
||||
activity.window,
|
||||
end
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyState(canScrollUp: Boolean) {
|
||||
val (start, end) = when {
|
||||
canScrollUp -> normalColor to scrollColor
|
||||
else -> scrollColor to normalColor
|
||||
}
|
||||
|
||||
animator?.cancel()
|
||||
animator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 200
|
||||
addUpdateListener {
|
||||
WindowUtil.setStatusBarColor(
|
||||
activity.window,
|
||||
ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, start, end)
|
||||
)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user