mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-29 07:16:08 +00:00
Merge remote-tracking branch 'upstream/main' into feature/blur-image
# Conflicts: # app/src/main/assets/nostr_relays.csv
This commit is contained in:
commit
5beae6e6d5
1
.github/workflows/android-build.yml
vendored
1
.github/workflows/android-build.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Android CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main", "develop" ]
|
||||
pull_request:
|
||||
|
||||
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@ -38,17 +38,20 @@ jobs:
|
||||
- name: Grant execute permission for Gradlew
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build Release APK (with Tor)
|
||||
- name: Build Release APKs (with architecture splits)
|
||||
run: ./gradlew assembleRelease --no-daemon --stacktrace
|
||||
|
||||
- name: List APK files
|
||||
run: |
|
||||
echo "APK files built:"
|
||||
find app/build/outputs/apk -name "*.apk" -type f
|
||||
find app/build/outputs/apk/release -name "*.apk" -type f -exec ls -lh {} \;
|
||||
|
||||
- name: Rename APK
|
||||
- name: Rename APKs for GitHub Release
|
||||
run: |
|
||||
mv app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/bitchat-android.apk
|
||||
cd app/build/outputs/apk/release
|
||||
[ -f "app-arm64-v8a-release-unsigned.apk" ] && mv app-arm64-v8a-release-unsigned.apk bitchat-android-arm64.apk
|
||||
[ -f "app-x86_64-release-unsigned.apk" ] && mv app-x86_64-release-unsigned.apk bitchat-android-x86_64.apk
|
||||
[ -f "app-universal-release-unsigned.apk" ] && mv app-universal-release-unsigned.apk bitchat-android-universal.apk
|
||||
|
||||
- name: DEBUG
|
||||
run: |
|
||||
@ -59,8 +62,8 @@ jobs:
|
||||
ls -all
|
||||
tree || ls -R
|
||||
|
||||
# Optional: Sign APK (requires secrets)
|
||||
# - name: Sign APK
|
||||
# Optional: Sign APKs (uncomment and configure secrets when ready)
|
||||
# - name: Sign APKs
|
||||
# uses: r0adkll/sign-android-release@v1
|
||||
# with:
|
||||
# releaseDirectory: app/build/outputs/apk/release
|
||||
@ -69,10 +72,10 @@ jobs:
|
||||
# keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
# keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload APK as artifact
|
||||
- name: Upload APKs as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bitchat-android-apk-${{ github.ref_name }}
|
||||
name: bitchat-android-release-${{ github.ref_name }}
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
@ -82,25 +85,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download APK artifact
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: bitchat-android-apk-${{ github.ref_name }}
|
||||
name: bitchat-android-release-${{ github.ref_name }}
|
||||
path: release
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
release/bitchat-android.apk
|
||||
release/bitchat-android-arm64.apk
|
||||
release/bitchat-android-x86_64.apk
|
||||
release/bitchat-android-universal.apk
|
||||
name: Release ${{ github.ref_name }}
|
||||
body: |
|
||||
## bitchat Android Release
|
||||
|
||||
**bitchat-android.apk** (~15MB)
|
||||
- Secure P2P messaging over Bluetooth mesh and Nostr
|
||||
- Built-in Tor support (custom Arti build with 16KB page size)
|
||||
- Compatible with Google Play requirements (Nov 2025+)
|
||||
- Cross-platform compatible with iOS version
|
||||
**bitchat-android-arm64.apk** - ARM64 (most phones)
|
||||
**bitchat-android-x86_64.apk** - x86_64 (Chromebooks, tablets)
|
||||
**bitchat-android-universal.apk** - All architectures (fallback)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -13,7 +13,7 @@ android {
|
||||
applicationId = "com.bitchat.droid"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 27
|
||||
versionCode = 30
|
||||
versionName = "1.6.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@ -43,13 +43,20 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
ndk {
|
||||
// ARM64-only to minimize APK size (~5.8MB savings)
|
||||
// Excludes x86_64 as emulator not needed for production builds
|
||||
abiFilters += listOf("arm64-v8a")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APK splits for GitHub releases - creates arm64, x86_64, and universal APKs
|
||||
// AAB for Play Store handles architecture distribution automatically
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a", "x86_64")
|
||||
isUniversalApk = true // For F-Droid and fallback
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@ -84,12 +91,22 @@ dependencies {
|
||||
|
||||
// Lifecycle
|
||||
implementation(libs.bundles.lifecycle)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
// Navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// Permissions
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
||||
// QR
|
||||
implementation(libs.zxing.core)
|
||||
implementation(libs.mlkit.barcode.scanning)
|
||||
|
||||
// CameraX
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.compose)
|
||||
|
||||
// Cryptography
|
||||
implementation(libs.bundles.cryptography)
|
||||
|
||||
@ -243,17 +243,6 @@
|
||||
column="35"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="OldTargetApi"
|
||||
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the android.os.Build.VERSION_CODES javadoc for details."
|
||||
errorLine1=" targetSdk = 34"
|
||||
errorLine2=" ~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="build.gradle.kts"
|
||||
line="14"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="RedundantLabel"
|
||||
message="Redundant label can be removed"
|
||||
|
||||
@ -16,19 +16,26 @@
|
||||
<!-- Location permission required for BLE scanning -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- Notification permissions -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- Signature permission for internal UI shutdown broadcasts -->
|
||||
<uses-permission android:name="com.bitchat.android.permission.FORCE_FINISH" />
|
||||
<!-- Foreground service and boot permissions for long-running background mesh -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- Connected device foreground service type for BLE operations (API 34+) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<!-- Data sync foreground service type (required when declaring dataSync) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!-- Location foreground service type required for BLE scanning in background -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- Microphone for voice notes -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!-- Camera for QR verification -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- Storage permissions for file sharing -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
@ -45,6 +52,11 @@
|
||||
<!-- Hardware features -->
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
|
||||
<permission
|
||||
android:name="com.bitchat.android.permission.FORCE_FINISH"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<application
|
||||
android:name=".BitchatApplication"
|
||||
@ -83,13 +95,19 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bitchat" android:host="verify" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Persistent foreground service to run the mesh in background -->
|
||||
<service
|
||||
android:name=".service.MeshForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="connectedDevice|dataSync"
|
||||
android:foregroundServiceType="connectedDevice|dataSync|location"
|
||||
tools:ignore="DataExtractionRules">
|
||||
</service>
|
||||
|
||||
|
||||
@ -1,295 +1,290 @@
|
||||
Relay URL,Latitude,Longitude
|
||||
strfry.shock.network,39.0438,-77.4874
|
||||
wot.nostr.party,36.1627,-86.7816
|
||||
nostr.blankfors.se,60.1699,24.9384
|
||||
nostr.now,36.55,139.733
|
||||
nostr.simplex.icu,51.5121,-0.0005238
|
||||
relay.threenine.services,51.5524,-0.29686
|
||||
relay.barine.co,43.6532,-79.3832
|
||||
theoutpost.life,64.1476,-21.9392
|
||||
purplerelay.com,50.1109,8.68213
|
||||
nostrue.com,40.8054,-74.0241
|
||||
relay-arg.zombi.cloudrodion.com,1.35208,103.82
|
||||
shu02.shugur.net,21.4902,39.2246
|
||||
relayone.geektank.ai,18.2148,-63.0574
|
||||
relay03.lnfi.network,39.0997,-94.5786
|
||||
soloco.nl,43.6532,-79.3832
|
||||
relay-testnet.k8s.layer3.news,37.3387,-121.885
|
||||
nostr.azzamo.net,52.2633,21.0283
|
||||
nostr.red5d.dev,43.6532,-79.3832
|
||||
nostr.thebiglake.org,32.71,-96.6745
|
||||
nostr.tadryanom.me,43.6532,-79.3832
|
||||
schnorr.me,43.6532,-79.3832
|
||||
ithurtswhenip.ee,51.223,6.78245
|
||||
orly.ft.hn,50.4754,12.3683
|
||||
relay02.lnfi.network,39.0997,-94.5786
|
||||
relay.nosto.re,51.1792,5.89444
|
||||
strfry.openhoofd.nl,51.9229,4.40833
|
||||
relays.diggoo.com,43.6532,-79.3832
|
||||
relay.nostrhub.tech,49.0291,8.35696
|
||||
relay.artx.market,43.652,-79.3633
|
||||
relay.sigit.io,50.4754,12.3683
|
||||
nostr-relay.psfoundation.info,39.0438,-77.4874
|
||||
wot.brightbolt.net,47.6735,-116.781
|
||||
relay.bitcoinartclock.com,50.4754,12.3683
|
||||
khatru.nostrver.se,51.1792,5.89444
|
||||
relay.cosmicbolt.net,37.3986,-121.964
|
||||
adre.su,59.9311,30.3609
|
||||
nostrelay.memory-art.xyz,43.6532,-79.3832
|
||||
temp.iris.to,43.6532,-79.3832
|
||||
nproxy.kristapsk.lv,60.1699,24.9384
|
||||
relay.ngengine.org,43.6532,-79.3832
|
||||
relay.puresignal.news,43.6532,-79.3832
|
||||
nostr.stakey.net,52.3676,4.90414
|
||||
nostr.commonshub.brussels,49.4543,11.0746
|
||||
wot.shaving.kiwi,43.6532,-79.3832
|
||||
espelho.girino.org,43.6532,-79.3832
|
||||
relay.thibautduchene.fr,43.6532,-79.3832
|
||||
nostr-relay.corb.net,38.8353,-104.822
|
||||
wot.soundhsa.com,33.1384,-95.6011
|
||||
relay.jabato.space,52.52,13.405
|
||||
relay.credenso.cafe,43.3601,-80.3127
|
||||
relayone.soundhsa.com,33.1384,-95.6011
|
||||
relay.bitcoinveneto.org,64.1466,-21.9426
|
||||
relay.samt.st,40.8302,-74.1299
|
||||
relay.vrtmrz.net,43.6532,-79.3832
|
||||
nostr.data.haus,50.4754,12.3683
|
||||
relay.npubhaus.com,43.6532,-79.3832
|
||||
strfry.elswa-dev.online,50.1109,8.68213
|
||||
relay.wavefunc.live,34.0362,-118.443
|
||||
wot.sovbit.host,64.1466,-21.9426
|
||||
relay.smies.me,33.7501,-84.3885
|
||||
nostr.bilthon.dev,25.8128,-80.2377
|
||||
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
|
||||
santo.iguanatech.net,40.8302,-74.1299
|
||||
prl.plus,42.6978,23.3246
|
||||
relay-freeharmonypeople.space,38.7223,-9.13934
|
||||
nostream.breadslice.com,1.35208,103.82
|
||||
nostr.notribe.net,40.8302,-74.1299
|
||||
relay.fountain.fm,39.0997,-94.5786
|
||||
orangepiller.org,60.1699,24.9384
|
||||
nostr.21crypto.ch,47.5356,8.73209
|
||||
nostr.camalolo.com,24.1469,120.684
|
||||
okn.czas.top,51.267,6.81738
|
||||
relay.wolfcoil.com,35.6092,139.73
|
||||
nostr.quali.chat,60.1699,24.9384
|
||||
nostr-relay.online,43.6532,-79.3832
|
||||
alienos.libretechsystems.xyz,55.4724,9.87335
|
||||
ribo.eu.nostria.app,52.3676,4.90414
|
||||
wot.yesnostr.net,50.9871,2.12554
|
||||
nostr.overmind.lol,43.6532,-79.3832
|
||||
relay.chakany.systems,43.6532,-79.3832
|
||||
relay.hasenpfeffr.com,39.0438,-77.4874
|
||||
nostrelites.org,41.8781,-87.6298
|
||||
strfry.bonsai.com,37.8715,-122.273
|
||||
relay.agora.social,50.7383,15.0648
|
||||
alien.macneilmediagroup.com,43.6532,-79.3832
|
||||
relay.getsafebox.app,43.6532,-79.3832
|
||||
relay.nuts.cash,34.0362,-118.443
|
||||
notemine.io,52.2026,20.9397
|
||||
relay.nostr.place,32.7767,-96.797
|
||||
relay.21e6.cz,50.7383,15.0648
|
||||
nostr.casa21.space,43.6532,-79.3832
|
||||
premium.primal.net,43.6532,-79.3832
|
||||
relay5.bitransfer.org,43.6532,-79.3832
|
||||
nostr.rblb.it,43.7094,10.6582
|
||||
relay.goodmorningbitcoin.com,43.6532,-79.3832
|
||||
relay.camelus.app,45.5201,-122.99
|
||||
relay.origin.land,35.6673,139.751
|
||||
relay.fr13nd5.com,52.5233,13.3426
|
||||
nos.lol,50.4754,12.3683
|
||||
wot.nostr.net,43.6532,-79.3832
|
||||
relay.javi.space,43.4633,11.8796
|
||||
relay2.ngengine.org,43.6532,-79.3832
|
||||
relay.angor.io,48.1046,11.6002
|
||||
pyramid.treegaze.com,43.6532,-79.3832
|
||||
relay.letsfo.com,52.2633,21.0283
|
||||
relay.nostrzh.org,43.6532,-79.3832
|
||||
nostr.hekster.org,37.3986,-121.964
|
||||
relay.0xchat.com,1.35208,103.82
|
||||
nostr.czas.top,50.1109,8.68213
|
||||
nostr.superfriends.online,43.6532,-79.3832
|
||||
hsuite-nostr-relay.hbarsuite.workers.dev,43.6532,-79.3832
|
||||
relay.mccormick.cx,52.3563,4.95714
|
||||
nostr.88mph.life,51.5072,-0.127586
|
||||
strfry.ymir.cloud,34.0965,-117.585
|
||||
nostr.lkjsxc.com,43.6532,-79.3832
|
||||
yabu.me,35.6092,139.73
|
||||
relay.chorus.community,50.1109,8.68213
|
||||
nostr-02.yakihonne.com,1.32123,103.695
|
||||
neuromancer.nettek.io,39.1429,-94.573
|
||||
relay04.lnfi.network,39.0997,-94.5786
|
||||
nostr-relay.amethyst.name,39.0438,-77.4874
|
||||
nostrcheck.tnsor.network,43.6532,-79.3832
|
||||
wot.nostr.place,32.7767,-96.797
|
||||
cyberspace.nostr1.com,40.7057,-74.0136
|
||||
relay.guggero.org,47.3769,8.54169
|
||||
nostr.calitabby.net,39.9268,-75.0246
|
||||
x.kojira.io,43.6532,-79.3832
|
||||
nostr.agentcampfire.com,52.3676,4.90414
|
||||
relay.bullishbounty.com,43.6532,-79.3832
|
||||
relay.nostrverse.net,43.6532,-79.3832
|
||||
nostr.robosats.org,64.1476,-21.9392
|
||||
nostr-verified.wellorder.net,45.5201,-122.99
|
||||
nostr.huszonegy.world,47.4979,19.0402
|
||||
relay.cypherflow.ai,48.8566,2.35222
|
||||
nostr-relay.xbytez.io,50.6924,3.20113
|
||||
nostr.faultables.net,43.6532,-79.3832
|
||||
relay.libernet.app,43.6532,-79.3832
|
||||
relay.magiccity.live,25.8128,-80.2377
|
||||
relay.wellorder.net,45.5201,-122.99
|
||||
black.nostrcity.club,48.8575,2.35138
|
||||
relay.jeffg.fyi,43.6532,-79.3832
|
||||
freeben666.fr,43.7221,7.15296
|
||||
relay.nostr.wirednet.jp,34.706,135.493
|
||||
nostrelay.circum.space,52.3676,4.90414
|
||||
relay.islandbitcoin.com,12.8498,77.6545
|
||||
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
|
||||
relay.nostriot.com,41.5695,-83.9786
|
||||
relayrs.notoshi.win,43.6532,-79.3832
|
||||
nostr-pub.wellorder.net,45.5201,-122.99
|
||||
nostr.vulpem.com,49.4543,11.0746
|
||||
nostr.sathoarder.com,48.5734,7.75211
|
||||
wheat.happytavern.co,43.6532,-79.3832
|
||||
relay.nostr.net,43.6532,-79.3832
|
||||
bcast.girino.org,43.6532,-79.3832
|
||||
nostr-02.czas.top,51.2277,6.77346
|
||||
vault.iris.to,43.6532,-79.3832
|
||||
ynostr.yael.at,60.1699,24.9384
|
||||
nostr.nodesmap.com,59.3327,18.0656
|
||||
nostr.n7ekb.net,47.4941,-122.294
|
||||
relayb.uid.ovh,43.6532,-79.3832
|
||||
shu05.shugur.net,48.8566,2.35222
|
||||
dev-relay.lnfi.network,39.0997,-94.5786
|
||||
relay.bitcoindistrict.org,43.6532,-79.3832
|
||||
nostr.spicyz.io,43.6532,-79.3832
|
||||
nostr.0x7e.xyz,47.4988,8.72369
|
||||
relay.dwadziesciajeden.pl,52.2297,21.0122
|
||||
relay.zone667.com,60.1699,24.9384
|
||||
nostr-dev.wellorder.net,45.5201,-122.99
|
||||
nos.xmark.cc,50.6924,3.20113
|
||||
relay.etch.social,41.2619,-95.8608
|
||||
nostr.na.social,43.6532,-79.3832
|
||||
relay.orangepill.ovh,49.1689,-0.358841
|
||||
relay.olas.app,50.4754,12.3683
|
||||
relay.holzeis.me,43.6532,-79.3832
|
||||
relay2.angor.io,48.1046,11.6002
|
||||
relay.degmods.com,50.4754,12.3683
|
||||
vitor.nostr1.com,40.7128,-74.006
|
||||
relay.btcforplebs.com,43.6532,-79.3832
|
||||
nostr.luisschwab.net,43.6532,-79.3832
|
||||
relay.moinsen.com,50.4754,12.3683
|
||||
czas.xyz,48.8566,2.35222
|
||||
nostr.bitcoiner.social,39.1585,-94.5728
|
||||
nostr.mehdibekhtaoui.com,49.4939,-1.54813
|
||||
inbox.azzamo.net,52.2633,21.0283
|
||||
nostr.ovia.to,43.6532,-79.3832
|
||||
nostr.myshosholoza.co.za,52.3676,4.90414
|
||||
fenrir-s.notoshi.win,43.6532,-79.3832
|
||||
relay-rpi.edufeed.org,49.4521,11.0767
|
||||
chat-relay.zap-work.com,43.6532,-79.3832
|
||||
nostr.bond,50.1109,8.68213
|
||||
relay01.lnfi.network,39.0997,-94.5786
|
||||
relay.seq1.net,43.6532,-79.3832
|
||||
nostr.rikmeijer.nl,50.4754,12.3683
|
||||
offchain.pub,47.6743,-117.112
|
||||
nostr.spaceshell.xyz,43.6532,-79.3832
|
||||
nos4smartnkind.tech,40.1872,44.5152
|
||||
kotukonostr.onrender.com,37.7775,-122.397
|
||||
nostr.ps1829.com,33.8851,130.883
|
||||
relay.lightning.pub,39.0438,-77.4874
|
||||
nostr-relay.gateway.in.th,15.2634,100.344
|
||||
relay.thebluepulse.com,49.4521,11.0767
|
||||
relay.malxte.de,52.52,13.405
|
||||
relay.usefusion.ai,38.7134,-78.1591
|
||||
nostr-relay.cbrx.io,43.6532,-79.3832
|
||||
shu01.shugur.net,21.4902,39.2246
|
||||
nostr.jerrynya.fun,31.2304,121.474
|
||||
dev-nostr.bityacht.io,25.0797,121.234
|
||||
bitsat.molonlabe.holdings,51.4012,-1.3147
|
||||
relay.primal.net,43.6532,-79.3832
|
||||
relay.wavlake.com,41.2619,-95.8608
|
||||
nostr-relay.nextblockvending.com,47.2343,-119.853
|
||||
v-relay.d02.vrtmrz.net,34.6937,135.502
|
||||
nostr.mikoshi.de,47.74,12.0917
|
||||
nostr.coincards.com,53.5501,-113.469
|
||||
relay.snort.social,53.3498,-6.26031
|
||||
bitcoiner.social,39.1585,-94.5728
|
||||
nostr.noones.com,50.1109,8.68213
|
||||
wot.dergigi.com,64.1476,-21.9392
|
||||
relay.thebluepulse.com,49.4521,11.0767
|
||||
relay.guggero.org,47.3769,8.54169
|
||||
relay.lumina.rocks,49.0291,8.35695
|
||||
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
|
||||
nostr.snowbla.de,60.1699,24.9384
|
||||
strfry.felixzieger.de,50.1013,8.62643
|
||||
relay.satlantis.io,32.8769,-80.0114
|
||||
relay-dev.satlantis.io,40.8302,-74.1299
|
||||
nostr.davidebtc.me,51.5072,-0.127586
|
||||
relay.mattybs.lol,43.6532,-79.3832
|
||||
relay.fundstr.me,42.3601,-71.0589
|
||||
relay.tagayasu.xyz,43.6715,-79.38
|
||||
nostr-relay.zimage.com,34.0549,-118.243
|
||||
nostrcheck.me,43.6532,-79.3832
|
||||
bucket.coracle.social,37.7775,-122.397
|
||||
relay.siamdev.cc,13.9178,100.424
|
||||
r.bitcoinhold.net,43.6532,-79.3832
|
||||
skeme.vanderwarker.family,40.8218,-74.45
|
||||
articles.layer3.news,37.3387,-121.885
|
||||
no.str.cr,9.92857,-84.0528
|
||||
srtrelay.c-stellar.net,43.6532,-79.3832
|
||||
relay.comcomponent.com,34.7062,135.493
|
||||
relay.illuminodes.com,47.6061,-122.333
|
||||
nostr.openhoofd.nl,51.9229,4.40833
|
||||
nostr.plantroon.com,50.1013,8.62643
|
||||
slick.mjex.me,39.048,-77.4817
|
||||
relay.minibolt.info,43.6532,-79.3832
|
||||
nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832
|
||||
relay.nostrdice.com,-33.8688,151.209
|
||||
wot.dtonon.com,43.6532,-79.3832
|
||||
shu04.shugur.net,25.2604,55.2989
|
||||
nostr.tavux.tech,48.8575,2.35138
|
||||
nostr-02.uid.ovh,43.6532,-79.3832
|
||||
relay.nostrcheck.me,43.6532,-79.3832
|
||||
relay.nsnip.io,60.1699,24.9384
|
||||
relay.toastr.net,40.8054,-74.0241
|
||||
relay.divine.video,43.6532,-79.3832
|
||||
nostr.4rs.nl,49.0291,8.35696
|
||||
wot.sebastix.social,51.1792,5.89444
|
||||
relay.openfarmtools.org,60.1699,24.9384
|
||||
relay.damus.io,43.6532,-79.3832
|
||||
purpura.cloud,43.6532,-79.3832
|
||||
wot.sudocarlos.com,51.5072,-0.127586
|
||||
relay.arx-ccn.com,50.4754,12.3683
|
||||
nostr.zoracle.org,45.6018,-121.185
|
||||
nostr.tac.lol,47.4748,-122.273
|
||||
relay.coinos.io,43.6532,-79.3832
|
||||
nostr.rtvslawenia.com,49.4543,11.0746
|
||||
relay.davidebtc.me,51.5072,-0.127586
|
||||
ribo.us.nostria.app,41.5868,-93.625
|
||||
relay.upleb.uk,51.9194,19.1451
|
||||
librerelay.aaroniumii.com,43.6532,-79.3832
|
||||
relay.endfiat.money,43.6532,-79.3832
|
||||
relay.nostar.org,43.6532,-79.3832
|
||||
relay.electriclifestyle.com,26.2897,-80.1293
|
||||
relay.nostrhub.fr,48.1045,11.6004
|
||||
relay.agorist.space,52.3734,4.89406
|
||||
freelay.sovbit.host,64.1476,-21.9392
|
||||
nostr.hifish.org,47.4043,8.57398
|
||||
nostr-01.yakihonne.com,1.32123,103.695
|
||||
relay.routstr.com,43.6532,-79.3832
|
||||
nr.yay.so,46.2126,6.1154
|
||||
bcast.seutoba.com.br,43.6532,-79.3832
|
||||
fanfares.nostr1.com,40.7128,-74.006
|
||||
nostr.girino.org,43.6532,-79.3832
|
||||
nostr2.girino.org,43.6532,-79.3832
|
||||
relay.notoshi.win,13.311,101.112
|
||||
nostrja-kari.heguro.com,43.6532,-79.3832
|
||||
relay.mostro.network,40.8302,-74.1299
|
||||
satsage.xyz,37.3986,-121.964
|
||||
relay-freeharmonypeople.space,38.7223,-9.13934
|
||||
nostr-relay.online,43.6532,-79.3832
|
||||
shu05.shugur.net,48.8566,2.35222
|
||||
relay.evanverma.com,40.8302,-74.1299
|
||||
nostr-2.21crypto.ch,47.5356,8.73209
|
||||
relay2.angor.io,48.1046,11.6002
|
||||
relay.arx-ccn.com,50.4754,12.3683
|
||||
relay.davidebtc.me,51.5072,-0.127586
|
||||
temp.iris.to,43.6532,-79.3832
|
||||
nostr.superfriends.online,43.6532,-79.3832
|
||||
vitor.nostr1.com,40.7057,-74.0136
|
||||
relay.moinsen.com,50.4754,12.3683
|
||||
ithurtswhenip.ee,51.223,6.78245
|
||||
nostr.tadryanom.me,43.6532,-79.3832
|
||||
relay.nostr.wirednet.jp,34.706,135.493
|
||||
relay.0xchat.com,1.35208,103.82
|
||||
nostrcheck.tnsor.network,43.6532,-79.3832
|
||||
relay.ngengine.org,43.6532,-79.3832
|
||||
relay.mitchelltribe.com,39.0438,-77.4874
|
||||
relaynostr.breadslice.com,43.6532,-79.3832
|
||||
nostr.chaima.info,51.223,6.78245
|
||||
nostr.mom,50.4754,12.3683
|
||||
wotr.relatr.xyz,53.3498,-6.26031
|
||||
relay.chorus.community,50.1109,8.68213
|
||||
orly.ft.hn,50.4754,12.3683
|
||||
nostr.stakey.net,52.3676,4.90414
|
||||
relay.malxte.de,52.52,13.405
|
||||
nostrelay.circum.space,52.3676,4.90414
|
||||
nostrcheck.me,43.6532,-79.3832
|
||||
wot.sudocarlos.com,51.5072,-0.127586
|
||||
relay.routstr.com,43.6532,-79.3832
|
||||
relay.siamdev.cc,13.8434,100.363
|
||||
wot.sovbit.host,64.1466,-21.9426
|
||||
strfry.bonsai.com,37.8715,-122.273
|
||||
freeben666.fr,43.7221,7.15296
|
||||
okn.czas.top,51.267,6.81738
|
||||
relay.nostar.org,43.6532,-79.3832
|
||||
relay.jeffg.fyi,43.6532,-79.3832
|
||||
nostr-02.uid.ovh,43.6532,-79.3832
|
||||
relay.fr13nd5.com,52.5233,13.3426
|
||||
srtrelay.c-stellar.net,43.6532,-79.3832
|
||||
relay.dwadziesciajeden.pl,52.2297,21.0122
|
||||
relay.sigit.io,50.4754,12.3683
|
||||
nostr.girino.org,43.6532,-79.3832
|
||||
bitsat.molonlabe.holdings,51.4012,-1.3147
|
||||
nostream.breadslice.com,1.35208,103.82
|
||||
nostr.rtvslawenia.com,49.4543,11.0746
|
||||
nostr.openhoofd.nl,51.9229,4.40833
|
||||
librerelay.aaroniumii.com,43.6532,-79.3832
|
||||
relay.etch.social,41.2619,-95.8608
|
||||
strfry.felixzieger.de,50.1013,8.62643
|
||||
relay.wavlake.com,41.2619,-95.8608
|
||||
relay.fundstr.me,42.3601,-71.0589
|
||||
slick.mjex.me,39.048,-77.4817
|
||||
relay.upleb.uk,51.9194,19.1451
|
||||
nos4smartnkind.tech,40.1872,44.5152
|
||||
discovery.eu.nostria.app,52.3676,4.90414
|
||||
nostr.tac.lol,47.4748,-122.273
|
||||
wot.nostr.net,43.6532,-79.3832
|
||||
nostr-dev.wellorder.net,45.5201,-122.99
|
||||
fanfares.nostr1.com,40.7128,-74.006
|
||||
relay.bitcoindistrict.org,43.6532,-79.3832
|
||||
nostr-relay.nextblockvending.com,47.2343,-119.853
|
||||
relay-rpi.edufeed.org,49.4521,11.0767
|
||||
relay.nsnip.io,60.1699,24.9384
|
||||
black.nostrcity.club,48.8575,2.35138
|
||||
nostr-02.czas.top,51.2277,6.77346
|
||||
schnorr.me,43.6532,-79.3832
|
||||
relay.fountain.fm,39.0997,-94.5786
|
||||
nostr-verified.wellorder.net,45.5201,-122.99
|
||||
relay.divine.video,43.6532,-79.3832
|
||||
relay.thibautduchene.fr,43.6532,-79.3832
|
||||
nostr.plantroon.com,50.1013,8.62643
|
||||
relay.hasenpfeffr.com,39.0438,-77.4874
|
||||
relay.chakany.systems,43.6532,-79.3832
|
||||
nostr-01.yakihonne.com,1.32123,103.695
|
||||
nostr.azzamo.net,52.2633,21.0283
|
||||
nostr.bond,50.1109,8.68213
|
||||
nostr.coincards.com,53.5501,-113.469
|
||||
relay.nostrhub.tech,49.0291,8.35696
|
||||
relay.cosmicbolt.net,37.3986,-121.964
|
||||
nostr.21crypto.ch,47.5356,8.73209
|
||||
relay-dev.satlantis.io,40.8302,-74.1299
|
||||
offchain.pub,47.6743,-117.112
|
||||
relay.orangepill.ovh,49.1689,-0.358841
|
||||
nostr-relay.gateway.in.th,15.2634,100.344
|
||||
nostr.now,36.55,139.733
|
||||
relay.usefusion.ai,38.7134,-78.1591
|
||||
nostr.lkjsxc.com,43.6532,-79.3832
|
||||
nproxy.kristapsk.lv,60.1699,24.9384
|
||||
relay.nostrverse.net,43.6532,-79.3832
|
||||
relay.zone667.com,60.1699,24.9384
|
||||
relay.origin.land,35.6673,139.751
|
||||
relay.damus.io,43.6532,-79.3832
|
||||
relay.bnos.space,1.35208,103.82
|
||||
relay.endfiat.money,43.6532,-79.3832
|
||||
relay.bullishbounty.com,43.6532,-79.3832
|
||||
wot.nostr.party,36.1627,-86.7816
|
||||
nostr.huszonegy.world,47.4979,19.0402
|
||||
theoutpost.life,64.1476,-21.9392
|
||||
nostr.noones.com,50.1109,8.68213
|
||||
relay.21e6.cz,50.7383,15.0648
|
||||
nostr-relay.xbytez.io,50.6924,3.20113
|
||||
wot.shaving.kiwi,43.6532,-79.3832
|
||||
relay.trustroots.org,43.6532,-79.3832
|
||||
wot.dtonon.com,43.6532,-79.3832
|
||||
nostr.camalolo.com,24.1469,120.684
|
||||
czas.xyz,48.8566,2.35222
|
||||
relayrs.notoshi.win,43.6532,-79.3832
|
||||
skeme.vanderwarker.family,40.8218,-74.45
|
||||
nostr.red5d.dev,43.6532,-79.3832
|
||||
relay.javi.space,43.4633,11.8796
|
||||
relay.nuts.cash,34.0362,-118.443
|
||||
nostr.commonshub.brussels,49.4543,11.0746
|
||||
relay.artx.market,43.652,-79.3633
|
||||
satsage.xyz,37.3986,-121.964
|
||||
relay.nostx.io,43.6532,-79.3832
|
||||
shu02.shugur.net,21.4902,39.2246
|
||||
strfry.ymir.cloud,34.0965,-117.585
|
||||
relay.nostr-check.me,43.6532,-79.3832
|
||||
relay04.lnfi.network,39.0997,-94.5786
|
||||
kotukonostr.onrender.com,37.7775,-122.397
|
||||
nostr.davidebtc.me,51.5072,-0.127586
|
||||
relay2.ngengine.org,43.6532,-79.3832
|
||||
ribo.us.nostria.app,41.5868,-93.625
|
||||
nostr.czas.top,50.1109,8.68213
|
||||
relay.nostrzh.org,43.6532,-79.3832
|
||||
relay.angor.io,48.1046,11.6002
|
||||
alienos.libretechsystems.xyz,55.4724,9.87335
|
||||
vault.iris.to,43.6532,-79.3832
|
||||
relay.mccormick.cx,52.3563,4.95714
|
||||
nostr.quali.chat,60.1699,24.9384
|
||||
wheat.happytavern.co,43.6532,-79.3832
|
||||
relay.olas.app,50.4754,12.3683
|
||||
nostr-relayrs.gateway.in.th,15.2634,100.344
|
||||
relay.snort.social,53.3498,-6.26031
|
||||
nostr-2.21crypto.ch,47.5356,8.73209
|
||||
pyramid.treegaze.com,43.6532,-79.3832
|
||||
relay.smies.me,33.7501,-84.3885
|
||||
wot.dergigi.com,64.1476,-21.9392
|
||||
nostr-relay.zimage.com,34.0549,-118.243
|
||||
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
|
||||
nostr.jerrynya.fun,31.2304,121.474
|
||||
purplerelay.com,50.1109,8.68213
|
||||
relay.comcomponent.com,34.7062,135.493
|
||||
relay.unitypay.cash,41.4513,-81.7021
|
||||
nostr.mikoshi.de,50.1109,8.68213
|
||||
prl.plus,42.6978,23.3246
|
||||
shu04.shugur.net,25.2604,55.2989
|
||||
nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832
|
||||
nostr.zoracle.org,45.6018,-121.185
|
||||
nostriches.club,43.6532,-79.3832
|
||||
nostr.notribe.net,40.8302,-74.1299
|
||||
nostr.vulpem.com,49.4543,11.0746
|
||||
bucket.coracle.social,37.7775,-122.397
|
||||
wot.yesnostr.net,50.9871,2.12554
|
||||
wot.nostr.place,32.7767,-96.797
|
||||
nostr.mehdibekhtaoui.com,49.4939,-1.54813
|
||||
relay.getsafebox.app,43.6532,-79.3832
|
||||
nostr2.girino.org,43.6532,-79.3832
|
||||
nostr.blankfors.se,60.1699,24.9384
|
||||
premium.primal.net,43.6532,-79.3832
|
||||
nostr.rikmeijer.nl,50.4754,12.3683
|
||||
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
|
||||
adre.su,59.9311,30.3609
|
||||
nostr.n7ekb.net,47.4941,-122.294
|
||||
nostr.hekster.org,37.3986,-121.964
|
||||
relay.jabato.space,52.52,13.405
|
||||
fenrir-s.notoshi.win,43.6532,-79.3832
|
||||
notemine.io,52.2026,20.9397
|
||||
r.bitcoinhold.net,43.6532,-79.3832
|
||||
nostr-pub.wellorder.net,45.5201,-122.99
|
||||
articles.layer3.news,37.3387,-121.885
|
||||
relays.diggoo.com,43.6532,-79.3832
|
||||
relayb.uid.ovh,43.6532,-79.3832
|
||||
nostr.thebiglake.org,32.71,-96.6745
|
||||
chat-relay.zap-work.com,43.6532,-79.3832
|
||||
nr.yay.so,46.2126,6.1154
|
||||
shu01.shugur.net,21.4902,39.2246
|
||||
khatru.nostrver.se,51.1792,5.89444
|
||||
relay.openfarmtools.org,60.1699,24.9384
|
||||
relay01.lnfi.network,39.0997,-94.5786
|
||||
nostrja-kari.heguro.com,43.6532,-79.3832
|
||||
nostr.snowbla.de,60.1699,24.9384
|
||||
relay.satlantis.io,32.8769,-80.0114
|
||||
relay.vrtmrz.net,43.6532,-79.3832
|
||||
purpura.cloud,43.6532,-79.3832
|
||||
orangepiller.org,60.1699,24.9384
|
||||
relay.coinos.io,43.6532,-79.3832
|
||||
relay.layer.systems,49.0291,8.35695
|
||||
relay.toastr.net,40.8054,-74.0241
|
||||
ynostr.yael.at,60.1699,24.9384
|
||||
ribo.eu.nostria.app,52.3676,4.90414
|
||||
relay.nostrcheck.me,43.6532,-79.3832
|
||||
relay.primal.net,43.6532,-79.3832
|
||||
relay-testnet.k8s.layer3.news,37.3387,-121.885
|
||||
relay.threenine.services,51.5524,-0.29686
|
||||
relay.nostr.net,43.6532,-79.3832
|
||||
nostr.middling.mydns.jp,35.8099,140.12
|
||||
soloco.nl,43.6532,-79.3832
|
||||
nostr.4rs.nl,49.0291,8.35696
|
||||
no.str.cr,9.92857,-84.0528
|
||||
espelho.girino.org,43.6532,-79.3832
|
||||
nostr.hifish.org,47.4043,8.57398
|
||||
wot.sebastix.social,51.1792,5.89444
|
||||
relay03.lnfi.network,39.0997,-94.5786
|
||||
relay.nosto.re,51.1792,5.89444
|
||||
relay.nostrdice.com,-33.8688,151.209
|
||||
nostr.agentcampfire.com,52.3676,4.90414
|
||||
nostr.night7.space,50.4754,12.3683
|
||||
nostr.nodesmap.com,59.3327,18.0656
|
||||
relayone.soundhsa.com,33.1384,-95.6011
|
||||
relay.illuminodes.com,47.6061,-122.333
|
||||
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
|
||||
nostr.robosats.org,64.1476,-21.9392
|
||||
relay.seq1.net,43.6532,-79.3832
|
||||
strfry.shock.network,39.0438,-77.4874
|
||||
nostr.ovia.to,43.6532,-79.3832
|
||||
relay.holzeis.me,43.6532,-79.3832
|
||||
bcast.seutoba.com.br,43.6532,-79.3832
|
||||
relay.libernet.app,43.6532,-79.3832
|
||||
nostr-01.uid.ovh,43.6532,-79.3832
|
||||
nostr.luisschwab.net,43.6532,-79.3832
|
||||
strfry.elswa-dev.online,50.1109,8.68213
|
||||
relay.nostriot.com,41.5695,-83.9786
|
||||
nostr.oxtr.dev,50.4754,12.3683
|
||||
nostr.mom,50.4754,12.3683
|
||||
nostr-03.dorafactory.org,1.35208,103.82
|
||||
relay.camelus.app,45.5201,-122.99
|
||||
strfry.openhoofd.nl,51.9229,4.40833
|
||||
nos.lol,50.4754,12.3683
|
||||
nostr.bitcoiner.social,39.1585,-94.5728
|
||||
relay.islandbitcoin.com,12.8498,77.6545
|
||||
relay.tagayasu.xyz,43.6715,-79.38
|
||||
dev-nostr.bityacht.io,25.0797,121.234
|
||||
relay.nostrhub.fr,48.1045,11.6004
|
||||
relay.satmaxt.xyz,43.6532,-79.3832
|
||||
yabu.me,35.6092,139.73
|
||||
relay.samt.st,40.8302,-74.1299
|
||||
relay02.lnfi.network,39.0997,-94.5786
|
||||
nostr.88mph.life,51.5072,-0.127586
|
||||
dev-relay.lnfi.network,39.0997,-94.5786
|
||||
relay.goodmorningbitcoin.com,43.6532,-79.3832
|
||||
nostrelay.memory-art.xyz,43.6532,-79.3832
|
||||
relay.degmods.com,50.4754,12.3683
|
||||
nostr-02.yakihonne.com,1.32123,103.695
|
||||
nostr.spicyz.io,43.6532,-79.3832
|
||||
relay.minibolt.info,43.6532,-79.3832
|
||||
bcast.girino.org,43.6532,-79.3832
|
||||
nostr.bilthon.dev,25.8128,-80.2377
|
||||
relay.bitcoinartclock.com,50.4754,12.3683
|
||||
cyberspace.nostr1.com,40.7128,-74.006
|
||||
nostr.data.haus,50.4754,12.3683
|
||||
relay.agora.social,50.7383,15.0648
|
||||
relay.wavefunc.live,34.0362,-118.443
|
||||
nostr.casa21.space,43.6532,-79.3832
|
||||
relay.mostro.network,40.8302,-74.1299
|
||||
relay.ditto.pub,43.6532,-79.3832
|
||||
nostr-relay.amethyst.name,39.0438,-77.4874
|
||||
relay.credenso.cafe,43.3601,-80.3127
|
||||
nostr.calitabby.net,39.9268,-75.0246
|
||||
relay.magiccity.live,25.8128,-80.2377
|
||||
nostr.chaima.info,51.223,6.78245
|
||||
wot.brightbolt.net,47.6735,-116.781
|
||||
nostr.sathoarder.com,48.5734,7.75211
|
||||
inbox.azzamo.net,52.2633,21.0283
|
||||
nostr.na.social,43.6532,-79.3832
|
||||
nostr.myshosholoza.co.za,52.3676,4.90414
|
||||
nostr.spaceshell.xyz,43.6532,-79.3832
|
||||
relay.wellorder.net,45.5201,-122.99
|
||||
nostr.0x7e.xyz,47.4988,8.72369
|
||||
nostr-relay.cbrx.io,43.6532,-79.3832
|
||||
relay.btcforplebs.com,43.6532,-79.3832
|
||||
bitcoiner.social,39.1585,-94.5728
|
||||
relaynostr.breadslice.com,43.6532,-79.3832
|
||||
nostr.overmind.lol,43.6532,-79.3832
|
||||
santo.iguanatech.net,40.8302,-74.1299
|
||||
relay.lightning.pub,39.0438,-77.4874
|
||||
nostr.rblb.it,43.7094,10.6582
|
||||
relay.bitcoinveneto.org,64.1466,-21.9426
|
||||
relay.wolfcoil.com,35.6092,139.73
|
||||
nostr-relay.corb.net,38.8353,-104.822
|
||||
relay.nostr.place,32.7767,-96.797
|
||||
wot.soundhsa.com,33.1384,-95.6011
|
||||
relay.npubhaus.com,43.6532,-79.3832
|
||||
nostr-relay.psfoundation.info,39.0438,-77.4874
|
||||
x.kojira.io,43.6532,-79.3832
|
||||
relay.cypherflow.ai,48.8566,2.35222
|
||||
|
||||
|
@ -26,6 +26,7 @@ import com.bitchat.android.onboarding.BatteryOptimizationManager
|
||||
import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager
|
||||
import com.bitchat.android.onboarding.BatteryOptimizationScreen
|
||||
import com.bitchat.android.onboarding.BatteryOptimizationStatus
|
||||
import com.bitchat.android.onboarding.BackgroundLocationPermissionScreen
|
||||
import com.bitchat.android.onboarding.InitializationErrorScreen
|
||||
import com.bitchat.android.onboarding.InitializingScreen
|
||||
import com.bitchat.android.onboarding.LocationCheckScreen
|
||||
@ -40,6 +41,7 @@ import com.bitchat.android.ui.ChatViewModel
|
||||
import com.bitchat.android.ui.OrientationAwareActivity
|
||||
import com.bitchat.android.ui.theme.BitchatTheme
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -63,15 +65,46 @@ class MainActivity : OrientationAwareActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val forceFinishReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||
if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) {
|
||||
android.util.Log.i("MainActivity", "Received force finish broadcast, closing UI")
|
||||
finishAffinity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Register receiver for force finish signal from shutdown coordinator
|
||||
val filter = android.content.IntentFilter(com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH)
|
||||
if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
registerReceiver(
|
||||
forceFinishReceiver,
|
||||
filter,
|
||||
com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
|
||||
null,
|
||||
android.content.Context.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
registerReceiver(
|
||||
forceFinishReceiver,
|
||||
filter,
|
||||
com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
// Check if this is a quit request from the notification
|
||||
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
|
||||
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||||
|
||||
// Enable edge-to-edge display for modern Android look
|
||||
enableEdgeToEdge()
|
||||
@ -103,6 +136,9 @@ class MainActivity : OrientationAwareActivity() {
|
||||
activity = this,
|
||||
permissionManager = permissionManager,
|
||||
onOnboardingComplete = ::handleOnboardingComplete,
|
||||
onBackgroundLocationRequired = {
|
||||
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
|
||||
},
|
||||
onOnboardingFailed = ::handleOnboardingFailed
|
||||
)
|
||||
|
||||
@ -235,6 +271,21 @@ class MainActivity : OrientationAwareActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
OnboardingState.BACKGROUND_LOCATION_EXPLANATION -> {
|
||||
BackgroundLocationPermissionScreen(
|
||||
modifier = modifier,
|
||||
onContinue = {
|
||||
onboardingCoordinator.requestBackgroundLocation()
|
||||
},
|
||||
onRetry = {
|
||||
onboardingCoordinator.checkBackgroundLocationAndProceed()
|
||||
},
|
||||
onSkip = {
|
||||
onboardingCoordinator.skipBackgroundLocation()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> {
|
||||
// Set up back navigation handling for the chat screen
|
||||
val backCallback = object : OnBackPressedCallback(true) {
|
||||
@ -348,10 +399,17 @@ class MainActivity : OrientationAwareActivity() {
|
||||
if (permissionManager.isFirstTimeLaunch()) {
|
||||
Log.d("MainActivity", "First time launch, showing permission explanation")
|
||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||
} else if (permissionManager.areAllPermissionsGranted()) {
|
||||
Log.d("MainActivity", "Existing user with permissions, initializing app")
|
||||
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||||
initializeApp()
|
||||
} else if (permissionManager.areRequiredPermissionsGranted()) {
|
||||
Log.d("MainActivity", "Existing user with required permissions")
|
||||
if (permissionManager.needsBackgroundLocationPermission() &&
|
||||
!permissionManager.isBackgroundLocationGranted() &&
|
||||
!com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity)
|
||||
) {
|
||||
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
|
||||
} else {
|
||||
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||||
initializeApp()
|
||||
}
|
||||
} else {
|
||||
Log.d("MainActivity", "Existing user missing permissions, showing explanation")
|
||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||
@ -624,6 +682,7 @@ class MainActivity : OrientationAwareActivity() {
|
||||
|
||||
// Handle any notification intent
|
||||
handleNotificationIntent(intent)
|
||||
handleVerificationIntent(intent)
|
||||
|
||||
// Small delay to ensure mesh service is fully initialized
|
||||
delay(500)
|
||||
@ -646,10 +705,13 @@ class MainActivity : OrientationAwareActivity() {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||||
|
||||
// Handle notification intents when app is already running
|
||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||
handleNotificationIntent(intent)
|
||||
handleVerificationIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -659,9 +721,6 @@ class MainActivity : OrientationAwareActivity() {
|
||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||
// Reattach mesh delegate to new ChatViewModel instance after Activity recreation
|
||||
try { meshService.delegate = chatViewModel } catch (_: Exception) { }
|
||||
// Set app foreground state
|
||||
meshService.connectionManager.setAppBackgroundState(false)
|
||||
chatViewModel.setAppBackgroundState(false)
|
||||
|
||||
// Check if Bluetooth was disabled while app was backgrounded
|
||||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||||
@ -688,9 +747,6 @@ class MainActivity : OrientationAwareActivity() {
|
||||
super.onPause()
|
||||
// Only set background state if app is fully initialized
|
||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||
// Set app background state
|
||||
meshService.connectionManager.setAppBackgroundState(true)
|
||||
chatViewModel.setAppBackgroundState(true)
|
||||
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
|
||||
try { meshService.delegate = null } catch (_: Exception) { }
|
||||
}
|
||||
@ -755,10 +811,23 @@ class MainActivity : OrientationAwareActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerificationIntent(intent: Intent) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme != "bitchat" || uri.host != "verify") return
|
||||
|
||||
chatViewModel.showVerificationSheet()
|
||||
val qr = VerificationService.verifyScannedQR(uri.toString())
|
||||
if (qr != null) {
|
||||
chatViewModel.beginQRVerification(qr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
try { unregisterReceiver(forceFinishReceiver) } catch (_: Exception) { }
|
||||
|
||||
// Cleanup location status manager
|
||||
try {
|
||||
locationStatusManager.cleanup()
|
||||
|
||||
@ -19,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* This is the main interface for all encryption/decryption operations in bitchat.
|
||||
* It now uses the Noise protocol for secure transport encryption with proper session management.
|
||||
*/
|
||||
class EncryptionService(private val context: Context) {
|
||||
open class EncryptionService(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EncryptionService"
|
||||
@ -27,14 +27,14 @@ class EncryptionService(private val context: Context) {
|
||||
}
|
||||
|
||||
// Core Noise encryption service
|
||||
private val noiseService: NoiseEncryptionService = NoiseEncryptionService(context)
|
||||
private val noiseService: NoiseEncryptionService by lazy { NoiseEncryptionService(context) }
|
||||
|
||||
// Session tracking for established connections
|
||||
private val establishedSessions = ConcurrentHashMap<String, String>() // peerID -> fingerprint
|
||||
|
||||
// Ed25519 signing keys (separate from Noise static keys)
|
||||
private val ed25519PrivateKey: Ed25519PrivateKeyParameters
|
||||
private val ed25519PublicKey: Ed25519PublicKeyParameters
|
||||
private lateinit var ed25519PrivateKey: Ed25519PrivateKeyParameters
|
||||
private lateinit var ed25519PublicKey: Ed25519PublicKeyParameters
|
||||
|
||||
// Callbacks for UI state updates
|
||||
var onSessionEstablished: ((String) -> Unit)? = null // peerID
|
||||
@ -42,6 +42,13 @@ class EncryptionService(private val context: Context) {
|
||||
var onHandshakeRequired: ((String) -> Unit)? = null // peerID
|
||||
|
||||
init {
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization logic moved to method to allow overriding in tests
|
||||
*/
|
||||
protected open fun initialize() {
|
||||
// Initialize or load Ed25519 signing keys
|
||||
val keyPair = loadOrCreateEd25519KeyPair()
|
||||
ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters
|
||||
@ -356,7 +363,7 @@ class EncryptionService(private val context: Context) {
|
||||
/**
|
||||
* Verify Ed25519 signature against data using a public key
|
||||
*/
|
||||
fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean {
|
||||
open fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean {
|
||||
return try {
|
||||
val publicKey = Ed25519PublicKeyParameters(publicKeyBytes, 0)
|
||||
val verifier = Ed25519Signer()
|
||||
|
||||
@ -124,36 +124,55 @@ class LocationChannelManager private constructor(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin periodic one-shot location refreshes while a selector UI is visible
|
||||
* Begin real-time location updates while a selector UI is visible
|
||||
* Uses requestLocationUpdates for continuous updates, plus a one-shot to prime state immediately
|
||||
*/
|
||||
fun beginLiveRefresh(interval: Long = 5000L) {
|
||||
Log.d(TAG, "Beginning live refresh with interval ${interval}ms")
|
||||
|
||||
Log.d(TAG, "Beginning live refresh (continuous updates)")
|
||||
|
||||
if (_permissionState.value != PermissionState.AUTHORIZED) {
|
||||
Log.w(TAG, "Cannot start live refresh - permission not authorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!isLocationServicesEnabled()) {
|
||||
Log.w(TAG, "Cannot start live refresh - location services disabled by user")
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel existing timer
|
||||
// Cancel any existing timer-based refreshers
|
||||
refreshTimer?.cancel()
|
||||
|
||||
// Start new timer with coroutines
|
||||
refreshTimer = CoroutineScope(Dispatchers.IO).launch {
|
||||
while (isActive) {
|
||||
if (isLocationServicesEnabled()) {
|
||||
requestOneShotLocation()
|
||||
refreshTimer = null
|
||||
|
||||
// Register for continuous updates from available providers
|
||||
try {
|
||||
if (hasLocationPermission()) {
|
||||
val providers = listOf(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
LocationManager.NETWORK_PROVIDER
|
||||
)
|
||||
|
||||
providers.forEach { provider ->
|
||||
if (locationManager.isProviderEnabled(provider)) {
|
||||
// 2s min time, 5m min distance for responsive yet battery-aware updates
|
||||
locationManager.requestLocationUpdates(
|
||||
provider,
|
||||
interval,
|
||||
5f,
|
||||
continuousLocationListener
|
||||
)
|
||||
Log.d(TAG, "Registered continuous updates for $provider")
|
||||
}
|
||||
}
|
||||
delay(interval)
|
||||
|
||||
// Prime state immediately with last known / current location
|
||||
requestOneShotLocation()
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Missing location permission for continuous updates: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to register continuous updates: ${e.message}")
|
||||
}
|
||||
|
||||
// Kick off immediately
|
||||
requestOneShotLocation()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,6 +182,12 @@ class LocationChannelManager private constructor(private val context: Context) {
|
||||
Log.d(TAG, "Ending live refresh")
|
||||
refreshTimer?.cancel()
|
||||
refreshTimer = null
|
||||
// Unregister continuous updates listener
|
||||
try {
|
||||
locationManager.removeUpdates(continuousLocationListener)
|
||||
} catch (_: SecurityException) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,14 +322,38 @@ class LocationChannelManager private constructor(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Continuous location listener for real-time updates
|
||||
private val continuousLocationListener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
Log.d(TAG, "Real-time location: ${location.latitude}, ${location.longitude} acc=${location.accuracy}m")
|
||||
lastLocation = location
|
||||
_isLoadingLocation.value = false
|
||||
computeChannels(location)
|
||||
reverseGeocodeIfNeeded(location)
|
||||
}
|
||||
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
|
||||
// Deprecated but can still be called on older devices
|
||||
Log.v(TAG, "Provider status changed: $provider -> $status")
|
||||
}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {
|
||||
Log.d(TAG, "Provider enabled: $provider")
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
Log.d(TAG, "Provider disabled: $provider")
|
||||
}
|
||||
}
|
||||
|
||||
// One-time location listener to get a fresh location update
|
||||
private val oneShotLocationListener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
Log.d(TAG, "Fresh location received: ${location.latitude}, ${location.longitude}")
|
||||
Log.d(TAG, "One-shot location: ${location.latitude}, ${location.longitude}")
|
||||
lastLocation = location
|
||||
computeChannels(location)
|
||||
reverseGeocodeIfNeeded(location)
|
||||
|
||||
|
||||
// Update loading state to indicate we have a location now
|
||||
_isLoadingLocation.value = false
|
||||
|
||||
@ -315,6 +364,18 @@ class LocationChannelManager private constructor(private val context: Context) {
|
||||
Log.e(TAG, "Error removing location listener: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
|
||||
// Required for compatibility with older platform versions
|
||||
}
|
||||
|
||||
override fun onProviderEnabled(provider: String) {
|
||||
// Required for compatibility with older platform versions
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
// Required for compatibility with older platform versions
|
||||
}
|
||||
}
|
||||
|
||||
// Request a fresh location update using getCurrentLocation instead of continuous updates
|
||||
@ -663,16 +724,9 @@ class LocationChannelManager private constructor(private val context: Context) {
|
||||
Log.d(TAG, "Cleaning up LocationChannelManager")
|
||||
endLiveRefresh()
|
||||
|
||||
// For older Android versions, remove any remaining location listener to prevent memory leaks
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
|
||||
try {
|
||||
locationManager.removeUpdates(oneShotLocationListener)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "Error removing location listener during cleanup: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during cleanup: ${e.message}")
|
||||
}
|
||||
}
|
||||
// Remove listeners to prevent memory leaks
|
||||
try { locationManager.removeUpdates(oneShotLocationListener) } catch (_: Exception) {}
|
||||
try { locationManager.removeUpdates(continuousLocationListener) } catch (_: Exception) {}
|
||||
// For Android 11+, getCurrentLocation doesn't need explicit cleanup as it's a one-time operation
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@ import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import java.security.MessageDigest
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* Manages persistent identity storage and peer ID rotation - 100% compatible with iOS implementation
|
||||
@ -24,9 +27,15 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
private const val KEY_STATIC_PUBLIC_KEY = "static_public_key"
|
||||
private const val KEY_SIGNING_PRIVATE_KEY = "signing_private_key"
|
||||
private const val KEY_SIGNING_PUBLIC_KEY = "signing_public_key"
|
||||
private const val KEY_VERIFIED_FINGERPRINTS = "verified_fingerprints"
|
||||
private const val KEY_CACHED_PEER_FINGERPRINTS = "cached_peer_fingerprints"
|
||||
private const val KEY_CACHED_PEER_NOISE_KEYS = "cached_peer_noise_keys"
|
||||
private const val KEY_CACHED_NOISE_FINGERPRINTS = "cached_noise_fingerprints"
|
||||
private const val KEY_CACHED_FINGERPRINT_NICKNAMES = "cached_fingerprint_nicknames"
|
||||
}
|
||||
|
||||
private val prefs: SharedPreferences
|
||||
private val lock = Any()
|
||||
|
||||
init {
|
||||
// Create master key for encryption
|
||||
@ -168,7 +177,7 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
fun generateFingerprint(publicKeyData: ByteArray): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKeyData)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
return hash.hexEncodedString()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,6 +187,112 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
// SHA-256 fingerprint should be 64 hex characters
|
||||
return fingerprint.matches(Regex("^[a-fA-F0-9]{64}$"))
|
||||
}
|
||||
|
||||
// MARK: - Verified Fingerprints
|
||||
|
||||
fun getVerifiedFingerprints(): Set<String> {
|
||||
return prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
fun isVerifiedFingerprint(fingerprint: String): Boolean {
|
||||
return getVerifiedFingerprints().contains(fingerprint)
|
||||
}
|
||||
|
||||
fun setVerifiedFingerprint(fingerprint: String, verified: Boolean) {
|
||||
if (!isValidFingerprint(fingerprint)) return
|
||||
synchronized(lock) {
|
||||
val current = prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (verified) {
|
||||
current.add(fingerprint)
|
||||
} else {
|
||||
current.remove(fingerprint)
|
||||
}
|
||||
prefs.edit { putStringSet(KEY_VERIFIED_FINGERPRINTS, current) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedPeerFingerprint(peerID: String): String? {
|
||||
val pid = peerID.lowercase()
|
||||
// Reading is safe without lock for SharedPreferences, but synchronizing ensures memory visibility
|
||||
// if we are paranoid, but SharedPreferences is generally thread-safe for reads.
|
||||
// However, to ensure we don't read a partial update (unlikely with SP), we can leave it.
|
||||
// The critical part is the write.
|
||||
val entries = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet()) ?: return null
|
||||
val entry = entries.firstOrNull { it.startsWith("$pid:") } ?: return null
|
||||
return entry.substringAfter(':').takeIf { isValidFingerprint(it) }
|
||||
}
|
||||
|
||||
fun cachePeerFingerprint(peerID: String, fingerprint: String) {
|
||||
if (!isValidFingerprint(fingerprint)) return
|
||||
val pid = peerID.lowercase()
|
||||
synchronized(lock) {
|
||||
val current = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
current.removeAll { it.startsWith("$pid:") }
|
||||
current.add("$pid:$fingerprint")
|
||||
prefs.edit { putStringSet(KEY_CACHED_PEER_FINGERPRINTS, current) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedNoiseKey(peerID: String): String? {
|
||||
val pid = peerID.lowercase()
|
||||
val entries = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet()) ?: return null
|
||||
val entry = entries.firstOrNull { it.startsWith("$pid=") } ?: return null
|
||||
return entry.substringAfter('=').takeIf { it.matches(Regex("^[a-fA-F0-9]{64}$")) }
|
||||
}
|
||||
|
||||
fun cachePeerNoiseKey(peerID: String, noiseKeyHex: String) {
|
||||
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
||||
val pid = peerID.lowercase()
|
||||
synchronized(lock) {
|
||||
val current = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
current.removeAll { it.startsWith("$pid=") }
|
||||
current.add("$pid=${noiseKeyHex.lowercase()}")
|
||||
prefs.edit { putStringSet(KEY_CACHED_PEER_NOISE_KEYS, current) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedNoiseFingerprint(noiseKeyHex: String): String? {
|
||||
val key = noiseKeyHex.lowercase()
|
||||
val entries = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet()) ?: return null
|
||||
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
|
||||
return entry.substringAfter('=').takeIf { isValidFingerprint(it) }
|
||||
}
|
||||
|
||||
fun cacheNoiseFingerprint(noiseKeyHex: String, fingerprint: String) {
|
||||
if (!isValidFingerprint(fingerprint)) return
|
||||
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
|
||||
val key = noiseKeyHex.lowercase()
|
||||
synchronized(lock) {
|
||||
val current = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
current.removeAll { it.startsWith("$key=") }
|
||||
current.add("$key=$fingerprint")
|
||||
prefs.edit { putStringSet(KEY_CACHED_NOISE_FINGERPRINTS, current) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedFingerprintNickname(fingerprint: String): String? {
|
||||
if (!isValidFingerprint(fingerprint)) return null
|
||||
val key = fingerprint.lowercase()
|
||||
val entries = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet()) ?: return null
|
||||
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
|
||||
val encoded = entry.substringAfter('=')
|
||||
return runCatching {
|
||||
val bytes = Base64.decode(encoded, Base64.NO_WRAP)
|
||||
String(bytes, Charsets.UTF_8)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun cacheFingerprintNickname(fingerprint: String, nickname: String) {
|
||||
if (!isValidFingerprint(fingerprint)) return
|
||||
val key = fingerprint.lowercase()
|
||||
val encoded = Base64.encodeToString(nickname.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
synchronized(lock) {
|
||||
val current = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
current.removeAll { it.startsWith("$key=") }
|
||||
current.add("$key=$encoded")
|
||||
prefs.edit { putStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, current) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Peer ID Rotation Management (removed)
|
||||
// Android now derives peer ID from the persisted Noise identity fingerprint.
|
||||
|
||||
@ -247,13 +247,6 @@ class BluetoothConnectionManager(
|
||||
return active
|
||||
}
|
||||
|
||||
/**
|
||||
* Set app background state for power optimization
|
||||
*/
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
powerManager.setAppBackgroundState(inBackground)
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast packet to connected devices with connection limit enforcement
|
||||
* Automatically fragments large packets to fit within BLE MTU limits
|
||||
|
||||
@ -7,12 +7,15 @@ import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.protocol.MessagePadding
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.model.NoisePayload
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlin.math.sign
|
||||
@ -72,6 +75,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID")
|
||||
VerificationService.configure(encryptionService)
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
//startPeriodicDebugLogging()
|
||||
@ -414,6 +418,14 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||
}
|
||||
|
||||
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
|
||||
}
|
||||
|
||||
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
|
||||
}
|
||||
}
|
||||
|
||||
// PacketProcessor delegates
|
||||
@ -939,6 +951,50 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: QR Verification over Noise
|
||||
|
||||
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||
val tlv = VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.VERIFY_CHALLENGE,
|
||||
data = tlv
|
||||
)
|
||||
sendNoisePayloadToPeer(payload, peerID, "verify challenge")
|
||||
}
|
||||
|
||||
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||
val tlv = VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.VERIFY_RESPONSE,
|
||||
data = tlv
|
||||
)
|
||||
sendNoisePayloadToPeer(payload, peerID, "verify response")
|
||||
}
|
||||
|
||||
private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||
)
|
||||
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||
@ -1127,6 +1183,10 @@ class BluetoothMeshService(private val context: Context) {
|
||||
fun getIdentityFingerprint(): String {
|
||||
return encryptionService.getIdentityFingerprint()
|
||||
}
|
||||
|
||||
fun getStaticNoisePublicKey(): ByteArray? {
|
||||
return encryptionService.getStaticPublicKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if encryption icon should be shown for a peer
|
||||
@ -1283,6 +1343,8 @@ interface BluetoothMeshDelegate {
|
||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun getNickname(): String?
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
|
||||
@ -33,9 +33,9 @@ class BluetoothPermissionManager(private val context: Context) {
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
))
|
||||
|
||||
|
||||
return permissions.all {
|
||||
ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,6 +157,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
// Simplified: Call delegate with messageID and peerID directly
|
||||
delegate?.onReadReceiptReceived(messageID, peerID)
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
|
||||
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
|
||||
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@ -611,4 +619,6 @@ interface MessageHandlerDelegate {
|
||||
fun onChannelLeave(channel: String, fromPeer: String)
|
||||
fun onDeliveryAckReceived(messageID: String, peerID: String)
|
||||
fun onReadReceiptReceived(messageID: String, peerID: String)
|
||||
fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||
}
|
||||
|
||||
@ -299,8 +299,7 @@ class PeerManager {
|
||||
*/
|
||||
fun isPeerActive(peerID: String): Boolean {
|
||||
val info = peers[peerID] ?: return false
|
||||
val now = System.currentTimeMillis()
|
||||
return (now - info.lastSeen) <= stalePeerTimeoutMs && info.isConnected
|
||||
return info.isConnected
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,8 +327,7 @@ class PeerManager {
|
||||
* Get list of active peer IDs
|
||||
*/
|
||||
fun getActivePeerIDs(): List<String> {
|
||||
val now = System.currentTimeMillis()
|
||||
return peers.filterValues { (now - it.lastSeen) <= stalePeerTimeoutMs && it.isConnected }
|
||||
return peers.filterValues { it.isConnected }
|
||||
.keys
|
||||
.toList()
|
||||
.sorted()
|
||||
|
||||
@ -7,7 +7,13 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.BatteryManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.math.max
|
||||
|
||||
@ -15,7 +21,7 @@ import kotlin.math.max
|
||||
* Power-aware Bluetooth management for bitchat
|
||||
* Adjusts scanning, advertising, and connection behavior based on battery state
|
||||
*/
|
||||
class PowerManager(private val context: Context) {
|
||||
class PowerManager(private val context: Context) : LifecycleEventObserver {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PowerManager"
|
||||
@ -49,7 +55,7 @@ class PowerManager(private val context: Context) {
|
||||
private var currentMode = PowerMode.BALANCED
|
||||
private var isCharging = false
|
||||
private var batteryLevel = 100
|
||||
private var isAppInBackground = false
|
||||
private var isAppInBackground = true
|
||||
|
||||
private val powerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var dutyCycleJob: Job? = null
|
||||
@ -87,6 +93,16 @@ class PowerManager(private val context: Context) {
|
||||
|
||||
init {
|
||||
registerBatteryReceiver()
|
||||
|
||||
// Register for process lifecycle events on the main thread
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to register lifecycle observer: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
updatePowerMode()
|
||||
}
|
||||
|
||||
@ -99,13 +115,30 @@ class PowerManager(private val context: Context) {
|
||||
Log.i(TAG, "Stopping power management")
|
||||
powerScope.cancel()
|
||||
unregisterBatteryReceiver()
|
||||
|
||||
// Unregister lifecycle observer
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
ProcessLifecycleOwner.get().lifecycle.removeObserver(this)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to remove lifecycle observer: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
if (isAppInBackground != inBackground) {
|
||||
isAppInBackground = inBackground
|
||||
Log.d(TAG, "App background state changed: $inBackground")
|
||||
updatePowerMode()
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
Log.d(TAG, "Process lifecycle: ON_START (App coming to foreground)")
|
||||
isAppInBackground = false
|
||||
updatePowerMode()
|
||||
}
|
||||
Lifecycle.Event.ON_STOP -> {
|
||||
Log.d(TAG, "Process lifecycle: ON_STOP (App going to background)")
|
||||
isAppInBackground = true
|
||||
updatePowerMode()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +166,7 @@ class PowerManager(private val context: Context) {
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
|
||||
PowerMode.ULTRA_LOW_POWER -> builder
|
||||
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
|
||||
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
}
|
||||
|
||||
@ -56,21 +56,30 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
|
||||
// Duplicate detection
|
||||
val messageID = generateMessageID(packet, peerID)
|
||||
if (messageType != MessageType.ANNOUNCE) {
|
||||
if (processedMessages.contains(messageID)) {
|
||||
|
||||
if (processedMessages.contains(messageID)) {
|
||||
// Check for ANNOUNCE exception: allow if it looks like a direct neighbor (max TTL)
|
||||
// This ensures we catch the "first announce" on a new connection for binding,
|
||||
// while still dropping looped/relayed duplicates.
|
||||
val isFreshAnnounce = messageType == MessageType.ANNOUNCE &&
|
||||
packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||
|
||||
if (!isFreshAnnounce) {
|
||||
Log.d(TAG, "Dropping duplicate packet: $messageID")
|
||||
return false
|
||||
}
|
||||
// Add to processed messages
|
||||
processedMessages.add(messageID)
|
||||
messageTimestamps[messageID] = currentTime
|
||||
} else {
|
||||
// Do not deduplicate ANNOUNCE at the security layer.
|
||||
// They are signed/idempotent and we need to ensure first-announce per-connection can bind.
|
||||
Log.d(TAG, "Allowing duplicate ANNOUNCE from direct neighbor: $messageID")
|
||||
}
|
||||
|
||||
// Add to processed messages
|
||||
processedMessages.add(messageID)
|
||||
messageTimestamps[messageID] = currentTime
|
||||
|
||||
// NEW: Signature verification logging (not rejecting yet)
|
||||
verifyPacketSignatureWithLogging(packet, peerID)
|
||||
// Enforce mandatory signature verification
|
||||
if (!verifyPacketSignature(packet, peerID)) {
|
||||
Log.w(TAG, "Dropping packet from $peerID due to signature verification failure")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID")
|
||||
return true
|
||||
@ -223,34 +232,58 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify packet signature using peer's signing public key and log the result
|
||||
* Verify packet signature using peer's signing public key
|
||||
* Returns true only if signature is present and valid
|
||||
*/
|
||||
private fun verifyPacketSignatureWithLogging(packet: BitchatPacket, peerID: String) {
|
||||
private fun verifyPacketSignature(packet: BitchatPacket, peerID: String): Boolean {
|
||||
try {
|
||||
// Check if packet has a signature
|
||||
// only verify ANNOUNCE, MESSAGE, and FILE_TRANSFER
|
||||
if (MessageType.fromValue(packet.type) !in setOf(
|
||||
MessageType.ANNOUNCE,
|
||||
MessageType.MESSAGE,
|
||||
MessageType.FILE_TRANSFER
|
||||
)) {
|
||||
return true
|
||||
}
|
||||
// 1. Mandatory Signature Check
|
||||
if (packet.signature == null) {
|
||||
Log.d(TAG, "📝 Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})")
|
||||
return
|
||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})")
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to get peer's signing public key from peer info
|
||||
val peerInfo = delegate?.getPeerInfo(peerID)
|
||||
val signingPublicKey = peerInfo?.signingPublicKey
|
||||
// 2. Get Signing Public Key
|
||||
var signingPublicKey: ByteArray? = null
|
||||
|
||||
if (MessageType.fromValue(packet.type) == MessageType.ANNOUNCE) {
|
||||
// Special Case: ANNOUNCE packets carry their own signing key
|
||||
try {
|
||||
val announcement = com.bitchat.android.model.IdentityAnnouncement.decode(packet.payload)
|
||||
signingPublicKey = announcement?.signingPublicKey
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to decode announcement for key extraction: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
// Standard Case: Get key from known peer info
|
||||
val peerInfo = delegate?.getPeerInfo(peerID)
|
||||
signingPublicKey = peerInfo?.signingPublicKey
|
||||
}
|
||||
|
||||
if (signingPublicKey == null) {
|
||||
Log.d(TAG, "📝 Signature check for $peerID: NO_SIGNING_KEY (packet type ${packet.type})")
|
||||
return
|
||||
// If we don't have a key (and it's not an announce), we can't verify.
|
||||
// For security, we must reject packets from unknown peers unless it's an announce.
|
||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})")
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the canonical packet data for signature verification (without signature)
|
||||
// 3. Get Canonical Data
|
||||
val packetDataForSigning = packet.toBinaryDataForSigning()
|
||||
if (packetDataForSigning == null) {
|
||||
Log.w(TAG, "📝 Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})")
|
||||
return
|
||||
Log.w(TAG, "❌ Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})")
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify the signature using the peer's signing public key
|
||||
val signature = packet.signature!! // We already checked for null above
|
||||
// 4. Verify Signature
|
||||
val signature = packet.signature!!
|
||||
val isSignatureValid = encryptionService.verifyEd25519Signature(
|
||||
signature,
|
||||
packetDataForSigning,
|
||||
@ -258,13 +291,16 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
)
|
||||
|
||||
if (isSignatureValid) {
|
||||
Log.d(TAG, "📝 Signature check for $peerID: ✅ VALID (packet type ${packet.type})")
|
||||
// Log.v(TAG, "✅ Signature verified for $peerID (type ${packet.type})")
|
||||
return true
|
||||
} else {
|
||||
Log.w(TAG, "📝 Signature check for $peerID: ❌ INVALID (packet type ${packet.type})")
|
||||
Log.w(TAG, "❌ Signature INVALID for $peerID (type ${packet.type})")
|
||||
return false
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "📝 Signature check for $peerID: ERROR - ${e.message} (packet type ${packet.type})")
|
||||
Log.e(TAG, "❌ Signature verification error for $peerID: ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ enum class NoisePayloadType(val value: UByte) {
|
||||
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
|
||||
READ_RECEIPT(0x02u), // Message was read
|
||||
DELIVERED(0x03u), // Message was delivered
|
||||
VERIFY_CHALLENGE(0x10u), // Verification challenge
|
||||
VERIFY_RESPONSE(0x11u), // Verification response
|
||||
FILE_TRANSFER(0x20u);
|
||||
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ package com.bitchat.android.nostr
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.model.NoisePayload
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.model.PrivateMessagePacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.services.SeenMessageStore
|
||||
import com.bitchat.android.ui.ChatState
|
||||
@ -71,7 +75,7 @@ class NostrDirectMessageHandler(
|
||||
|
||||
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
|
||||
|
||||
val noisePayload = com.bitchat.android.model.NoisePayload.decode(packet.payload) ?: return@launch
|
||||
val noisePayload = NoisePayload.decode(packet.payload) ?: return@launch
|
||||
val messageTimestamp = Date(giftWrap.createdAt * 1000L)
|
||||
val convKey = "nostr_${senderPubkey.take(16)}"
|
||||
repo.putNostrKeyMapping(convKey, senderPubkey)
|
||||
@ -104,7 +108,7 @@ class NostrDirectMessageHandler(
|
||||
}
|
||||
|
||||
private suspend fun processNoisePayload(
|
||||
payload: com.bitchat.android.model.NoisePayload,
|
||||
payload: NoisePayload,
|
||||
convKey: String,
|
||||
senderNickname: String,
|
||||
timestamp: Date,
|
||||
@ -112,8 +116,8 @@ class NostrDirectMessageHandler(
|
||||
recipientIdentity: NostrIdentity
|
||||
) {
|
||||
when (payload.type) {
|
||||
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||
val pm = com.bitchat.android.model.PrivateMessagePacket.decode(payload.data) ?: return
|
||||
NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||
val pm = PrivateMessagePacket.decode(payload.data) ?: return
|
||||
val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList()
|
||||
if (existingMessages.any { it.id == pm.messageID }) return
|
||||
|
||||
@ -148,21 +152,21 @@ class NostrDirectMessageHandler(
|
||||
seenStore.markRead(pm.messageID)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||
NoisePayloadType.DELIVERED -> {
|
||||
val messageId = String(payload.data, Charsets.UTF_8)
|
||||
withContext(Dispatchers.Main) {
|
||||
meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
||||
NoisePayloadType.READ_RECEIPT -> {
|
||||
val messageId = String(payload.data, Charsets.UTF_8)
|
||||
withContext(Dispatchers.Main) {
|
||||
meshDelegateHandler.didReceiveReadReceipt(messageId, convKey)
|
||||
}
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> {
|
||||
NoisePayloadType.FILE_TRANSFER -> {
|
||||
// Properly handle encrypted file transfer
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(payload.data)
|
||||
val file = BitchatFilePacket.decode(payload.data)
|
||||
if (file != null) {
|
||||
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(application, file)
|
||||
@ -185,6 +189,8 @@ class NostrDirectMessageHandler(
|
||||
Log.w(TAG, "⚠️ Failed to decode Nostr file transfer from $convKey")
|
||||
}
|
||||
}
|
||||
NoisePayloadType.VERIFY_CHALLENGE,
|
||||
NoisePayloadType.VERIFY_RESPONSE -> Unit // Ignore verification payloads in Nostr direct messages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,251 @@
|
||||
package com.bitchat.android.onboarding
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitchat.android.R
|
||||
|
||||
/**
|
||||
* Explanation screen shown before requesting background location permission.
|
||||
*/
|
||||
@Composable
|
||||
fun BackgroundLocationPermissionScreen(
|
||||
modifier: Modifier,
|
||||
onContinue: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onSkip: () -> Unit
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 88.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
HeaderSection(colorScheme)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.LocationOn,
|
||||
contentDescription = stringResource(R.string.cd_location_services),
|
||||
tint = colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_required_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_explanation),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colorScheme.onBackground.copy(alpha = 0.8f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_settings_tip),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
color = colorScheme.onBackground.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Security,
|
||||
contentDescription = stringResource(R.string.cd_privacy_protected),
|
||||
tint = colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_needs_for),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_needs_bullets),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = colorScheme.onBackground.copy(alpha = 0.8f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_privacy_note),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium
|
||||
),
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
color = colorScheme.surface,
|
||||
shadowElevation = 8.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onContinue,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.grant_background_location),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onRetry,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.check_again),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onSkip,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.battery_optimization_skip),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeaderSection(colorScheme: ColorScheme) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.headlineLarge.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 32.sp
|
||||
),
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.background_location_required_subtitle),
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = colorScheme.onBackground.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.bitchat.android.onboarding
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Preference manager for background location skip choice.
|
||||
*/
|
||||
object BackgroundLocationPreferenceManager {
|
||||
private const val PREFS_NAME = "bitchat_settings"
|
||||
private const val KEY_BACKGROUND_LOCATION_SKIP = "background_location_skipped"
|
||||
|
||||
fun setSkipped(context: Context, skipped: Boolean) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putBoolean(KEY_BACKGROUND_LOCATION_SKIP, skipped).apply()
|
||||
}
|
||||
|
||||
fun isSkipped(context: Context): Boolean {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getBoolean(KEY_BACKGROUND_LOCATION_SKIP, false)
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ class OnboardingCoordinator(
|
||||
private val activity: ComponentActivity,
|
||||
private val permissionManager: PermissionManager,
|
||||
private val onOnboardingComplete: () -> Unit,
|
||||
private val onBackgroundLocationRequired: () -> Unit,
|
||||
private val onOnboardingFailed: (String) -> Unit
|
||||
) {
|
||||
|
||||
@ -27,9 +28,11 @@ class OnboardingCoordinator(
|
||||
}
|
||||
|
||||
private var permissionLauncher: ActivityResultLauncher<Array<String>>? = null
|
||||
private var backgroundLocationLauncher: ActivityResultLauncher<String>? = null
|
||||
|
||||
init {
|
||||
setupPermissionLauncher()
|
||||
setupBackgroundLocationLauncher()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,6 +46,14 @@ class OnboardingCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBackgroundLocationLauncher() {
|
||||
backgroundLocationLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { granted ->
|
||||
handleBackgroundLocationResult(granted)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the onboarding process
|
||||
*/
|
||||
@ -50,9 +61,14 @@ class OnboardingCoordinator(
|
||||
Log.d(TAG, "Starting onboarding process")
|
||||
permissionManager.logPermissionStatus()
|
||||
|
||||
if (permissionManager.areAllPermissionsGranted()) {
|
||||
Log.d(TAG, "All permissions already granted, completing onboarding")
|
||||
completeOnboarding()
|
||||
if (permissionManager.areRequiredPermissionsGranted()) {
|
||||
if (shouldRequestBackgroundLocation()) {
|
||||
Log.d(TAG, "Foreground permissions granted; background location recommended")
|
||||
onBackgroundLocationRequired()
|
||||
} else {
|
||||
Log.d(TAG, "Required permissions already granted, completing onboarding")
|
||||
completeOnboarding()
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Missing permissions, need to start explanation flow")
|
||||
// The explanation screen will be shown by the calling activity
|
||||
@ -76,7 +92,11 @@ class OnboardingCoordinator(
|
||||
val missingPermissions = (missingRequired + optionalToRequest).distinct()
|
||||
|
||||
if (missingPermissions.isEmpty()) {
|
||||
completeOnboarding()
|
||||
if (shouldRequestBackgroundLocation()) {
|
||||
onBackgroundLocationRequired()
|
||||
} else {
|
||||
completeOnboarding()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -98,13 +118,19 @@ class OnboardingCoordinator(
|
||||
val criticalGranted = criticalPermissions.all { permissions[it] == true }
|
||||
|
||||
when {
|
||||
allGranted -> {
|
||||
Log.d(TAG, "All permissions granted successfully")
|
||||
completeOnboarding()
|
||||
}
|
||||
criticalGranted -> {
|
||||
Log.d(TAG, "Critical permissions granted, can proceed with limited functionality")
|
||||
showPartialPermissionWarning(permissions)
|
||||
if (shouldRequestBackgroundLocation()) {
|
||||
Log.d(TAG, "Foreground permissions granted; requesting background location next")
|
||||
onBackgroundLocationRequired()
|
||||
return
|
||||
}
|
||||
if (allGranted) {
|
||||
Log.d(TAG, "All permissions granted successfully")
|
||||
completeOnboarding()
|
||||
} else {
|
||||
Log.d(TAG, "Critical permissions granted, can proceed with limited functionality")
|
||||
showPartialPermissionWarning(permissions)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Critical permissions denied")
|
||||
@ -113,15 +139,50 @@ class OnboardingCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
fun requestBackgroundLocation() {
|
||||
val permission = permissionManager.getBackgroundLocationPermission()
|
||||
if (permission == null) {
|
||||
completeOnboarding()
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "Requesting background location permission")
|
||||
backgroundLocationLauncher?.launch(permission)
|
||||
}
|
||||
|
||||
private fun handleBackgroundLocationResult(granted: Boolean) {
|
||||
if (granted) {
|
||||
Log.d(TAG, "Background location permission granted")
|
||||
} else {
|
||||
Log.w(TAG, "Background location permission denied; continuing without it")
|
||||
}
|
||||
completeOnboarding()
|
||||
}
|
||||
|
||||
fun skipBackgroundLocation() {
|
||||
Log.d(TAG, "User skipped background location permission")
|
||||
BackgroundLocationPreferenceManager.setSkipped(activity, true)
|
||||
completeOnboarding()
|
||||
}
|
||||
|
||||
fun checkBackgroundLocationAndProceed() {
|
||||
if (!shouldRequestBackgroundLocation()) {
|
||||
completeOnboarding()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldRequestBackgroundLocation(): Boolean {
|
||||
return permissionManager.needsBackgroundLocationPermission() &&
|
||||
!permissionManager.isBackgroundLocationGranted() &&
|
||||
!BackgroundLocationPreferenceManager.isSkipped(activity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of critical permissions that are absolutely required
|
||||
*/
|
||||
private fun getCriticalPermissions(): List<String> {
|
||||
// For bitchat, Bluetooth and location permissions are critical
|
||||
// Notifications are nice-to-have but not critical
|
||||
return permissionManager.getRequiredPermissions().filter { permission ->
|
||||
!permission.contains("POST_NOTIFICATIONS")
|
||||
}
|
||||
// Notifications are nice-to-have but not critical and are not included in getRequiredPermissions()
|
||||
return permissionManager.getRequiredPermissions()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,6 +269,7 @@ class OnboardingCoordinator(
|
||||
private fun getPermissionDisplayName(permission: String): String {
|
||||
return when {
|
||||
permission.contains("BLUETOOTH") -> "Bluetooth/Nearby Devices"
|
||||
permission.contains("BACKGROUND") -> "Background Location"
|
||||
permission.contains("LOCATION") -> "Location (for Bluetooth scanning)"
|
||||
permission.contains("NOTIFICATION") -> "Notifications"
|
||||
else -> permission.substringAfterLast(".")
|
||||
|
||||
@ -6,8 +6,9 @@ enum class OnboardingState {
|
||||
LOCATION_CHECK,
|
||||
BATTERY_OPTIMIZATION_CHECK,
|
||||
PERMISSION_EXPLANATION,
|
||||
BACKGROUND_LOCATION_EXPLANATION,
|
||||
PERMISSION_REQUESTING,
|
||||
INITIALIZING,
|
||||
COMPLETE,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,12 +12,10 @@ import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -209,29 +207,6 @@ private fun PermissionCategoryCard(
|
||||
color = colorScheme.onBackground.copy(alpha = 0.8f)
|
||||
)
|
||||
|
||||
if (category.type == PermissionType.PRECISE_LOCATION) {
|
||||
// Extra emphasis for location permission
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Warning,
|
||||
contentDescription = stringResource(R.string.cd_warning),
|
||||
tint = Color(0xFFFF9800),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.location_tracking_warning),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFFF9800)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,6 +215,7 @@ private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
|
||||
return when (permissionType) {
|
||||
PermissionType.NEARBY_DEVICES -> Icons.Filled.Bluetooth
|
||||
PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
|
||||
PermissionType.BACKGROUND_LOCATION -> Icons.Filled.LocationOn
|
||||
PermissionType.MICROPHONE -> Icons.Filled.Mic
|
||||
PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
|
||||
PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
|
||||
|
||||
@ -7,6 +7,7 @@ import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bitchat.android.R
|
||||
|
||||
/**
|
||||
* Centralized permission management for bitchat app
|
||||
@ -40,7 +41,8 @@ class PermissionManager(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions required by the app
|
||||
* Get required permissions that can be requested together.
|
||||
* Background location is handled separately to ensure correct request order.
|
||||
* Note: Notification permission is optional and not included here,
|
||||
* so the app works without notification access.
|
||||
*/
|
||||
@ -72,6 +74,27 @@ class PermissionManager(private val context: Context) {
|
||||
return permissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Background location permission is required on Android 10+ for background BLE scanning.
|
||||
* Must be requested after foreground location permissions are granted.
|
||||
*/
|
||||
fun needsBackgroundLocationPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
fun getBackgroundLocationPermission(): String? {
|
||||
return if (needsBackgroundLocationPermission()) {
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun isBackgroundLocationGranted(): Boolean {
|
||||
val permission = getBackgroundLocationPermission() ?: return true
|
||||
return isPermissionGranted(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optional permissions that improve the experience but aren't required.
|
||||
* Currently includes POST_NOTIFICATIONS on Android 13+.
|
||||
@ -93,9 +116,13 @@ class PermissionManager(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all required permissions are granted
|
||||
* Check if all required permissions are granted (background location is optional).
|
||||
*/
|
||||
fun areAllPermissionsGranted(): Boolean {
|
||||
return areRequiredPermissionsGranted()
|
||||
}
|
||||
|
||||
fun areRequiredPermissionsGranted(): Boolean {
|
||||
return getRequiredPermissions().all { isPermissionGranted(it) }
|
||||
}
|
||||
|
||||
@ -131,6 +158,11 @@ class PermissionManager(private val context: Context) {
|
||||
return getRequiredPermissions().filter { !isPermissionGranted(it) }
|
||||
}
|
||||
|
||||
fun getMissingBackgroundLocationPermission(): List<String> {
|
||||
val permission = getBackgroundLocationPermission() ?: return emptyList()
|
||||
return if (isPermissionGranted(permission)) emptyList() else listOf(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categorized permission information for display
|
||||
*/
|
||||
@ -177,6 +209,19 @@ class PermissionManager(private val context: Context) {
|
||||
)
|
||||
)
|
||||
|
||||
if (needsBackgroundLocationPermission()) {
|
||||
val backgroundPermission = listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
categories.add(
|
||||
PermissionCategory(
|
||||
type = PermissionType.BACKGROUND_LOCATION,
|
||||
description = context.getString(R.string.perm_background_location_desc),
|
||||
permissions = backgroundPermission,
|
||||
isGranted = backgroundPermission.all { isPermissionGranted(it) },
|
||||
systemDescription = context.getString(R.string.perm_background_location_system)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Notifications category (if applicable)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
categories.add(
|
||||
@ -216,7 +261,7 @@ class PermissionManager(private val context: Context) {
|
||||
appendLine("Permission Diagnostics:")
|
||||
appendLine("Android SDK: ${Build.VERSION.SDK_INT}")
|
||||
appendLine("First time launch: ${isFirstTimeLaunch()}")
|
||||
appendLine("All permissions granted: ${areAllPermissionsGranted()}")
|
||||
appendLine("Required permissions granted: ${areAllPermissionsGranted()}")
|
||||
appendLine()
|
||||
|
||||
getCategorizedPermissions().forEach { category ->
|
||||
@ -228,7 +273,7 @@ class PermissionManager(private val context: Context) {
|
||||
appendLine()
|
||||
}
|
||||
|
||||
val missing = getMissingPermissions()
|
||||
val missing = getMissingPermissions() + getMissingBackgroundLocationPermission()
|
||||
if (missing.isNotEmpty()) {
|
||||
appendLine("Missing permissions:")
|
||||
missing.forEach { permission ->
|
||||
@ -260,6 +305,7 @@ data class PermissionCategory(
|
||||
enum class PermissionType(val nameValue: String) {
|
||||
NEARBY_DEVICES("Nearby Devices"),
|
||||
PRECISE_LOCATION("Precise Location"),
|
||||
BACKGROUND_LOCATION("Background Location"),
|
||||
MICROPHONE("Microphone"),
|
||||
NOTIFICATIONS("Notifications"),
|
||||
BATTERY_OPTIMIZATION("Battery Optimization"),
|
||||
|
||||
@ -9,10 +9,13 @@ import com.bitchat.android.net.TorMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
* Coordinates a full application shutdown:
|
||||
@ -24,6 +27,15 @@ import kotlinx.coroutines.withTimeoutOrNull
|
||||
*/
|
||||
object AppShutdownCoordinator {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private val shutdownToken = AtomicLong(0L)
|
||||
@Volatile
|
||||
private var shutdownJob: Job? = null
|
||||
|
||||
fun cancelPendingShutdown() {
|
||||
shutdownToken.incrementAndGet()
|
||||
shutdownJob?.cancel()
|
||||
shutdownJob = null
|
||||
}
|
||||
|
||||
fun requestFullShutdownAndKill(
|
||||
app: Application,
|
||||
@ -32,7 +44,16 @@ object AppShutdownCoordinator {
|
||||
stopForeground: () -> Unit,
|
||||
stopService: () -> Unit
|
||||
) {
|
||||
scope.launch {
|
||||
val token = shutdownToken.incrementAndGet()
|
||||
shutdownJob?.cancel()
|
||||
val job = scope.launch {
|
||||
// Signal UI to finish gracefully before we kill the process
|
||||
try {
|
||||
val intent = android.content.Intent(com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH)
|
||||
.setPackage(app.packageName)
|
||||
app.sendBroadcast(intent, com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH)
|
||||
} catch (_: Exception) { }
|
||||
|
||||
// Stop mesh (best-effort)
|
||||
try { mesh?.stopServices() } catch (_: Exception) { }
|
||||
|
||||
@ -56,12 +77,19 @@ object AppShutdownCoordinator {
|
||||
}
|
||||
|
||||
// Stop the service itself
|
||||
if (!isActive || shutdownToken.get() != token) return@launch
|
||||
try { stopService() } catch (_: Exception) { }
|
||||
|
||||
// Hard kill the app process
|
||||
if (!isActive || shutdownToken.get() != token) return@launch
|
||||
try { Process.killProcess(Process.myPid()) } catch (_: Exception) { }
|
||||
try { System.exit(0) } catch (_: Exception) { }
|
||||
}
|
||||
shutdownJob = job
|
||||
job.invokeOnCompletion {
|
||||
if (shutdownJob === job) {
|
||||
shutdownJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -115,6 +116,7 @@ class MeshForegroundService : Service() {
|
||||
private val serviceJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
|
||||
private var isInForeground: Boolean = false
|
||||
private var isShuttingDown: Boolean = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -133,6 +135,13 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (isShuttingDown && intent?.action == ACTION_START) {
|
||||
AppShutdownCoordinator.cancelPendingShutdown()
|
||||
isShuttingDown = false
|
||||
}
|
||||
if (isShuttingDown && intent?.action != ACTION_QUIT) {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
// Stop FGS and mesh cleanly
|
||||
@ -145,6 +154,12 @@ class MeshForegroundService : Service() {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_QUIT -> {
|
||||
isShuttingDown = true
|
||||
updateJob?.cancel()
|
||||
updateJob = null
|
||||
try { stopForeground(true) } catch (_: Exception) { }
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
isInForeground = false
|
||||
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
|
||||
AppShutdownCoordinator.requestFullShutdownAndKill(
|
||||
app = application,
|
||||
@ -162,7 +177,7 @@ class MeshForegroundService : Service() {
|
||||
// If we became eligible and are not in foreground yet, promote once
|
||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
||||
val n = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
||||
startForeground(NOTIFICATION_ID, n)
|
||||
startForegroundCompat(n)
|
||||
isInForeground = true
|
||||
} else {
|
||||
updateNotification(force = true)
|
||||
@ -177,7 +192,7 @@ class MeshForegroundService : Service() {
|
||||
// Promote exactly once when eligible, otherwise stay background (or stop)
|
||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
||||
val notification = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
startForegroundCompat(notification)
|
||||
isInForeground = true
|
||||
}
|
||||
|
||||
@ -208,6 +223,7 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
|
||||
private fun ensureMeshStarted() {
|
||||
if (isShuttingDown) return
|
||||
if (!hasBluetoothPermissions()) return
|
||||
try {
|
||||
android.util.Log.d("MeshForegroundService", "Ensuring mesh service is started")
|
||||
@ -218,6 +234,10 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
|
||||
private fun updateNotification(force: Boolean) {
|
||||
if (isShuttingDown) {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
return
|
||||
}
|
||||
val count = meshService?.getActivePeerCount() ?: 0
|
||||
val notification = buildNotification(count)
|
||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
||||
@ -257,7 +277,7 @@ class MeshForegroundService : Service() {
|
||||
} else true
|
||||
}
|
||||
|
||||
private fun buildNotification(activeUsers: Int): Notification {
|
||||
private fun buildNotification(activePeers: Int): Notification {
|
||||
val openIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, openIntent,
|
||||
@ -272,7 +292,7 @@ class MeshForegroundService : Service() {
|
||||
)
|
||||
|
||||
val title = getString(R.string.app_name)
|
||||
val content = getString(R.string.mesh_service_notification_content, activeUsers)
|
||||
val content = getString(R.string.mesh_service_notification_content, activePeers)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
@ -307,6 +327,35 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasLocationPermission(): Boolean {
|
||||
val fine = androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
val coarse = androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
return fine || coarse
|
||||
}
|
||||
|
||||
private fun startForegroundCompat(notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val type = if (hasLocationPermission()) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
}
|
||||
try {
|
||||
startForeground(NOTIFICATION_ID, notification, type)
|
||||
} catch (e: SecurityException) {
|
||||
// Fallback for cases where "While In Use" permission exists but background start is restricted
|
||||
if (type and ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION != 0) {
|
||||
android.util.Log.w("MeshForegroundService", "Failed to start with LOCATION type, falling back to CONNECTED_DEVICE: ${e.message}")
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
updateJob?.cancel()
|
||||
updateJob = null
|
||||
|
||||
@ -0,0 +1,294 @@
|
||||
package com.bitchat.android.services
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.util.AppConstants
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.SecureRandom
|
||||
import androidx.core.net.toUri
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* QR verification helpers: schema, signing, and basic challenge/response helpers.
|
||||
*/
|
||||
object VerificationService {
|
||||
private const val CONTEXT = "bitchat-verify-v1"
|
||||
private const val RESPONSE_CONTEXT = "bitchat-verify-resp-v1"
|
||||
|
||||
private var encryptionServiceRef: WeakReference<EncryptionService>? = null
|
||||
|
||||
fun configure(encryptionService: EncryptionService) {
|
||||
this.encryptionServiceRef = WeakReference(encryptionService)
|
||||
}
|
||||
|
||||
data class VerificationQR(
|
||||
val v: Int,
|
||||
val noiseKeyHex: String,
|
||||
val signKeyHex: String,
|
||||
val npub: String?,
|
||||
val nickname: String,
|
||||
val ts: Long,
|
||||
val nonceB64: String,
|
||||
val sigHex: String
|
||||
) {
|
||||
fun canonicalBytes(): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
|
||||
fun appendField(value: String) {
|
||||
val data = value.toByteArray(Charsets.UTF_8)
|
||||
val len = minOf(data.size, 255)
|
||||
out.write(len)
|
||||
out.write(data, 0, len)
|
||||
}
|
||||
|
||||
appendField(CONTEXT)
|
||||
appendField(v.toString())
|
||||
appendField(noiseKeyHex.lowercase())
|
||||
appendField(signKeyHex.lowercase())
|
||||
appendField(npub ?: "")
|
||||
appendField(nickname)
|
||||
appendField(ts.toString())
|
||||
appendField(nonceB64)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun toUrlString(): String {
|
||||
val builder = Uri.Builder()
|
||||
.scheme("bitchat")
|
||||
.authority("verify")
|
||||
.appendQueryParameter("v", v.toString())
|
||||
.appendQueryParameter("noise", noiseKeyHex)
|
||||
.appendQueryParameter("sign", signKeyHex)
|
||||
.appendQueryParameter("nick", nickname)
|
||||
.appendQueryParameter("ts", ts.toString())
|
||||
.appendQueryParameter("nonce", nonceB64)
|
||||
.appendQueryParameter("sig", sigHex)
|
||||
if (npub != null) {
|
||||
builder.appendQueryParameter("npub", npub)
|
||||
}
|
||||
return builder.build().toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromUrlString(urlString: String): VerificationQR? {
|
||||
val uri = runCatching { urlString.toUri() }.getOrNull() ?: return null
|
||||
if (uri.scheme != "bitchat" || uri.host != "verify") return null
|
||||
|
||||
val vStr = uri.getQueryParameter("v") ?: return null
|
||||
val v = vStr.toIntOrNull() ?: return null
|
||||
val noise = uri.getQueryParameter("noise") ?: return null
|
||||
val sign = uri.getQueryParameter("sign") ?: return null
|
||||
val nick = uri.getQueryParameter("nick") ?: return null
|
||||
val tsStr = uri.getQueryParameter("ts") ?: return null
|
||||
val ts = tsStr.toLongOrNull() ?: return null
|
||||
val nonce = uri.getQueryParameter("nonce") ?: return null
|
||||
val sig = uri.getQueryParameter("sig") ?: return null
|
||||
val npub = uri.getQueryParameter("npub")
|
||||
|
||||
return VerificationQR(
|
||||
v = v,
|
||||
noiseKeyHex = noise,
|
||||
signKeyHex = sign,
|
||||
npub = npub,
|
||||
nickname = nick,
|
||||
ts = ts,
|
||||
nonceB64 = nonce,
|
||||
sigHex = sig
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildMyQRString(nickname: String, npub: String?): String? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val cache = Cache.last
|
||||
if (cache != null && cache.nickname == nickname && cache.npub == npub) {
|
||||
if (System.currentTimeMillis() - cache.builtAtMs < 60_000L) {
|
||||
return cache.value
|
||||
}
|
||||
}
|
||||
|
||||
val noiseKey = service.getStaticPublicKey()?.hexEncodedString() ?: return null
|
||||
val signKey = service.getSigningPublicKey()?.hexEncodedString() ?: return null
|
||||
val ts = System.currentTimeMillis() / 1000L
|
||||
val nonce = ByteArray(16)
|
||||
SecureRandom().nextBytes(nonce)
|
||||
val nonceB64 = Base64.encodeToString(
|
||||
nonce,
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
)
|
||||
|
||||
val payload = VerificationQR(
|
||||
v = 1,
|
||||
noiseKeyHex = noiseKey,
|
||||
signKeyHex = signKey,
|
||||
npub = npub,
|
||||
nickname = nickname,
|
||||
ts = ts,
|
||||
nonceB64 = nonceB64,
|
||||
sigHex = ""
|
||||
)
|
||||
|
||||
val signature = service.signData(payload.canonicalBytes()) ?: return null
|
||||
val signed = payload.copy(sigHex = signature.hexEncodedString())
|
||||
val out = signed.toUrlString()
|
||||
Cache.last = CacheEntry(nickname, npub, System.currentTimeMillis(), out)
|
||||
return out
|
||||
}
|
||||
|
||||
fun verifyScannedQR(
|
||||
urlString: String,
|
||||
maxAgeSeconds: Long = AppConstants.Verification.QR_MAX_AGE_SECONDS
|
||||
): VerificationQR? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val qr = VerificationQR.fromUrlString(urlString) ?: return null
|
||||
val now = System.currentTimeMillis() / 1000L
|
||||
if (now - qr.ts > maxAgeSeconds) return null
|
||||
|
||||
val sig = qr.sigHex.dataFromHexString() ?: return null
|
||||
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
|
||||
val ok = service.verifyEd25519Signature(sig, qr.canonicalBytes(), signKey)
|
||||
return if (ok) qr else null
|
||||
}
|
||||
|
||||
fun buildVerifyChallenge(noiseKeyHex: String, nonceA: ByteArray): ByteArray {
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x01)
|
||||
out.write(minOf(noiseData.size, 255))
|
||||
out.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
out.write(0x02)
|
||||
out.write(minOf(nonceA.size, 255))
|
||||
out.write(nonceA, 0, minOf(nonceA.size, 255))
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun buildVerifyResponse(noiseKeyHex: String, nonceA: ByteArray): ByteArray? {
|
||||
val service = encryptionServiceRef?.get() ?: return null
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val msg = ByteArrayOutputStream()
|
||||
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
|
||||
msg.write(minOf(noiseData.size, 255))
|
||||
msg.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
msg.write(nonceA)
|
||||
val sig = service.signData(msg.toByteArray()) ?: return null
|
||||
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x01)
|
||||
out.write(minOf(noiseData.size, 255))
|
||||
out.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
out.write(0x02)
|
||||
out.write(minOf(nonceA.size, 255))
|
||||
out.write(nonceA, 0, minOf(nonceA.size, 255))
|
||||
out.write(0x03)
|
||||
out.write(minOf(sig.size, 255))
|
||||
out.write(sig, 0, minOf(sig.size, 255))
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
fun parseVerifyChallenge(data: ByteArray): Pair<String, ByteArray>? {
|
||||
var idx = 0
|
||||
|
||||
fun take(n: Int): ByteArray? {
|
||||
if (idx + n > data.size) return null
|
||||
val out = data.copyOfRange(idx, idx + n)
|
||||
idx += n
|
||||
return out
|
||||
}
|
||||
|
||||
val t1 = take(1) ?: return null
|
||||
if (t1[0].toInt() != 0x01) return null
|
||||
val l1 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val noiseBytes = take(l1) ?: return null
|
||||
val noise = noiseBytes.toString(Charsets.UTF_8)
|
||||
|
||||
val t2 = take(1) ?: return null
|
||||
if (t2[0].toInt() != 0x02) return null
|
||||
val l2 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val nonce = take(l2) ?: return null
|
||||
|
||||
return noise to nonce
|
||||
}
|
||||
|
||||
data class VerifyResponse(val noiseKeyHex: String, val nonceA: ByteArray, val signature: ByteArray) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as VerifyResponse
|
||||
|
||||
if (noiseKeyHex != other.noiseKeyHex) return false
|
||||
if (!nonceA.contentEquals(other.nonceA)) return false
|
||||
if (!signature.contentEquals(other.signature)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = noiseKeyHex.hashCode()
|
||||
result = 31 * result + nonceA.contentHashCode()
|
||||
result = 31 * result + signature.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun parseVerifyResponse(data: ByteArray): VerifyResponse? {
|
||||
var idx = 0
|
||||
|
||||
fun take(n: Int): ByteArray? {
|
||||
if (idx + n > data.size) return null
|
||||
val out = data.copyOfRange(idx, idx + n)
|
||||
idx += n
|
||||
return out
|
||||
}
|
||||
|
||||
val t1 = take(1) ?: return null
|
||||
if (t1[0].toInt() != 0x01) return null
|
||||
val l1 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val noiseBytes = take(l1) ?: return null
|
||||
val noise = noiseBytes.toString(Charsets.UTF_8)
|
||||
|
||||
val t2 = take(1) ?: return null
|
||||
if (t2[0].toInt() != 0x02) return null
|
||||
val l2 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val nonce = take(l2) ?: return null
|
||||
|
||||
val t3 = take(1) ?: return null
|
||||
if (t3[0].toInt() != 0x03) return null
|
||||
val l3 = take(1)?.get(0)?.toInt() ?: return null
|
||||
val sig = take(l3) ?: return null
|
||||
|
||||
return VerifyResponse(noise, nonce, sig)
|
||||
}
|
||||
|
||||
fun verifyResponseSignature(
|
||||
noiseKeyHex: String,
|
||||
nonceA: ByteArray,
|
||||
signature: ByteArray,
|
||||
signerPublicKeyHex: String
|
||||
): Boolean {
|
||||
val service = encryptionServiceRef?.get() ?: return false
|
||||
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
|
||||
val msg = ByteArrayOutputStream()
|
||||
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
|
||||
msg.write(minOf(noiseData.size, 255))
|
||||
msg.write(noiseData, 0, minOf(noiseData.size, 255))
|
||||
msg.write(nonceA)
|
||||
val signerKey = signerPublicKeyHex.dataFromHexString() ?: return false
|
||||
return service.verifyEd25519Signature(signature, msg.toByteArray(), signerKey)
|
||||
}
|
||||
|
||||
private data class CacheEntry(
|
||||
val nickname: String,
|
||||
val npub: String?,
|
||||
val builtAtMs: Long,
|
||||
val value: String
|
||||
)
|
||||
|
||||
private object Cache {
|
||||
var last: CacheEntry? = null
|
||||
}
|
||||
}
|
||||
@ -318,6 +318,10 @@ private fun PrivateChatHeader(
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isNostrDM = peerID.startsWith("nostr_") || peerID.startsWith("nostr:")
|
||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||
val isVerified = remember(peerID, verifiedFingerprints) {
|
||||
viewModel.isPeerVerified(peerID, verifiedFingerprints)
|
||||
}
|
||||
// Determine mutual favorite state for this peer (supports mesh ephemeral 16-hex via favorites lookup)
|
||||
val isMutualFavorite = remember(peerID, peerNicknames) {
|
||||
try {
|
||||
@ -413,17 +417,33 @@ private fun PrivateChatHeader(
|
||||
|
||||
// Show a globe when chatting via Nostr alias, or when mesh session not established but mutual favorite exists
|
||||
val showGlobe = isNostrDM || (sessionState != "established" && isMutualFavorite)
|
||||
val securityModifier = if (!isNostrDM) {
|
||||
Modifier.clickable { viewModel.showSecurityVerificationSheet() }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
if (showGlobe) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
contentDescription = stringResource(R.string.cd_nostr_reachable),
|
||||
modifier = Modifier.size(14.dp),
|
||||
modifier = Modifier.size(14.dp).then(securityModifier),
|
||||
tint = Color(0xFF9B59B6) // Purple like iOS
|
||||
)
|
||||
} else {
|
||||
NoiseSessionIcon(
|
||||
sessionState = sessionState,
|
||||
modifier = Modifier.size(14.dp)
|
||||
modifier = Modifier.size(14.dp).then(securityModifier)
|
||||
)
|
||||
}
|
||||
|
||||
if (isVerified) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
modifier = Modifier.size(14.dp).then(securityModifier),
|
||||
tint = Color(0xFF32D74B)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
val showMentionSuggestions by viewModel.showMentionSuggestions.collectAsStateWithLifecycle()
|
||||
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
|
||||
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
|
||||
val showVerificationSheet by viewModel.showVerificationSheet.collectAsStateWithLifecycle()
|
||||
val showSecurityVerificationSheet by viewModel.showSecurityVerificationSheet.collectAsStateWithLifecycle()
|
||||
|
||||
var messageText by remember { mutableStateOf(TextFieldValue("")) }
|
||||
var showPasswordPrompt by remember { mutableStateOf(false) }
|
||||
@ -327,6 +329,10 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
SidebarOverlay(
|
||||
viewModel = viewModel,
|
||||
onDismiss = { viewModel.hideSidebar() },
|
||||
onShowVerification = {
|
||||
viewModel.showVerificationSheet(fromSidebar = true)
|
||||
viewModel.hideSidebar()
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
@ -373,7 +379,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
},
|
||||
selectedUserForSheet = selectedUserForSheet,
|
||||
selectedMessageForSheet = selectedMessageForSheet,
|
||||
viewModel = viewModel
|
||||
viewModel = viewModel,
|
||||
showVerificationSheet = showVerificationSheet,
|
||||
onVerificationSheetDismiss = viewModel::hideVerificationSheet,
|
||||
showSecurityVerificationSheet = showSecurityVerificationSheet,
|
||||
onSecurityVerificationSheetDismiss = viewModel::hideSecurityVerificationSheet
|
||||
)
|
||||
}
|
||||
|
||||
@ -513,7 +523,11 @@ private fun ChatDialogs(
|
||||
onUserSheetDismiss: () -> Unit,
|
||||
selectedUserForSheet: String,
|
||||
selectedMessageForSheet: BitchatMessage?,
|
||||
viewModel: ChatViewModel
|
||||
viewModel: ChatViewModel,
|
||||
showVerificationSheet: Boolean,
|
||||
onVerificationSheetDismiss: () -> Unit,
|
||||
showSecurityVerificationSheet: Boolean,
|
||||
onSecurityVerificationSheetDismiss: () -> Unit
|
||||
) {
|
||||
// Password dialog
|
||||
PasswordPromptDialog(
|
||||
@ -567,4 +581,20 @@ private fun ChatDialogs(
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
if (showVerificationSheet) {
|
||||
VerificationSheet(
|
||||
isPresented = showVerificationSheet,
|
||||
onDismiss = onVerificationSheetDismiss,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
if (showSecurityVerificationSheet) {
|
||||
SecurityVerificationSheet(
|
||||
isPresented = showSecurityVerificationSheet,
|
||||
onDismiss = onSecurityVerificationSheetDismiss,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,6 +122,12 @@ class ChatState(
|
||||
// Navigation state
|
||||
private val _showAppInfo = MutableStateFlow<Boolean>(false)
|
||||
val showAppInfo: StateFlow<Boolean> = _showAppInfo.asStateFlow()
|
||||
|
||||
private val _showVerificationSheet = MutableStateFlow(false)
|
||||
val showVerificationSheet: StateFlow<Boolean> = _showVerificationSheet.asStateFlow()
|
||||
|
||||
private val _showSecurityVerificationSheet = MutableStateFlow(false)
|
||||
val showSecurityVerificationSheet: StateFlow<Boolean> = _showSecurityVerificationSheet.asStateFlow()
|
||||
|
||||
// Location channels state (for Nostr geohash features)
|
||||
private val _selectedLocationChannel = MutableStateFlow<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
|
||||
@ -302,6 +308,14 @@ class ChatState(
|
||||
fun setShowAppInfo(show: Boolean) {
|
||||
_showAppInfo.value = show
|
||||
}
|
||||
|
||||
fun setShowVerificationSheet(show: Boolean) {
|
||||
_showVerificationSheet.value = show
|
||||
}
|
||||
|
||||
fun setShowSecurityVerificationSheet(show: Boolean) {
|
||||
_showSecurityVerificationSheet.value = show
|
||||
}
|
||||
|
||||
fun setSelectedLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
|
||||
_selectedLocationChannel.value = channel
|
||||
|
||||
@ -5,11 +5,15 @@ import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitchat.android.favorites.FavoritesPersistenceService
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.BitchatMessageType
|
||||
import com.bitchat.android.nostr.NostrIdentityBridge
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
|
||||
|
||||
@ -19,6 +23,13 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import kotlin.random.Random
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.noise.NoiseSession
|
||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
||||
@ -46,6 +57,20 @@ class ChatViewModel(
|
||||
mediaSendingManager.sendImageNote(toPeerIDOrNull, channelOrNull, filePath)
|
||||
}
|
||||
|
||||
fun getCurrentNpub(): String? {
|
||||
return try {
|
||||
NostrIdentityBridge
|
||||
.getCurrentNostrIdentity(getApplication())
|
||||
?.npub
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun buildMyQRString(nickname: String, npub: String?): String {
|
||||
return VerificationService.buildMyQRString(nickname, npub) ?: ""
|
||||
}
|
||||
|
||||
// MARK: - State management
|
||||
private val state = ChatState(
|
||||
scope = viewModelScope,
|
||||
@ -57,6 +82,7 @@ class ChatViewModel(
|
||||
|
||||
// Specialized managers
|
||||
private val dataManager = DataManager(application.applicationContext)
|
||||
private val identityManager by lazy { SecureIdentityStateManager(getApplication()) }
|
||||
private val messageManager = MessageManager(state)
|
||||
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
|
||||
|
||||
@ -75,6 +101,17 @@ class ChatViewModel(
|
||||
NotificationIntervalManager()
|
||||
)
|
||||
|
||||
private val verificationHandler = VerificationHandler(
|
||||
context = application.applicationContext,
|
||||
scope = viewModelScope,
|
||||
meshService = meshService,
|
||||
identityManager = identityManager,
|
||||
state = state,
|
||||
notificationManager = notificationManager,
|
||||
messageManager = messageManager
|
||||
)
|
||||
val verifiedFingerprints = verificationHandler.verifiedFingerprints
|
||||
|
||||
// Media file sending manager
|
||||
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService)
|
||||
|
||||
@ -134,6 +171,8 @@ class ChatViewModel(
|
||||
val peerRSSI: StateFlow<Map<String, Int>> = state.peerRSSI
|
||||
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
|
||||
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
|
||||
val showVerificationSheet: StateFlow<Boolean> = state.showVerificationSheet
|
||||
val showSecurityVerificationSheet: StateFlow<Boolean> = state.showSecurityVerificationSheet
|
||||
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
val isTeleported: StateFlow<Boolean> = state.isTeleported
|
||||
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||
@ -247,6 +286,9 @@ class ChatViewModel(
|
||||
// Initialize favorites persistence service
|
||||
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication())
|
||||
|
||||
// Load verified fingerprints from secure storage
|
||||
verificationHandler.loadVerifiedFingerprints()
|
||||
|
||||
|
||||
// Ensure NostrTransport knows our mesh peer ID for embedded packets
|
||||
try {
|
||||
@ -646,6 +688,18 @@ class ChatViewModel(
|
||||
// Update fingerprint mappings from centralized manager
|
||||
val fingerprints = privateChatManager.getAllPeerFingerprints()
|
||||
state.setPeerFingerprints(fingerprints)
|
||||
fingerprints.forEach { (peerID, fingerprint) ->
|
||||
identityManager.cachePeerFingerprint(peerID, fingerprint)
|
||||
val info = try { meshService.getPeerInfo(peerID) } catch (_: Exception) { null }
|
||||
val noiseKeyHex = info?.noisePublicKey?.hexEncodedString()
|
||||
if (noiseKeyHex != null) {
|
||||
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
||||
identityManager.cacheNoiseFingerprint(noiseKeyHex, fingerprint)
|
||||
}
|
||||
info?.nickname?.takeIf { it.isNotBlank() }?.let { nickname ->
|
||||
identityManager.cacheFingerprintNickname(fingerprint, nickname)
|
||||
}
|
||||
}
|
||||
|
||||
val nicknames = meshService.getPeerNicknames()
|
||||
state.setPeerNicknames(nicknames)
|
||||
@ -660,6 +714,34 @@ class ChatViewModel(
|
||||
}
|
||||
state.setPeerDirect(directMap)
|
||||
} catch (_: Exception) { }
|
||||
|
||||
// Flush any pending QR verification once a Noise session is established
|
||||
currentPeers.forEach { peerID ->
|
||||
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
||||
verificationHandler.sendPendingVerificationIfNeeded(peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - QR Verification
|
||||
|
||||
fun isPeerVerified(peerID: String, verifiedFingerprints: Set<String>): Boolean {
|
||||
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
|
||||
val fingerprint = verificationHandler.getPeerFingerprintForDisplay(peerID)
|
||||
return fingerprint != null && verifiedFingerprints.contains(fingerprint)
|
||||
}
|
||||
|
||||
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray, verifiedFingerprints: Set<String>): Boolean {
|
||||
val fingerprint = verificationHandler.fingerprintFromNoiseBytes(noisePublicKey)
|
||||
return verifiedFingerprints.contains(fingerprint)
|
||||
}
|
||||
|
||||
fun unverifyFingerprint(peerID: String) {
|
||||
verificationHandler.unverifyFingerprint(peerID)
|
||||
}
|
||||
|
||||
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
|
||||
return verificationHandler.beginQRVerification(qr)
|
||||
}
|
||||
|
||||
// MARK: - Debug and Troubleshooting
|
||||
@ -668,41 +750,71 @@ class ChatViewModel(
|
||||
return meshService.getDebugStatus()
|
||||
}
|
||||
|
||||
// Note: Mesh service restart is now handled by MainActivity
|
||||
// This function is no longer needed
|
||||
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
// Forward to notification manager for notification logic
|
||||
notificationManager.setAppBackgroundState(inBackground)
|
||||
}
|
||||
|
||||
fun setCurrentPrivateChatPeer(peerID: String?) {
|
||||
// Update notification manager with current private chat peer
|
||||
notificationManager.setCurrentPrivateChatPeer(peerID)
|
||||
}
|
||||
|
||||
fun setCurrentGeohash(geohash: String?) {
|
||||
// Update notification manager with current geohash for notification logic
|
||||
notificationManager.setCurrentGeohash(geohash)
|
||||
}
|
||||
|
||||
fun clearNotificationsForSender(peerID: String) {
|
||||
// Clear notifications when user opens a chat
|
||||
notificationManager.clearNotificationsForSender(peerID)
|
||||
}
|
||||
|
||||
fun clearNotificationsForGeohash(geohash: String) {
|
||||
// Clear notifications when user opens a geohash chat
|
||||
notificationManager.clearNotificationsForGeohash(geohash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear mesh mention notifications when user opens mesh chat
|
||||
*/
|
||||
fun clearMeshMentionNotifications() {
|
||||
notificationManager.clearMeshMentionNotifications()
|
||||
}
|
||||
|
||||
private var reopenSidebarAfterVerification = false
|
||||
|
||||
fun showVerificationSheet(fromSidebar: Boolean = false) {
|
||||
if (fromSidebar) {
|
||||
reopenSidebarAfterVerification = true
|
||||
}
|
||||
state.setShowVerificationSheet(true)
|
||||
}
|
||||
|
||||
fun hideVerificationSheet() {
|
||||
state.setShowVerificationSheet(false)
|
||||
if (reopenSidebarAfterVerification) {
|
||||
reopenSidebarAfterVerification = false
|
||||
state.setShowSidebar(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSecurityVerificationSheet() {
|
||||
state.setShowSecurityVerificationSheet(true)
|
||||
}
|
||||
|
||||
fun hideSecurityVerificationSheet() {
|
||||
state.setShowSecurityVerificationSheet(false)
|
||||
}
|
||||
|
||||
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
||||
return verificationHandler.getPeerFingerprintForDisplay(peerID)
|
||||
}
|
||||
|
||||
fun getMyFingerprint(): String {
|
||||
return verificationHandler.getMyFingerprint()
|
||||
}
|
||||
|
||||
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
|
||||
return verificationHandler.resolvePeerDisplayNameForFingerprint(peerID)
|
||||
}
|
||||
|
||||
fun verifyFingerprintValue(fingerprint: String) {
|
||||
verificationHandler.verifyFingerprintValue(fingerprint)
|
||||
}
|
||||
|
||||
fun unverifyFingerprintValue(fingerprint: String) {
|
||||
verificationHandler.unverifyFingerprintValue(fingerprint)
|
||||
}
|
||||
|
||||
// MARK: - Command Autocomplete (delegated)
|
||||
|
||||
fun updateCommandSuggestions(input: String) {
|
||||
@ -744,6 +856,14 @@ class ChatViewModel(
|
||||
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
|
||||
meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID)
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
verificationHandler.didReceiveVerifyChallenge(peerID, payload)
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
verificationHandler.didReceiveVerifyResponse(peerID, payload)
|
||||
}
|
||||
|
||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||
return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel)
|
||||
@ -827,7 +947,7 @@ class ChatViewModel(
|
||||
|
||||
// Clear secure identity state (if used)
|
||||
try {
|
||||
val identityManager = com.bitchat.android.identity.SecureIdentityStateManager(getApplication())
|
||||
val identityManager = SecureIdentityStateManager(getApplication())
|
||||
identityManager.clearIdentityData()
|
||||
// Also clear secure values used by FavoritesPersistenceService (favorites + peerID index)
|
||||
try {
|
||||
@ -840,7 +960,7 @@ class ChatViewModel(
|
||||
|
||||
// Clear FavoritesPersistenceService persistent relationships
|
||||
try {
|
||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.clearAllFavorites()
|
||||
FavoritesPersistenceService.shared.clearAllFavorites()
|
||||
Log.d(TAG, "✅ Cleared FavoritesPersistenceService relationships")
|
||||
} catch (_: Exception) { }
|
||||
|
||||
@ -969,5 +1089,6 @@ class ChatViewModel(
|
||||
*/
|
||||
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
|
||||
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -216,6 +216,14 @@ class MeshDelegateHandler(
|
||||
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
// Handled by ChatViewModel for verification flow
|
||||
}
|
||||
|
||||
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||
// Handled by ChatViewModel for verification flow
|
||||
}
|
||||
|
||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||
return channelManager.decryptChannelMessage(encryptedContent, channel)
|
||||
|
||||
@ -280,6 +280,37 @@ class NotificationManager(
|
||||
Log.d(TAG, "Displayed notification for $contentTitle with ID $notificationId")
|
||||
}
|
||||
|
||||
fun showVerificationNotification(title: String, body: String, peerID: String? = null) {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (peerID != null) {
|
||||
putExtra(EXTRA_OPEN_PRIVATE_CHAT, true)
|
||||
putExtra(EXTRA_PEER_ID, peerID)
|
||||
putExtra(EXTRA_SENDER_NICKNAME, body)
|
||||
}
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
(System.currentTimeMillis() and 0x7FFFFFFF).toInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setShowWhen(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
|
||||
notificationManager.notify((System.currentTimeMillis() and 0x7FFFFFFF).toInt(), builder.build())
|
||||
}
|
||||
|
||||
private fun showNotificationForActivePeers(peersSize: Int) {
|
||||
// Create intent to open the app
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
|
||||
@ -0,0 +1,443 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.NoEncryption
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
|
||||
private data class SecurityStatusInfo(
|
||||
val text: String,
|
||||
val icon: ImageVector,
|
||||
val tint: Color
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SecurityVerificationSheet(
|
||||
isPresented: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: ChatViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (!isPresented) return
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val peerID by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
||||
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
||||
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
|
||||
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
|
||||
|
||||
ModalBottomSheet(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
dragHandle = null
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SecurityVerificationHeader(
|
||||
accent = accent,
|
||||
onClose = onDismiss
|
||||
)
|
||||
|
||||
if (peerID == null) {
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_no_peer),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
} else {
|
||||
val selectedPeerID = peerID!!
|
||||
val displayName = viewModel.resolvePeerDisplayNameForFingerprint(selectedPeerID)
|
||||
val fingerprint = viewModel.getPeerFingerprintForDisplay(selectedPeerID)
|
||||
val isVerified = fingerprint != null && verifiedFingerprints.contains(fingerprint)
|
||||
val sessionState = peerSessionStates[selectedPeerID]
|
||||
val statusInfo = buildStatusInfo(
|
||||
isVerified = isVerified,
|
||||
sessionState = sessionState,
|
||||
accent = accent
|
||||
)
|
||||
|
||||
SecurityStatusCard(
|
||||
displayName = displayName,
|
||||
accent = accent,
|
||||
boxColor = boxColor,
|
||||
statusInfo = statusInfo
|
||||
)
|
||||
|
||||
FingerprintBlock(
|
||||
title = stringResource(R.string.fingerprint_their),
|
||||
fingerprint = fingerprint,
|
||||
boxColor = boxColor,
|
||||
accent = accent
|
||||
)
|
||||
|
||||
FingerprintBlock(
|
||||
title = stringResource(R.string.fingerprint_yours),
|
||||
fingerprint = viewModel.getMyFingerprint(),
|
||||
boxColor = boxColor,
|
||||
accent = accent
|
||||
)
|
||||
|
||||
SecurityVerificationActions(
|
||||
isVerified = isVerified,
|
||||
fingerprint = fingerprint,
|
||||
displayName = displayName,
|
||||
accent = accent,
|
||||
canStartHandshake = fingerprint == null && selectedPeerID.matches(peerHexRegex),
|
||||
onStartHandshake = { viewModel.meshService.initiateNoiseHandshake(selectedPeerID) },
|
||||
onVerify = { fp -> viewModel.verifyFingerprintValue(fp) },
|
||||
onUnverify = { fp -> viewModel.unverifyFingerprintValue(fp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityVerificationHeader(
|
||||
accent: Color,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.security_verification_title),
|
||||
style = MaterialTheme.typography.titleSmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = accent
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
CloseButton(onClick = onClose)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildStatusInfo(
|
||||
isVerified: Boolean,
|
||||
sessionState: String?,
|
||||
accent: Color
|
||||
): SecurityStatusInfo {
|
||||
val text = when {
|
||||
isVerified -> stringResource(R.string.fingerprint_status_verified)
|
||||
sessionState == "established" -> stringResource(R.string.fingerprint_status_encrypted)
|
||||
sessionState == "handshaking" -> stringResource(R.string.fingerprint_status_handshaking)
|
||||
sessionState == "failed" -> stringResource(R.string.fingerprint_status_failed)
|
||||
else -> stringResource(R.string.fingerprint_status_uninitialized)
|
||||
}
|
||||
val icon = when {
|
||||
isVerified -> Icons.Filled.Verified
|
||||
sessionState == "handshaking" -> Icons.Outlined.Sync
|
||||
sessionState == "failed" -> Icons.Outlined.OutlinedWarning
|
||||
sessionState == "established" -> Icons.Filled.Lock
|
||||
else -> Icons.Outlined.NoEncryption
|
||||
}
|
||||
val tint = when {
|
||||
isVerified -> Color(0xFF32D74B)
|
||||
sessionState == "failed" -> Color(0xFFFF3B30)
|
||||
sessionState == "handshaking" -> Color(0xFFFF9500)
|
||||
sessionState == "established" -> Color(0xFF32D74B)
|
||||
else -> accent.copy(alpha = 0.6f)
|
||||
}
|
||||
return SecurityStatusInfo(text, icon, tint)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityStatusCard(
|
||||
displayName: String,
|
||||
accent: Color,
|
||||
boxColor: Color,
|
||||
statusInfo: SecurityStatusInfo
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(boxColor, shape = MaterialTheme.shapes.medium)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = statusInfo.icon,
|
||||
contentDescription = null,
|
||||
tint = statusInfo.tint
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = displayName,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = accent
|
||||
)
|
||||
Text(
|
||||
text = statusInfo.text,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
color = accent.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityVerificationActions(
|
||||
isVerified: Boolean,
|
||||
fingerprint: String?,
|
||||
displayName: String,
|
||||
accent: Color,
|
||||
canStartHandshake: Boolean,
|
||||
onStartHandshake: () -> Unit,
|
||||
onVerify: (String) -> Unit,
|
||||
onUnverify: (String) -> Unit
|
||||
) {
|
||||
if (canStartHandshake) {
|
||||
Button(
|
||||
onClick = onStartHandshake,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accent,
|
||||
contentColor = Color.White
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_start_handshake),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerified) {
|
||||
VerificationStatusRow(
|
||||
icon = Icons.Filled.Verified,
|
||||
iconTint = Color(0xFF32D74B),
|
||||
text = stringResource(R.string.fingerprint_verified_label),
|
||||
textTint = Color(0xFF32D74B)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_verified_message),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
color = accent.copy(alpha = 0.7f),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Button(
|
||||
onClick = { fingerprint?.let(onUnverify) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFFFF3B30),
|
||||
contentColor = Color.White
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_remove),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
} else {
|
||||
VerificationStatusRow(
|
||||
icon = Icons.Filled.Warning,
|
||||
iconTint = Color(0xFFFF9500),
|
||||
text = stringResource(R.string.fingerprint_not_verified_label),
|
||||
textTint = Color(0xFFFF9500)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_not_verified_message_fmt, displayName),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontFamily = FontFamily.Monospace
|
||||
),
|
||||
color = accent.copy(alpha = 0.7f),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
if (fingerprint != null) {
|
||||
Button(
|
||||
onClick = { onVerify(fingerprint) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF34C759),
|
||||
contentColor = Color.White
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_mark_verified),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerificationStatusRow(
|
||||
icon: ImageVector,
|
||||
iconTint: Color,
|
||||
text: String,
|
||||
textTint: Color
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = textTint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FingerprintBlock(
|
||||
title: String,
|
||||
fingerprint: String?,
|
||||
boxColor: Color,
|
||||
accent: Color
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
var showMenu by remember(fingerprint) { mutableStateOf(false) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
color = accent.copy(alpha = 0.8f)
|
||||
)
|
||||
if (fingerprint != null) {
|
||||
Column {
|
||||
Text(
|
||||
text = formatFingerprint(fingerprint),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
color = accent,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = {},
|
||||
onLongClick = { showMenu = true }
|
||||
)
|
||||
.background(boxColor, shape = MaterialTheme.shapes.small)
|
||||
.padding(16.dp),
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.fingerprint_copy)) },
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(fingerprint))
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.fingerprint_pending),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
color = Color(0xFFFF9500),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFingerprint(fingerprint: String): String {
|
||||
val upper = fingerprint.uppercase()
|
||||
val sb = StringBuilder()
|
||||
upper.forEachIndexed { index, c ->
|
||||
if (index > 0 && index % 4 == 0) {
|
||||
if (index % 16 == 0) sb.append('\n') else sb.append(' ')
|
||||
}
|
||||
sb.append(c)
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
@ -23,6 +23,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
|
||||
|
||||
/**
|
||||
@ -34,6 +35,7 @@ import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
fun SidebarOverlay(
|
||||
viewModel: ChatViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onShowVerification: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
@ -131,6 +133,7 @@ fun SidebarOverlay(
|
||||
colorScheme = colorScheme,
|
||||
selectedPrivatePeer = selectedPrivatePeer,
|
||||
viewModel = viewModel,
|
||||
onShowVerification = onShowVerification,
|
||||
onPrivateChatStart = { peerID ->
|
||||
viewModel.startPrivateChat(peerID)
|
||||
onDismiss()
|
||||
@ -257,8 +260,11 @@ fun PeopleSection(
|
||||
colorScheme: ColorScheme,
|
||||
selectedPrivatePeer: String?,
|
||||
viewModel: ChatViewModel,
|
||||
onShowVerification: () -> Unit,
|
||||
onPrivateChatStart: (String) -> Unit
|
||||
) {
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -279,6 +285,17 @@ fun PeopleSection(
|
||||
color = colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location) {
|
||||
IconButton(onClick = onShowVerification, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.QrCode,
|
||||
contentDescription = stringResource(R.string.verify_title),
|
||||
tint = colorScheme.onSurface.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connectedPeers.isEmpty()) {
|
||||
@ -295,6 +312,7 @@ fun PeopleSection(
|
||||
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||
|
||||
// Reactive favorite computation for all peers
|
||||
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
||||
@ -308,10 +326,16 @@ fun PeopleSection(
|
||||
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
||||
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
||||
try {
|
||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()
|
||||
} catch (_: Exception) { null }
|
||||
}.filterValues { it != null }.mapValues { it.value!! }
|
||||
|
||||
val peerVerifiedStates = remember(verifiedFingerprints, peerFingerprints, connectedPeers) {
|
||||
connectedPeers.associateWith { peerID ->
|
||||
viewModel.isPeerVerified(peerID, verifiedFingerprints)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
|
||||
|
||||
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
||||
@ -342,7 +366,7 @@ fun PeopleSection(
|
||||
// Offline favorites (exclude ones mapped to connected)
|
||||
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
||||
offlineFavorites.forEach { fav ->
|
||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
|
||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||
if (!isMappedToConnected) {
|
||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||
@ -368,6 +392,7 @@ fun PeopleSection(
|
||||
|
||||
sortedPeers.forEach { peerID ->
|
||||
val isFavorite = peerFavoriteStates[peerID] ?: false
|
||||
val isVerified = peerVerifiedStates[peerID] ?: false
|
||||
// fingerprint and favorite relationship resolution not needed here; UI will show Nostr globe for appended offline favorites below
|
||||
|
||||
val noiseHex = noiseHexByPeerID[peerID]
|
||||
@ -392,6 +417,7 @@ fun PeopleSection(
|
||||
isDirect = isDirectLive,
|
||||
isSelected = peerID == selectedPrivatePeer,
|
||||
isFavorite = isFavorite,
|
||||
isVerified = isVerified,
|
||||
hasUnreadDM = combinedHasUnread,
|
||||
colorScheme = colorScheme,
|
||||
viewModel = viewModel,
|
||||
@ -408,7 +434,7 @@ fun PeopleSection(
|
||||
|
||||
// Append offline favorites we actively favorite (and not currently connected)
|
||||
offlineFavorites.forEach { fav ->
|
||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
|
||||
// If any connected peer maps to this noise key, skip showing the offline entry
|
||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||
if (isMappedToConnected) return@forEach
|
||||
@ -419,7 +445,7 @@ fun PeopleSection(
|
||||
if (npubOrHex != null) {
|
||||
val hex = if (npubOrHex.startsWith("npub")) {
|
||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npubOrHex)
|
||||
if (hrp == "npub") data.joinToString("") { "%02x".format(it) } else null
|
||||
if (hrp == "npub") data.hexEncodedString() else null
|
||||
} else {
|
||||
npubOrHex.lowercase()
|
||||
}
|
||||
@ -435,6 +461,7 @@ fun PeopleSection(
|
||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||
val (bName, _) = com.bitchat.android.ui.splitSuffix(dn)
|
||||
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
||||
val isVerified = viewModel.isNoisePublicKeyVerified(fav.peerNoisePublicKey, verifiedFingerprints)
|
||||
|
||||
// Compute unreadCount from either noise conversation or Nostr conversation
|
||||
val unreadCount = (
|
||||
@ -449,6 +476,7 @@ fun PeopleSection(
|
||||
isDirect = false,
|
||||
isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
|
||||
isFavorite = true,
|
||||
isVerified = isVerified,
|
||||
hasUnreadDM = hasUnread,
|
||||
colorScheme = colorScheme,
|
||||
viewModel = viewModel,
|
||||
@ -516,6 +544,7 @@ private fun PeerItem(
|
||||
isDirect: Boolean,
|
||||
isSelected: Boolean,
|
||||
isFavorite: Boolean,
|
||||
isVerified: Boolean,
|
||||
hasUnreadDM: Boolean,
|
||||
colorScheme: ColorScheme,
|
||||
viewModel: ChatViewModel,
|
||||
@ -616,6 +645,16 @@ private fun PeerItem(
|
||||
color = baseColor.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
if (isVerified) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = Color(0xFF32D74B)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Favorite star with proper filled/outlined states
|
||||
|
||||
368
app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt
Normal file
368
app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt
Normal file
@ -0,0 +1,368 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import android.content.Context
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.favorites.FavoritesPersistenceService
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.noise.NoiseSession
|
||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.security.MessageDigest
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Handles QR verification logic and state, extracted from ChatViewModel.
|
||||
*/
|
||||
class VerificationHandler(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val meshService: BluetoothMeshService,
|
||||
private val identityManager: SecureIdentityStateManager,
|
||||
private val state: ChatState,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val messageManager: MessageManager
|
||||
) {
|
||||
|
||||
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
|
||||
val verifiedFingerprints: StateFlow<Set<String>> = _verifiedFingerprints.asStateFlow()
|
||||
|
||||
private val pendingQRVerifications = ConcurrentHashMap<String, PendingVerification>()
|
||||
private val lastVerifyNonceByPeer = ConcurrentHashMap<String, ByteArray>()
|
||||
private val lastInboundVerifyChallengeAt = ConcurrentHashMap<String, Long>()
|
||||
private val lastMutualToastAt = ConcurrentHashMap<String, Long>()
|
||||
|
||||
fun loadVerifiedFingerprints() {
|
||||
_verifiedFingerprints.value = identityManager.getVerifiedFingerprints()
|
||||
}
|
||||
|
||||
fun isPeerVerified(peerID: String): Boolean {
|
||||
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
|
||||
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
||||
return fingerprint != null && _verifiedFingerprints.value.contains(fingerprint)
|
||||
}
|
||||
|
||||
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray): Boolean {
|
||||
val fingerprint = fingerprintFromNoiseBytes(noisePublicKey)
|
||||
return _verifiedFingerprints.value.contains(fingerprint)
|
||||
}
|
||||
|
||||
fun unverifyFingerprint(peerID: String) {
|
||||
val fingerprint = meshService.getPeerFingerprint(peerID) ?: return
|
||||
identityManager.setVerifiedFingerprint(fingerprint, false)
|
||||
val current = _verifiedFingerprints.value.toMutableSet()
|
||||
current.remove(fingerprint)
|
||||
_verifiedFingerprints.value = current
|
||||
}
|
||||
|
||||
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
|
||||
val targetNoise = qr.noiseKeyHex.lowercase()
|
||||
val peerID = state.getConnectedPeersValue().firstOrNull { pid ->
|
||||
val noiseKeyHex = meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()?.lowercase()
|
||||
noiseKeyHex == targetNoise
|
||||
} ?: return false
|
||||
|
||||
if (pendingQRVerifications.containsKey(peerID)) return true
|
||||
val nonce = ByteArray(16)
|
||||
java.security.SecureRandom().nextBytes(nonce)
|
||||
val pending = PendingVerification(qr.noiseKeyHex, qr.signKeyHex, nonce, System.currentTimeMillis(), false)
|
||||
pendingQRVerifications[peerID] = pending
|
||||
|
||||
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
||||
meshService.sendVerifyChallenge(peerID, qr.noiseKeyHex, nonce)
|
||||
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
||||
} else {
|
||||
meshService.initiateNoiseHandshake(peerID)
|
||||
}
|
||||
fingerprintFromNoiseHex(qr.noiseKeyHex)?.let { fp ->
|
||||
identityManager.cacheFingerprintNickname(fp, qr.nickname)
|
||||
identityManager.cacheNoiseFingerprint(qr.noiseKeyHex, fp)
|
||||
identityManager.cachePeerNoiseKey(peerID, qr.noiseKeyHex)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun sendPendingVerificationIfNeeded(peerID: String) {
|
||||
val pending = pendingQRVerifications[peerID] ?: return
|
||||
if (pending.sent) return
|
||||
meshService.sendVerifyChallenge(peerID, pending.noiseKeyHex, pending.nonceA)
|
||||
pendingQRVerifications[peerID] = pending.copy(sent = true)
|
||||
}
|
||||
|
||||
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray) {
|
||||
scope.launch {
|
||||
val parsed = VerificationService.parseVerifyChallenge(payload) ?: return@launch
|
||||
val myNoiseHex = meshService.getStaticNoisePublicKey()?.hexEncodedString()?.lowercase() ?: return@launch
|
||||
if (parsed.first.lowercase() != myNoiseHex) return@launch
|
||||
|
||||
val lastNonce = lastVerifyNonceByPeer[peerID]
|
||||
if (lastNonce != null && lastNonce.contentEquals(parsed.second)) return@launch
|
||||
lastVerifyNonceByPeer[peerID] = parsed.second
|
||||
|
||||
val fp = meshService.getPeerFingerprint(peerID)
|
||||
if (fp != null) {
|
||||
lastInboundVerifyChallengeAt[fp] = System.currentTimeMillis()
|
||||
if (_verifiedFingerprints.value.contains(fp)) {
|
||||
val lastToast = lastMutualToastAt[fp] ?: 0L
|
||||
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
||||
lastMutualToastAt[fp] = System.currentTimeMillis()
|
||||
val name = resolvePeerDisplayName(peerID)
|
||||
val body = context.getString(R.string.verify_mutual_match_body, name)
|
||||
addVerificationSystemMessage(peerID, context.getString(R.string.verify_mutual_system_message, name))
|
||||
sendVerificationNotification(context.getString(R.string.verify_mutual_match_title), body, peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshService.sendVerifyResponse(peerID, parsed.first, parsed.second)
|
||||
}
|
||||
}
|
||||
|
||||
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray) {
|
||||
scope.launch {
|
||||
val resp = VerificationService.parseVerifyResponse(payload) ?: return@launch
|
||||
val pending = pendingQRVerifications[peerID] ?: return@launch
|
||||
if (!resp.noiseKeyHex.equals(pending.noiseKeyHex, ignoreCase = true)) return@launch
|
||||
if (!resp.nonceA.contentEquals(pending.nonceA)) return@launch
|
||||
|
||||
val ok = VerificationService.verifyResponseSignature(
|
||||
noiseKeyHex = resp.noiseKeyHex,
|
||||
nonceA = resp.nonceA,
|
||||
signature = resp.signature,
|
||||
signerPublicKeyHex = pending.signKeyHex
|
||||
)
|
||||
if (!ok) return@launch
|
||||
|
||||
pendingQRVerifications.remove(peerID)
|
||||
val fp = meshService.getPeerFingerprint(peerID) ?: return@launch
|
||||
identityManager.setVerifiedFingerprint(fp, true)
|
||||
val current = _verifiedFingerprints.value.toMutableSet()
|
||||
current.add(fp)
|
||||
_verifiedFingerprints.value = current
|
||||
|
||||
val name = resolvePeerDisplayName(peerID)
|
||||
identityManager.cacheFingerprintNickname(fp, name)
|
||||
val noiseKeyHex = try {
|
||||
meshService.getPeerInfo(peerID)?.noisePublicKey?.hexEncodedString()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (noiseKeyHex != null) {
|
||||
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
||||
identityManager.cacheNoiseFingerprint(noiseKeyHex, fp)
|
||||
}
|
||||
addVerificationSystemMessage(peerID, context.getString(R.string.verify_success_system_message, name))
|
||||
sendVerificationNotification(context.getString(R.string.verify_success_title), context.getString(R.string.verify_success_body, name), peerID)
|
||||
|
||||
val lastChallenge = lastInboundVerifyChallengeAt[fp] ?: 0L
|
||||
if (System.currentTimeMillis() - lastChallenge < 600_000L) {
|
||||
val lastToast = lastMutualToastAt[fp] ?: 0L
|
||||
if (System.currentTimeMillis() - lastToast > 60_000L) {
|
||||
lastMutualToastAt[fp] = System.currentTimeMillis()
|
||||
val body = context.getString(R.string.verify_mutual_match_body, name)
|
||||
addVerificationSystemMessage(peerID, context.getString(R.string.verify_mutual_system_message, name))
|
||||
sendVerificationNotification(context.getString(R.string.verify_mutual_match_title), body, peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPeerFingerprintForDisplay(peerID: String): String? {
|
||||
val fromMap = state.getPeerFingerprintsValue()[peerID]
|
||||
if (fromMap != null) return fromMap
|
||||
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
||||
return try {
|
||||
when {
|
||||
peerID.length == 64 && peerID.matches(hexRegex) -> {
|
||||
identityManager.getCachedNoiseFingerprint(peerID)?.let { return it }
|
||||
fingerprintFromNoiseHex(peerID)?.also { identityManager.cacheNoiseFingerprint(peerID, it) }
|
||||
}
|
||||
peerID.length == 16 && peerID.matches(hexRegex) -> {
|
||||
val meshFp = meshService.getPeerFingerprint(peerID)
|
||||
if (meshFp != null) return meshFp
|
||||
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
||||
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
||||
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
||||
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
||||
}
|
||||
val favorite = try {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
||||
}
|
||||
peerID.startsWith("nostr_") -> {
|
||||
val pubHex = GeohashAliasRegistry.get(peerID)
|
||||
val noiseKey = pubHex?.let {
|
||||
FavoritesPersistenceService.shared.findNoiseKey(it)
|
||||
}
|
||||
noiseKey?.let {
|
||||
val noiseHex = it.hexEncodedString()
|
||||
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
||||
}
|
||||
}
|
||||
peerID.startsWith("nostr:") -> {
|
||||
val prefix = peerID.removePrefix("nostr:").lowercase()
|
||||
val pubHex = GeohashAliasRegistry
|
||||
.snapshot()
|
||||
.values
|
||||
.firstOrNull { it.lowercase().startsWith(prefix) }
|
||||
val noiseKey = pubHex?.let {
|
||||
FavoritesPersistenceService.shared.findNoiseKey(it)
|
||||
}
|
||||
noiseKey?.let {
|
||||
val noiseHex = it.hexEncodedString()
|
||||
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val meshFp = meshService.getPeerFingerprint(peerID)
|
||||
if (meshFp != null) return meshFp
|
||||
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
|
||||
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
|
||||
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
|
||||
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
|
||||
}
|
||||
val favorite = try {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
|
||||
val nicknameMap = state.peerNicknames.value
|
||||
nicknameMap[peerID]?.let { return it }
|
||||
try {
|
||||
meshService.getPeerInfo(peerID)?.nickname?.let { return it }
|
||||
} catch (_: Exception) { }
|
||||
|
||||
val fingerprint = getPeerFingerprintForDisplay(peerID)
|
||||
fingerprint?.let { fp ->
|
||||
identityManager.getCachedFingerprintNickname(fp)?.let { cached ->
|
||||
if (cached.isNotBlank()) return cached
|
||||
}
|
||||
}
|
||||
|
||||
val hexRegex = Regex("^[0-9a-fA-F]+$")
|
||||
if (peerID.length == 64 && peerID.matches(hexRegex)) {
|
||||
val noiseKeyBytes = try {
|
||||
peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
} catch (_: Exception) { null }
|
||||
val favorite = noiseKeyBytes?.let {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(it)
|
||||
}
|
||||
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
}
|
||||
|
||||
if (peerID.length == 16 && peerID.matches(hexRegex)) {
|
||||
val favorite = try {
|
||||
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
}
|
||||
|
||||
return peerID.take(8)
|
||||
}
|
||||
|
||||
fun getMyFingerprint(): String {
|
||||
return meshService.getIdentityFingerprint()
|
||||
}
|
||||
|
||||
fun verifyFingerprintValue(fingerprint: String) {
|
||||
if (fingerprint.isBlank()) return
|
||||
identityManager.setVerifiedFingerprint(fingerprint, true)
|
||||
val current = _verifiedFingerprints.value.toMutableSet()
|
||||
current.add(fingerprint)
|
||||
_verifiedFingerprints.value = current
|
||||
}
|
||||
|
||||
fun unverifyFingerprintValue(fingerprint: String) {
|
||||
if (fingerprint.isBlank()) return
|
||||
identityManager.setVerifiedFingerprint(fingerprint, false)
|
||||
val current = _verifiedFingerprints.value.toMutableSet()
|
||||
current.remove(fingerprint)
|
||||
_verifiedFingerprints.value = current
|
||||
}
|
||||
|
||||
private fun addVerificationSystemMessage(peerID: String, text: String) {
|
||||
val msg = BitchatMessage(
|
||||
sender = "system",
|
||||
content = text,
|
||||
timestamp = Date(),
|
||||
isRelay = false,
|
||||
isPrivate = true,
|
||||
senderPeerID = peerID
|
||||
)
|
||||
messageManager.addPrivateMessageNoUnread(peerID, msg)
|
||||
}
|
||||
|
||||
private fun resolvePeerDisplayName(peerID: String): String {
|
||||
val nick = try { meshService.getPeerInfo(peerID)?.nickname } catch (_: Exception) { null }
|
||||
return nick ?: peerID.take(8)
|
||||
}
|
||||
|
||||
private fun sendVerificationNotification(title: String, body: String, peerID: String) {
|
||||
notificationManager.showVerificationNotification(title, body, peerID)
|
||||
}
|
||||
|
||||
private fun fingerprintFromNoiseHex(noiseHex: String): String? {
|
||||
val bytes = noiseHex.dataFromHexString() ?: return null
|
||||
return fingerprintFromNoiseBytes(bytes)
|
||||
}
|
||||
|
||||
fun fingerprintFromNoiseBytes(bytes: ByteArray): String {
|
||||
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
|
||||
return hash.hexEncodedString()
|
||||
}
|
||||
|
||||
private data class PendingVerification(
|
||||
val noiseKeyHex: String,
|
||||
val signKeyHex: String,
|
||||
val nonceA: ByteArray,
|
||||
val startedAtMs: Long,
|
||||
val sent: Boolean
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PendingVerification
|
||||
|
||||
if (startedAtMs != other.startedAtMs) return false
|
||||
if (sent != other.sent) return false
|
||||
if (noiseKeyHex != other.noiseKeyHex) return false
|
||||
if (signKeyHex != other.signKeyHex) return false
|
||||
if (!nonceA.contentEquals(other.nonceA)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = startedAtMs.hashCode()
|
||||
result = 31 * result + sent.hashCode()
|
||||
result = 31 * result + noiseKeyHex.hashCode()
|
||||
result = 31 * result + signKeyHex.hashCode()
|
||||
result = 31 * result + nonceA.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
496
app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt
Normal file
496
app/src/main/java/com/bitchat/android/ui/VerificationSheet.kt
Normal file
@ -0,0 +1,496 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.camera.compose.CameraXViewfinder
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.core.SurfaceRequest
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.viewfinder.core.ImplementationMode
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.QrCode
|
||||
import androidx.compose.material.icons.outlined.QrCodeScanner
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.set
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VerificationSheet(
|
||||
isPresented: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: ChatViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (!isPresented) return
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = if (isDark) Color.Green else Color(0xFF008000)
|
||||
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
|
||||
|
||||
var showingScanner by remember { mutableStateOf(false) }
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
val npub = remember {
|
||||
viewModel.getCurrentNpub()
|
||||
}
|
||||
|
||||
|
||||
val qrString = remember(nickname, npub) {
|
||||
viewModel.buildMyQRString(nickname, npub)
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
dragHandle = null
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
VerificationHeader(
|
||||
accent = accent,
|
||||
onClose = onDismiss
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (showingScanner) {
|
||||
QRScannerPanel(
|
||||
accent = accent,
|
||||
boxColor = boxColor,
|
||||
onScan = { code ->
|
||||
val qr = VerificationService.verifyScannedQR(code)
|
||||
if (qr != null && viewModel.beginQRVerification(qr)) {
|
||||
showingScanner = false
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MyQrPanel(
|
||||
qrString = qrString,
|
||||
accent = accent,
|
||||
boxColor = boxColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
ToggleVerificationModeButton(
|
||||
showingScanner = showingScanner,
|
||||
onToggle = { showingScanner = !showingScanner }
|
||||
)
|
||||
|
||||
val peerID by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||
val fingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||
if (peerID != null) {
|
||||
val fingerprint = viewModel.meshService.getPeerFingerprint(peerID!!)
|
||||
if (fingerprint != null && fingerprints.contains(fingerprint)) {
|
||||
Button(
|
||||
onClick = { viewModel.unverifyFingerprint(peerID!!) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_remove),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerificationHeader(
|
||||
accent: Color,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_title).uppercase(),
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = accent
|
||||
)
|
||||
CloseButton(onClick = onClose)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MyQrPanel(
|
||||
qrString: String,
|
||||
accent: Color,
|
||||
boxColor: Color,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_my_qr_title),
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = accent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
QRCodeCard(qrString = qrString, boxColor = boxColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleVerificationModeButton(
|
||||
showingScanner: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onToggle,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (showingScanner) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.QrCode,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.verify_show_my_qr),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.QrCodeScanner,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.verify_scan_someone),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QRCodeCard(qrString: String, boxColor: Color) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(boxColor, RoundedCornerShape(12.dp))
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (qrString.isNotBlank()) {
|
||||
QRCodeImage(data = qrString, size = 220.dp)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(220.dp)
|
||||
.background(Color.Transparent, RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_qr_unavailable),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = qrString,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QRCodeImage(data: String, size: Dp) {
|
||||
val sizePx = with(LocalDensity.current) { size.toPx().toInt() }
|
||||
val bitmap = remember(data, sizePx) { generateQrBitmap(data, sizePx) }
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
bitmap = bitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateQrBitmap(data: String, sizePx: Int): Bitmap? {
|
||||
if (data.isBlank() || sizePx <= 0) return null
|
||||
return try {
|
||||
val matrix = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, sizePx, sizePx)
|
||||
bitmapFromMatrix(matrix)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapFromMatrix(matrix: BitMatrix): Bitmap {
|
||||
val width = matrix.width
|
||||
val height = matrix.height
|
||||
val bitmap = createBitmap(width, height)
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bitmap[x, y] =
|
||||
if (matrix[x, y]) android.graphics.Color.BLACK else android.graphics.Color.WHITE
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
private fun QRScannerPanel(
|
||||
onScan: (String) -> Unit,
|
||||
accent: Color,
|
||||
boxColor: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val permissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var lastValid by remember { mutableStateOf<String?>(null) }
|
||||
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
|
||||
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
|
||||
val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
|
||||
val surfaceRequest by surfaceRequests.collectAsState(initial = null)
|
||||
val mainHandler = remember { Handler(Looper.getMainLooper()) }
|
||||
|
||||
val onCodeState = rememberUpdatedState(onScan)
|
||||
val analyzer = remember {
|
||||
QRCodeAnalyzer { text ->
|
||||
mainHandler.post {
|
||||
if (text == lastValid) return@post
|
||||
lastValid = text
|
||||
onCodeState.value(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(permissionState.status.isGranted) {
|
||||
var cameraProvider: ProcessCameraProvider? = null
|
||||
if (permissionState.status.isGranted) {
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
cameraProviderFuture.addListener(
|
||||
{
|
||||
val provider = cameraProviderFuture.get()
|
||||
cameraProvider = provider
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.also { it.setSurfaceProvider { request -> surfaceRequests.value = request } }
|
||||
val analysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { it.setAnalyzer(cameraExecutor, analyzer) }
|
||||
|
||||
runCatching {
|
||||
provider.unbindAll()
|
||||
provider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
analysis
|
||||
)
|
||||
}.onFailure {
|
||||
Log.w("VerificationSheet", "Failed to bind camera: ${it.message}")
|
||||
}
|
||||
},
|
||||
executor
|
||||
)
|
||||
}
|
||||
|
||||
onDispose {
|
||||
surfaceRequests.value = null
|
||||
runCatching { cameraProvider?.unbindAll() }
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { cameraExecutor.shutdown() }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(boxColor, RoundedCornerShape(12.dp))
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_scan_prompt_friend),
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = accent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (permissionState.status.isGranted) {
|
||||
surfaceRequest?.let { request ->
|
||||
CameraXViewfinder(
|
||||
surfaceRequest = request,
|
||||
implementationMode = ImplementationMode.EMBEDDED,
|
||||
modifier = Modifier
|
||||
.size(220.dp)
|
||||
.clipToBounds()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_camera_permission),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
Button(
|
||||
onClick = { permissionState.launchPermissionRequest() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.verify_request_camera),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class QRCodeAnalyzer(
|
||||
private val onCode: (String) -> Unit
|
||||
) : ImageAnalysis.Analyzer {
|
||||
private val scanner = BarcodeScanning.getClient(
|
||||
BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
)
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
val mediaImage = imageProxy.image ?: run {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
val input = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
scanner.process(input)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
val text = barcodes.firstOrNull()?.rawValue
|
||||
if (!text.isNullOrBlank()) onCode(text)
|
||||
}
|
||||
.addOnCompleteListener { imageProxy.close() }
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,10 @@ object AppConstants {
|
||||
const val HIGH_NONCE_WARNING_THRESHOLD: Long = 1_000_000_000L
|
||||
}
|
||||
|
||||
object Verification {
|
||||
const val QR_MAX_AGE_SECONDS: Long = 300L // 5 minutes
|
||||
}
|
||||
|
||||
object Protocol {
|
||||
const val COMPRESSION_THRESHOLD_BYTES: Int = 100
|
||||
}
|
||||
@ -76,9 +80,9 @@ object AppConstants {
|
||||
const val SCAN_ON_DURATION_NORMAL_MS: Long = 8_000L
|
||||
const val SCAN_OFF_DURATION_NORMAL_MS: Long = 2_000L
|
||||
const val SCAN_ON_DURATION_POWER_SAVE_MS: Long = 2_000L
|
||||
const val SCAN_OFF_DURATION_POWER_SAVE_MS: Long = 8_000L
|
||||
const val SCAN_OFF_DURATION_POWER_SAVE_MS: Long = 28_000L
|
||||
const val SCAN_ON_DURATION_ULTRA_LOW_MS: Long = 1_000L
|
||||
const val SCAN_OFF_DURATION_ULTRA_LOW_MS: Long = 10_000L
|
||||
const val SCAN_OFF_DURATION_ULTRA_LOW_MS: Long = 29_000L
|
||||
const val MAX_CONNECTIONS_NORMAL: Int = 8
|
||||
const val MAX_CONNECTIONS_POWER_SAVE: Int = 8
|
||||
const val MAX_CONNECTIONS_ULTRA_LOW: Int = 4
|
||||
@ -115,6 +119,8 @@ object AppConstants {
|
||||
const val MESSAGE_DEDUP_TIMEOUT_MS: Long = 30_000L
|
||||
const val SYSTEM_EVENT_DEDUP_TIMEOUT_MS: Long = 5_000L
|
||||
const val ACTIVE_PEERS_NOTIFICATION_INTERVAL_MS: Long = 300_000L
|
||||
const val ACTION_FORCE_FINISH: String = "com.bitchat.android.ACTION_FORCE_FINISH"
|
||||
const val PERMISSION_FORCE_FINISH: String = "com.bitchat.android.permission.FORCE_FINISH"
|
||||
}
|
||||
|
||||
object Media {
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">كن أول من يضيف ملاحظة لهذا المكان.</string>
|
||||
<string name="dismiss">إغلاق</string>
|
||||
<string name="location_notes_input_placeholder">أضف ملاحظة لهذا المكان</string>
|
||||
<string name="mesh_service_notification_content">الشبكة تعمل — %1$d أقران</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d জন</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">মেশ চলছে — %1$d পিয়ার</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Aus Favoriten entfernen</string>
|
||||
<string name="cd_add_bookmark">Lesezeichen hinzufügen</string>
|
||||
|
||||
<!-- Chat‑Kopf & Barrierefreiheit -->
|
||||
<!-- Chat‑Kopf & Barrierefreiheit -->
|
||||
<string name="chat_back">zurück</string>
|
||||
<string name="chat_channel_prefix">Kanal: %1$s</string>
|
||||
<string name="chat_leave">verlassen</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Online‑Geohash‑Kanäle</string>
|
||||
<string name="cd_end_to_end_encryption">Ende‑zu‑Ende‑Verschlüsselung</string>
|
||||
|
||||
<!-- Bilder & Dateien -->
|
||||
<!-- Bilder & Dateien -->
|
||||
<string name="image_page_of">Bild %1$d von %2$d</string>
|
||||
<string name="image_unavailable">Bild nicht verfügbar</string>
|
||||
<string name="image_saved_to_downloads">Bild in "Downloads" gespeichert</string>
|
||||
@ -363,4 +363,41 @@
|
||||
<string name="location_notes_empty_desc">sei der Erste, der hier eine Notiz hinterlässt.</string>
|
||||
<string name="dismiss">schließen</string>
|
||||
<string name="location_notes_input_placeholder">füge eine Notiz zu diesem Ort hinzu</string>
|
||||
<string name="mesh_service_notification_content">Mesh läuft — %1$d Peers</string>
|
||||
|
||||
<string name="verify_title">verifizieren</string>
|
||||
<string name="verify_my_qr_title">scannen zur verifizierung</string>
|
||||
<string name="verify_scan_prompt_friend">anderen qr scannen</string>
|
||||
<string name="verify_scan_someone">anderen qr scannen</string>
|
||||
<string name="verify_show_my_qr">mein qr zeigen</string>
|
||||
<string name="verify_remove">verifizierung entfernen</string>
|
||||
<string name="verify_qr_unavailable">qr nicht verfügbar</string>
|
||||
<string name="verify_camera_permission">kamera-berechtigung nötig</string>
|
||||
<string name="verify_request_camera">kamera aktivieren</string>
|
||||
<string name="verify_paste_label">verifizierungs-url einfügen</string>
|
||||
<string name="verify_validate">validieren</string>
|
||||
<string name="verify_scanned">verifizierung angefordert</string>
|
||||
<string name="security_verification_title">sicherheitsüberprüfung</string>
|
||||
<string name="fingerprint_their">ihr fingerabdruck</string>
|
||||
<string name="fingerprint_yours">dein fingerabdruck</string>
|
||||
<string name="fingerprint_pending">handshake ausstehend</string>
|
||||
<string name="fingerprint_no_peer">privatchat öffnen</string>
|
||||
<string name="fingerprint_status_verified">verschlüsselt & verifiziert</string>
|
||||
<string name="fingerprint_status_encrypted">verschlüsselt</string>
|
||||
<string name="fingerprint_status_handshaking">handshake</string>
|
||||
<string name="fingerprint_status_failed">handshake fehlgeschlagen</string>
|
||||
<string name="fingerprint_status_uninitialized">unverschlüsselt</string>
|
||||
<string name="fingerprint_verified_label">verifiziert</string>
|
||||
<string name="fingerprint_verified_message">du hast diese person verifiziert.</string>
|
||||
<string name="fingerprint_not_verified_label">nicht verifiziert</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">vergleiche fingerabdrücke mit %1$s.</string>
|
||||
<string name="fingerprint_mark_verified">als verifiziert markieren</string>
|
||||
<string name="fingerprint_start_handshake">handshake starten</string>
|
||||
<string name="fingerprint_copy">kopieren</string>
|
||||
<string name="verify_mutual_match_title">Gegenseitige Verifizierung</string>
|
||||
<string name="verify_mutual_match_body">Du und %1$s habt euch verifiziert</string>
|
||||
<string name="verify_mutual_system_message">gegenseitige verifizierung mit %1$s</string>
|
||||
<string name="verify_success_title">Verifiziert</string>
|
||||
<string name="verify_success_body">Du hast %1$s verifiziert</string>
|
||||
<string name="verify_success_system_message">verifiziert %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">sé el primero en añadir una para este sitio.</string>
|
||||
<string name="dismiss">cerrar</string>
|
||||
<string name="location_notes_input_placeholder">agrega una nota para este lugar</string>
|
||||
<string name="mesh_service_notification_content">Mesh en ejecución — %1$d pares</string>
|
||||
|
||||
<string name="verify_title">verificar</string>
|
||||
<string name="verify_my_qr_title">escanear para verificarme</string>
|
||||
<string name="verify_scan_prompt_friend">escanear qr de otro</string>
|
||||
<string name="verify_scan_someone">escanear qr de otro</string>
|
||||
<string name="verify_show_my_qr">mostrar mi qr</string>
|
||||
<string name="verify_remove">eliminar verificación</string>
|
||||
<string name="verify_qr_unavailable">qr no disponible</string>
|
||||
<string name="verify_camera_permission">se necesita permiso de cámara</string>
|
||||
<string name="verify_request_camera">activar cámara</string>
|
||||
<string name="verify_paste_label">pegar url de verificación</string>
|
||||
<string name="verify_validate">validar</string>
|
||||
<string name="verify_scanned">verificación solicitada</string>
|
||||
<string name="security_verification_title">verificación de seguridad</string>
|
||||
<string name="fingerprint_their">su huella digital</string>
|
||||
<string name="fingerprint_yours">tu huella digital</string>
|
||||
<string name="fingerprint_pending">intercambio pendiente</string>
|
||||
<string name="fingerprint_no_peer">abrir chat privado para ver</string>
|
||||
<string name="fingerprint_status_verified">cifrado y verificado</string>
|
||||
<string name="fingerprint_status_encrypted">cifrado</string>
|
||||
<string name="fingerprint_status_handshaking">negociando</string>
|
||||
<string name="fingerprint_status_failed">falló negociación</string>
|
||||
<string name="fingerprint_status_uninitialized">no cifrado</string>
|
||||
<string name="fingerprint_verified_label">verificado</string>
|
||||
<string name="fingerprint_verified_message">has verificado esta identidad.</string>
|
||||
<string name="fingerprint_not_verified_label">no verificado</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compara huellas con %1$s por canal seguro.</string>
|
||||
<string name="fingerprint_mark_verified">marcar verificado</string>
|
||||
<string name="fingerprint_start_handshake">iniciar handshake</string>
|
||||
<string name="fingerprint_copy">copiar</string>
|
||||
<string name="verify_mutual_match_title">Verificación mutua</string>
|
||||
<string name="verify_mutual_match_body">Tú y %1$s se verificaron</string>
|
||||
<string name="verify_mutual_system_message">verificación mutua con %1$s</string>
|
||||
<string name="verify_success_title">Verificado</string>
|
||||
<string name="verify_success_body">Verificaste a %1$s</string>
|
||||
<string name="verify_success_system_message">verificado %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d نفر</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">مش در حال اجرا — %1$d همتا</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<item quantity="other">%d tao</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">Tumatakbo ang Mesh — %1$d na peer</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Retirer des favoris</string>
|
||||
<string name="cd_add_bookmark">Ajouter un signet</string>
|
||||
|
||||
<!-- En‑tête de chat & accessibilité -->
|
||||
<!-- En‑tête de chat & accessibilité -->
|
||||
<string name="chat_back">retour</string>
|
||||
<string name="chat_channel_prefix">canal : %1$s</string>
|
||||
<string name="chat_leave">quitter</string>
|
||||
@ -165,7 +165,7 @@
|
||||
<string name="cd_online_geohash_channels">Canaux Geohash en ligne</string>
|
||||
<string name="cd_end_to_end_encryption">Chiffrement de bout en bout</string>
|
||||
|
||||
<!-- Images & fichiers -->
|
||||
<!-- Images & fichiers -->
|
||||
<string name="image_page_of">Image %1$d sur %2$d</string>
|
||||
<string name="image_unavailable">Image indisponible</string>
|
||||
<string name="image_saved_to_downloads">Image enregistrée dans "Téléchargements"</string>
|
||||
@ -376,4 +376,41 @@
|
||||
<string name="location_notes_empty_desc">soyez le premier à en ajouter pour cet endroit.</string>
|
||||
<string name="dismiss">fermer</string>
|
||||
<string name="location_notes_input_placeholder">ajoutez une note pour cet endroit</string>
|
||||
<string name="mesh_service_notification_content">Mesh actif — %1$d pairs</string>
|
||||
|
||||
<string name="verify_title">vérifier</string>
|
||||
<string name="verify_my_qr_title">scanner pour me vérifier</string>
|
||||
<string name="verify_scan_prompt_friend">scanner un autre qr</string>
|
||||
<string name="verify_scan_someone">scanner un autre qr</string>
|
||||
<string name="verify_show_my_qr">afficher mon qr</string>
|
||||
<string name="verify_remove">supprimer la vérification</string>
|
||||
<string name="verify_qr_unavailable">qr indisponible</string>
|
||||
<string name="verify_camera_permission">permission caméra requise</string>
|
||||
<string name="verify_request_camera">activer caméra</string>
|
||||
<string name="verify_paste_label">coller url de vérification</string>
|
||||
<string name="verify_validate">valider</string>
|
||||
<string name="verify_scanned">vérification demandée</string>
|
||||
<string name="security_verification_title">vérification de sécurité</string>
|
||||
<string name="fingerprint_their">leur empreinte</string>
|
||||
<string name="fingerprint_yours">votre empreinte</string>
|
||||
<string name="fingerprint_pending">négociation en cours</string>
|
||||
<string name="fingerprint_no_peer">ouvrir un chat privé</string>
|
||||
<string name="fingerprint_status_verified">chiffré & vérifié</string>
|
||||
<string name="fingerprint_status_encrypted">chiffré</string>
|
||||
<string name="fingerprint_status_handshaking">négociation</string>
|
||||
<string name="fingerprint_status_failed">échec négociation</string>
|
||||
<string name="fingerprint_status_uninitialized">non chiffré</string>
|
||||
<string name="fingerprint_verified_label">vérifié</string>
|
||||
<string name="fingerprint_verified_message">vous avez vérifié cette personne.</string>
|
||||
<string name="fingerprint_not_verified_label">non vérifié</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">comparez avec %1$s via canal sûr.</string>
|
||||
<string name="fingerprint_mark_verified">marquer vérifié</string>
|
||||
<string name="fingerprint_start_handshake">lancer handshake</string>
|
||||
<string name="fingerprint_copy">copier</string>
|
||||
<string name="verify_mutual_match_title">Vérification mutuelle</string>
|
||||
<string name="verify_mutual_match_body">Vous et %1$s êtes vérifiés</string>
|
||||
<string name="verify_mutual_system_message">vérification mutuelle avec %1$s</string>
|
||||
<string name="verify_success_title">Vérifié</string>
|
||||
<string name="verify_success_body">Vous avez vérifié %1$s</string>
|
||||
<string name="verify_success_system_message">vérifié %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -14,5 +14,42 @@
|
||||
<string name="location_notes_empty_desc">היה הראשון להוסיף הערה למקום זה.</string>
|
||||
<string name="dismiss">סגור</string>
|
||||
<string name="location_notes_input_placeholder">הוסף הערה למקום זה</string>
|
||||
<string name="mesh_service_notification_content">רשת Mesh פועלת — %1$d עמיתים</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">पसंदीदा से हटाएँ</string>
|
||||
<string name="cd_add_bookmark">बुकमार्क जोड़ें</string>
|
||||
|
||||
<!-- चैट हेडर & एक्सेसिबिलिटी -->
|
||||
<!-- चैट हेडर & एक्सेसिबिलिटी -->
|
||||
<string name="chat_back">वापस</string>
|
||||
<string name="chat_channel_prefix">चैनल: %1$s</string>
|
||||
<string name="chat_leave">छोड़ें</string>
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">इस स्थान के लिए पहला नोट जोड़ें।</string>
|
||||
<string name="dismiss">बंद करें</string>
|
||||
<string name="location_notes_input_placeholder">इस स्थान के लिए एक नोट जोड़ें</string>
|
||||
<string name="mesh_service_notification_content">मेश चल रहा है — %1$d पीयर्स</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Hapus dari favorit</string>
|
||||
<string name="cd_add_bookmark">Tambah bookmark</string>
|
||||
|
||||
<!-- Header chat & aksesibilitas -->
|
||||
<!-- Header chat & aksesibilitas -->
|
||||
<string name="chat_back">kembali</string>
|
||||
<string name="chat_channel_prefix">Channel: %1$s</string>
|
||||
<string name="chat_leave">keluar</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Channel Geohash Online</string>
|
||||
<string name="cd_end_to_end_encryption">Enkripsi end-to-end</string>
|
||||
|
||||
<!-- Gambar & file -->
|
||||
<!-- Gambar & file -->
|
||||
<string name="image_page_of">Gambar %1$d dari %2$d</string>
|
||||
<string name="image_unavailable">Gambar tidak tersedia</string>
|
||||
<string name="image_saved_to_downloads">Gambar disimpan ke \"Downloads\"</string>
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">jadilah yang pertama menambahkan catatan untuk tempat ini.</string>
|
||||
<string name="dismiss">tutup</string>
|
||||
<string name="location_notes_input_placeholder">tambahkan catatan untuk tempat ini</string>
|
||||
<string name="mesh_service_notification_content">Mesh berjalan — %1$d peer</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -396,4 +396,41 @@
|
||||
<string name="location_notes_empty_desc">sii il primo ad aggiungerne una per questo posto.</string>
|
||||
<string name="dismiss">chiudi</string>
|
||||
<string name="location_notes_input_placeholder">aggiungi una nota per questo luogo</string>
|
||||
<string name="mesh_service_notification_content">Mesh in esecuzione — %1$d peer</string>
|
||||
|
||||
<string name="verify_title">verifica</string>
|
||||
<string name="verify_my_qr_title">scansiona per verificarmi</string>
|
||||
<string name="verify_scan_prompt_friend">scansiona qr altrui</string>
|
||||
<string name="verify_scan_someone">scansiona qr altrui</string>
|
||||
<string name="verify_show_my_qr">mostra mio qr</string>
|
||||
<string name="verify_remove">rimuovi verifica</string>
|
||||
<string name="verify_qr_unavailable">qr non disponibile</string>
|
||||
<string name="verify_camera_permission">permesso fotocamera necessario</string>
|
||||
<string name="verify_request_camera">abilita fotocamera</string>
|
||||
<string name="verify_paste_label">incolla url verifica</string>
|
||||
<string name="verify_validate">convalida</string>
|
||||
<string name="verify_scanned">verifica richiesta</string>
|
||||
<string name="security_verification_title">verifica sicurezza</string>
|
||||
<string name="fingerprint_their">loro impronta</string>
|
||||
<string name="fingerprint_yours">tua impronta</string>
|
||||
<string name="fingerprint_pending">handshake in corso</string>
|
||||
<string name="fingerprint_no_peer">apri chat privata</string>
|
||||
<string name="fingerprint_status_verified">criptato & verificato</string>
|
||||
<string name="fingerprint_status_encrypted">criptato</string>
|
||||
<string name="fingerprint_status_handshaking">handshake</string>
|
||||
<string name="fingerprint_status_failed">handshake fallito</string>
|
||||
<string name="fingerprint_status_uninitialized">non criptato</string>
|
||||
<string name="fingerprint_verified_label">verificato</string>
|
||||
<string name="fingerprint_verified_message">hai verificato questa persona.</string>
|
||||
<string name="fingerprint_not_verified_label">non verificato</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">confronta con %1$s via canale sicuro.</string>
|
||||
<string name="fingerprint_mark_verified">segna verificato</string>
|
||||
<string name="fingerprint_start_handshake">avvia handshake</string>
|
||||
<string name="fingerprint_copy">copia</string>
|
||||
<string name="verify_mutual_match_title">Verifica reciproca</string>
|
||||
<string name="verify_mutual_match_body">Tu e %1$s vi siete verificati</string>
|
||||
<string name="verify_mutual_system_message">verifica reciproca con %1$s</string>
|
||||
<string name="verify_success_title">Verificato</string>
|
||||
<string name="verify_success_body">Hai verificato %1$s</string>
|
||||
<string name="verify_success_system_message">verificato %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">お気に入りから削除</string>
|
||||
<string name="cd_add_bookmark">ブックマークに追加</string>
|
||||
|
||||
<!-- チャットヘッダー & アクセシビリティ -->
|
||||
<!-- チャットヘッダー & アクセシビリティ -->
|
||||
<string name="chat_back">戻る</string>
|
||||
<string name="chat_channel_prefix">チャンネル: %1$s</string>
|
||||
<string name="chat_leave">退出</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">オンライン Geohash チャンネル</string>
|
||||
<string name="cd_end_to_end_encryption">エンドツーエンド暗号化</string>
|
||||
|
||||
<!-- 画像 & ファイル -->
|
||||
<!-- 画像 & ファイル -->
|
||||
<string name="image_page_of">画像 %1$d / %2$d</string>
|
||||
<string name="image_unavailable">画像は利用できません</string>
|
||||
<string name="image_saved_to_downloads">画像を「ダウンロード」に保存しました</string>
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">この場所の最初のメモを追加しましょう。</string>
|
||||
<string name="dismiss">閉じる</string>
|
||||
<string name="location_notes_input_placeholder">この場所へのメモを追加</string>
|
||||
<string name="mesh_service_notification_content">メッシュ実行中 — %1$d ピア</string>
|
||||
|
||||
<string name="verify_title">検証</string>
|
||||
<string name="verify_my_qr_title">私を検証するためにスキャン</string>
|
||||
<string name="verify_scan_prompt_friend">他人のQRをスキャン</string>
|
||||
<string name="verify_scan_someone">他人のQRをスキャン</string>
|
||||
<string name="verify_show_my_qr">QRコードを表示</string>
|
||||
<string name="verify_remove">検証を削除</string>
|
||||
<string name="verify_qr_unavailable">QR利用不可</string>
|
||||
<string name="verify_camera_permission">QRスキャンにカメラ権限が必要です</string>
|
||||
<string name="verify_request_camera">カメラを有効化</string>
|
||||
<string name="verify_paste_label">検証URLを貼り付け</string>
|
||||
<string name="verify_validate">検証する</string>
|
||||
<string name="verify_scanned">検証をリクエストしました</string>
|
||||
<string name="security_verification_title">セキュリティ検証</string>
|
||||
<string name="fingerprint_their">相手の指紋</string>
|
||||
<string name="fingerprint_yours">あなたの指紋</string>
|
||||
<string name="fingerprint_pending">ハンドシェイク保留中</string>
|
||||
<string name="fingerprint_no_peer">指紋を見るにはプライベートチャットを開いてください</string>
|
||||
<string name="fingerprint_status_verified">暗号化&検証済み</string>
|
||||
<string name="fingerprint_status_encrypted">暗号化済み</string>
|
||||
<string name="fingerprint_status_handshaking">ハンドシェイク中</string>
|
||||
<string name="fingerprint_status_failed">ハンドシェイク失敗</string>
|
||||
<string name="fingerprint_status_uninitialized">未暗号化</string>
|
||||
<string name="fingerprint_verified_label">検証済み</string>
|
||||
<string name="fingerprint_verified_message">この人物の身元を検証しました。</string>
|
||||
<string name="fingerprint_not_verified_label">未検証</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">安全な経路で %1$s と指紋を比較してください。</string>
|
||||
<string name="fingerprint_mark_verified">検証済みにする</string>
|
||||
<string name="fingerprint_start_handshake">ハンドシェイク開始</string>
|
||||
<string name="fingerprint_copy">コピー</string>
|
||||
<string name="verify_mutual_match_title">相互検証</string>
|
||||
<string name="verify_mutual_match_body">あなたと %1$s は相互に検証しました</string>
|
||||
<string name="verify_mutual_system_message">%1$s と相互検証しました</string>
|
||||
<string name="verify_success_title">検証済み</string>
|
||||
<string name="verify_success_body">%1$s を検証しました</string>
|
||||
<string name="verify_success_system_message">%1$s を検証しました</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">რჩეულებიდან წაშლა</string>
|
||||
<string name="cd_add_bookmark">სანიშნეს დამატება</string>
|
||||
|
||||
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
|
||||
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
|
||||
<string name="chat_back">უკან</string>
|
||||
<string name="chat_channel_prefix">არხი: %1$s</string>
|
||||
<string name="chat_leave">გასვლა</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">ონლაინ geohash არხები</string>
|
||||
<string name="cd_end_to_end_encryption">ბოლო-მდე დაშიფვრა</string>
|
||||
|
||||
<!-- სურათები & ფაილები -->
|
||||
<!-- სურათები & ფაილები -->
|
||||
<string name="image_page_of">სურათი %1$d %2$d-დან</string>
|
||||
<string name="image_unavailable">სურათი მიუწვდომელია</string>
|
||||
<string name="image_saved_to_downloads">სურათი შენახულია "ჩამოტვირთვებში"</string>
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d ადამიანი</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">Mesh გაშვებულია — %1$d პირები</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">즐겨찾기에서 제거</string>
|
||||
<string name="cd_add_bookmark">북마크 추가</string>
|
||||
|
||||
<!-- 채팅 헤더 & 접근성 -->
|
||||
<!-- 채팅 헤더 & 접근성 -->
|
||||
<string name="chat_back">뒤로</string>
|
||||
<string name="chat_channel_prefix">채널: %1$s</string>
|
||||
<string name="chat_leave">나가기</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">온라인 geohash 채널</string>
|
||||
<string name="cd_end_to_end_encryption">종단간 암호화</string>
|
||||
|
||||
<!-- 이미지 & 파일 -->
|
||||
<!-- 이미지 & 파일 -->
|
||||
<string name="image_page_of">이미지 %1$d / %2$d</string>
|
||||
<string name="image_unavailable">이미지를 사용할 수 없음</string>
|
||||
<string name="image_saved_to_downloads">이미지가 \"Downloads\"에 저장됨</string>
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">이 장소에 첫 번째 노트를 추가해 보세요.</string>
|
||||
<string name="dismiss">닫기</string>
|
||||
<string name="location_notes_input_placeholder">이 장소에 노트 추가</string>
|
||||
<string name="mesh_service_notification_content">메시 실행 중 — %1$d 피어</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Hanesorana amin\'ny ankafizina</string>
|
||||
<string name="cd_add_bookmark">Hanampy bookmark</string>
|
||||
|
||||
<!-- Lohan\'ny resaka & fahafaha-miditra -->
|
||||
<!-- Lohan\'ny resaka & fahafaha-miditra -->
|
||||
<string name="chat_back">miverina</string>
|
||||
<string name="chat_channel_prefix">fantsona: %1$s</string>
|
||||
<string name="chat_leave">hiala</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Fantsona Geohash an-tserasera</string>
|
||||
<string name="cd_end_to_end_encryption">Fandokoana Farany amin\'ny Farany</string>
|
||||
|
||||
<!-- Sary & rakitra -->
|
||||
<!-- Sary & rakitra -->
|
||||
<string name="image_page_of">Sary %1$d amin\'ny %2$d</string>
|
||||
<string name="image_unavailable">Tsy misy sary</string>
|
||||
<string name="image_saved_to_downloads">Sary voatahiry ao amin\'ny Downloads</string>
|
||||
@ -376,4 +376,41 @@
|
||||
<item quantity="other">Olona %d</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">Mandeha ny Mesh — %1$d peers</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -1,5 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO: Malay translations -->
|
||||
<string name="mesh_service_notification_content">Mesh sedang berjalan — %1$d rakan</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<item quantity="other">%d जना</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">मेश चलिरहेको छ — %1$d पियर्स</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Verwijderen uit favorieten</string>
|
||||
<string name="cd_add_bookmark">Bladwijzer toevoegen</string>
|
||||
|
||||
<!-- Chatkop & toegankelijkheid -->
|
||||
<!-- Chatkop & toegankelijkheid -->
|
||||
<string name="chat_back">terug</string>
|
||||
<string name="chat_channel_prefix">kanaal: %1$s</string>
|
||||
<string name="chat_leave">verlaten</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Online geohash-kanalen</string>
|
||||
<string name="cd_end_to_end_encryption">End-to-end-versleuteling</string>
|
||||
|
||||
<!-- Afbeeldingen & bestanden -->
|
||||
<!-- Afbeeldingen & bestanden -->
|
||||
<string name="image_page_of">Afbeelding %1$d van %2$d</string>
|
||||
<string name="image_unavailable">Afbeelding niet beschikbaar</string>
|
||||
<string name="image_saved_to_downloads">Afbeelding opgeslagen in Downloads</string>
|
||||
@ -338,7 +338,7 @@
|
||||
<string name="join">Deelnemen</string>
|
||||
<string name="cancel">Annuleren</string>
|
||||
|
||||
<!-- Symbolen & speciale strings -->
|
||||
<!-- Symbolen & speciale strings -->
|
||||
<string name="at_symbol">@</string>
|
||||
<string name="app_brand">bitchat/</string>
|
||||
<string name="channel_count_prefix"> · ⧉ </string>
|
||||
@ -394,4 +394,41 @@
|
||||
<string name="location_notes_empty_desc">wees de eerste die een notitie voor deze plek toevoegt.</string>
|
||||
<string name="dismiss">sluiten</string>
|
||||
<string name="location_notes_input_placeholder">voeg een notitie toe voor deze plek</string>
|
||||
<string name="mesh_service_notification_content">Mesh actief — %1$d peers</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">پسندیدہ توں ہٹاؤ</string>
|
||||
<string name="cd_add_bookmark">بُک مارک پاؤ</string>
|
||||
|
||||
<!-- چیٹ ہیڈر & رسائی پذیری -->
|
||||
<!-- چیٹ ہیڈر & رسائی پذیری -->
|
||||
<string name="chat_back">واپس</string>
|
||||
<string name="chat_channel_prefix">چینل: %1$s</string>
|
||||
<string name="chat_leave">باہر آؤ</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">آن لائن geohash چینل</string>
|
||||
<string name="cd_end_to_end_encryption">اینڈ ٹو اینڈ انکرپشن</string>
|
||||
|
||||
<!-- تصویراں & فائلز -->
|
||||
<!-- تصویراں & فائلز -->
|
||||
<string name="image_page_of">تصویر %1$d وچوں %2$d</string>
|
||||
<string name="image_unavailable">تصویر دستیاب نئیں</string>
|
||||
<string name="image_saved_to_downloads">تصویر \"Downloads\" وچ سیو ہو گئی</string>
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d بندے</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">میش چل رہا ہے — %1$d ساتھی</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -14,5 +14,42 @@
|
||||
<string name="location_notes_empty_desc">bądź pierwszy, który doda notatkę dla tego miejsca.</string>
|
||||
<string name="dismiss">zamknij</string>
|
||||
<string name="location_notes_input_placeholder">dodaj notatkę dla tego miejsca</string>
|
||||
<string name="mesh_service_notification_content">Mesh działa — %1$d peerów</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Remover dos favoritos</string>
|
||||
<string name="cd_add_bookmark">Adicionar favorito</string>
|
||||
|
||||
<!-- Cabeçalho do chat & acessibilidade -->
|
||||
<!-- Cabeçalho do chat & acessibilidade -->
|
||||
<string name="chat_back">voltar</string>
|
||||
<string name="chat_channel_prefix">Canal: %1$s</string>
|
||||
<string name="chat_leave">sair</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Canais geohash online</string>
|
||||
<string name="cd_end_to_end_encryption">Criptografia de ponta a ponta</string>
|
||||
|
||||
<!-- Imagens & arquivos -->
|
||||
<!-- Imagens & arquivos -->
|
||||
<string name="image_page_of">Imagem %1$d de %2$d</string>
|
||||
<string name="image_unavailable">Imagem indisponível</string>
|
||||
<string name="image_saved_to_downloads">Imagem salva em "Downloads"</string>
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">seja o primeiro a adicionar uma para este local.</string>
|
||||
<string name="dismiss">fechar</string>
|
||||
<string name="location_notes_input_placeholder">adicione uma nota para este local</string>
|
||||
<string name="mesh_service_notification_content">Mesh rodando — %1$d pares</string>
|
||||
|
||||
<string name="verify_title">verificar</string>
|
||||
<string name="verify_my_qr_title">digitalize para verificar-me</string>
|
||||
<string name="verify_scan_prompt_friend">digitalizar outro qr</string>
|
||||
<string name="verify_scan_someone">digitalizar outro qr</string>
|
||||
<string name="verify_show_my_qr">mostrar meu qr</string>
|
||||
<string name="verify_remove">remover verificação</string>
|
||||
<string name="verify_qr_unavailable">qr indisponível</string>
|
||||
<string name="verify_camera_permission">permissão de câmera necessária</string>
|
||||
<string name="verify_request_camera">ativar câmera</string>
|
||||
<string name="verify_paste_label">colar url de verificação</string>
|
||||
<string name="verify_validate">validar</string>
|
||||
<string name="verify_scanned">verificação solicitada</string>
|
||||
<string name="security_verification_title">verificação de segurança</string>
|
||||
<string name="fingerprint_their">impressão digital deles</string>
|
||||
<string name="fingerprint_yours">sua impressão digital</string>
|
||||
<string name="fingerprint_pending">handshake pendente</string>
|
||||
<string name="fingerprint_no_peer">abra chat privado para ver</string>
|
||||
<string name="fingerprint_status_verified">criptografado e verificado</string>
|
||||
<string name="fingerprint_status_encrypted">criptografado</string>
|
||||
<string name="fingerprint_status_handshaking">handshake</string>
|
||||
<string name="fingerprint_status_failed">falha no handshake</string>
|
||||
<string name="fingerprint_status_uninitialized">não criptografado</string>
|
||||
<string name="fingerprint_verified_label">verificado</string>
|
||||
<string name="fingerprint_verified_message">você verificou esta pessoa.</string>
|
||||
<string name="fingerprint_not_verified_label">não verificado</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare com %1$s em canal seguro.</string>
|
||||
<string name="fingerprint_mark_verified">marcar verificado</string>
|
||||
<string name="fingerprint_start_handshake">iniciar handshake</string>
|
||||
<string name="fingerprint_copy">copiar</string>
|
||||
<string name="verify_mutual_match_title">Verificação mútua</string>
|
||||
<string name="verify_mutual_match_body">Você e %1$s verificaram-se</string>
|
||||
<string name="verify_mutual_system_message">verificação mútua com %1$s</string>
|
||||
<string name="verify_success_title">Verificado</string>
|
||||
<string name="verify_success_body">Você verificou %1$s</string>
|
||||
<string name="verify_success_system_message">verificou %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">seja o primeiro a adicionar uma para este local.</string>
|
||||
<string name="dismiss">fechar</string>
|
||||
<string name="location_notes_input_placeholder">adicione uma nota para este local</string>
|
||||
<string name="mesh_service_notification_content">Mesh em execução — %1$d pares</string>
|
||||
|
||||
<string name="verify_title">verificar</string>
|
||||
<string name="verify_my_qr_title">digitalize para verificar-me</string>
|
||||
<string name="verify_scan_prompt_friend">digitalizar outro qr</string>
|
||||
<string name="verify_scan_someone">digitalizar outro qr</string>
|
||||
<string name="verify_show_my_qr">mostrar meu qr</string>
|
||||
<string name="verify_remove">remover verificação</string>
|
||||
<string name="verify_qr_unavailable">qr indisponível</string>
|
||||
<string name="verify_camera_permission">permissão de câmera necessária</string>
|
||||
<string name="verify_request_camera">ativar câmera</string>
|
||||
<string name="verify_paste_label">colar url de verificação</string>
|
||||
<string name="verify_validate">validar</string>
|
||||
<string name="verify_scanned">verificação solicitada</string>
|
||||
<string name="security_verification_title">verificação de segurança</string>
|
||||
<string name="fingerprint_their">impressão digital deles</string>
|
||||
<string name="fingerprint_yours">sua impressão digital</string>
|
||||
<string name="fingerprint_pending">handshake pendente</string>
|
||||
<string name="fingerprint_no_peer">abra chat privado para ver</string>
|
||||
<string name="fingerprint_status_verified">criptografado e verificado</string>
|
||||
<string name="fingerprint_status_encrypted">criptografado</string>
|
||||
<string name="fingerprint_status_handshaking">handshake</string>
|
||||
<string name="fingerprint_status_failed">falha no handshake</string>
|
||||
<string name="fingerprint_status_uninitialized">não criptografado</string>
|
||||
<string name="fingerprint_verified_label">verificado</string>
|
||||
<string name="fingerprint_verified_message">você verificou esta pessoa.</string>
|
||||
<string name="fingerprint_not_verified_label">não verificado</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare com %1$s em canal seguro.</string>
|
||||
<string name="fingerprint_mark_verified">marcar verificado</string>
|
||||
<string name="fingerprint_start_handshake">iniciar handshake</string>
|
||||
<string name="fingerprint_copy">copiar</string>
|
||||
<string name="verify_mutual_match_title">Verificação mútua</string>
|
||||
<string name="verify_mutual_match_body">Você e %1$s verificaram-se</string>
|
||||
<string name="verify_mutual_system_message">verificação mútua com %1$s</string>
|
||||
<string name="verify_success_title">Verificado</string>
|
||||
<string name="verify_success_body">Você verificou %1$s</string>
|
||||
<string name="verify_success_system_message">verificou %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -352,4 +352,41 @@
|
||||
<string name="location_notes_empty_desc">станьте первым, кто добавит заметку для этого места.</string>
|
||||
<string name="dismiss">закрыть</string>
|
||||
<string name="location_notes_input_placeholder">добавьте заметку для этого места</string>
|
||||
<string name="mesh_service_notification_content">Mesh запущен — %1$d пиров</string>
|
||||
|
||||
<string name="verify_title">проверить</string>
|
||||
<string name="verify_my_qr_title">сканируй для проверки</string>
|
||||
<string name="verify_scan_prompt_friend">сканировать чужой QR</string>
|
||||
<string name="verify_scan_someone">сканировать чужой QR</string>
|
||||
<string name="verify_show_my_qr">показать мой QR</string>
|
||||
<string name="verify_remove">удалить проверку</string>
|
||||
<string name="verify_qr_unavailable">QR недоступен</string>
|
||||
<string name="verify_camera_permission">нужен доступ к камере</string>
|
||||
<string name="verify_request_camera">включить камеру</string>
|
||||
<string name="verify_paste_label">вставить URL проверки</string>
|
||||
<string name="verify_validate">проверить</string>
|
||||
<string name="verify_scanned">запрос проверки</string>
|
||||
<string name="security_verification_title">проверка безопасности</string>
|
||||
<string name="fingerprint_their">их отпечаток</string>
|
||||
<string name="fingerprint_yours">ваш отпечаток</string>
|
||||
<string name="fingerprint_pending">ожидание рукопожатия</string>
|
||||
<string name="fingerprint_no_peer">откройте личный чат</string>
|
||||
<string name="fingerprint_status_verified">шифровано и проверено</string>
|
||||
<string name="fingerprint_status_encrypted">шифровано</string>
|
||||
<string name="fingerprint_status_handshaking">рукопожатие</string>
|
||||
<string name="fingerprint_status_failed">сбой рукопожатия</string>
|
||||
<string name="fingerprint_status_uninitialized">не шифровано</string>
|
||||
<string name="fingerprint_verified_label">проверено</string>
|
||||
<string name="fingerprint_verified_message">вы проверили этого человека.</string>
|
||||
<string name="fingerprint_not_verified_label">не проверено</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">сравните с %1$s.</string>
|
||||
<string name="fingerprint_mark_verified">отметить проверенным</string>
|
||||
<string name="fingerprint_start_handshake">начать рукопожатие</string>
|
||||
<string name="fingerprint_copy">копировать</string>
|
||||
<string name="verify_mutual_match_title">Взаимная проверка</string>
|
||||
<string name="verify_mutual_match_body">Вы и %1$s проверили друг друга</string>
|
||||
<string name="verify_mutual_system_message">взаимная проверка с %1$s</string>
|
||||
<string name="verify_success_title">Проверено</string>
|
||||
<string name="verify_success_body">Вы проверили %1$s</string>
|
||||
<string name="verify_success_system_message">проверен %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -350,4 +350,41 @@
|
||||
<string name="location_notes_empty_desc">var först med att lägga till en anteckning för den här platsen.</string>
|
||||
<string name="dismiss">stäng</string>
|
||||
<string name="location_notes_input_placeholder">lägg till en anteckning för den här platsen</string>
|
||||
<string name="mesh_service_notification_content">Mesh körs — %1$d peers</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -1,5 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO: Tamil translations -->
|
||||
<string name="mesh_service_notification_content">மெஷ் இயங்குகிறது — %1$d பியர்ஸ்</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">เอาออกจากรายการโปรด</string>
|
||||
<string name="cd_add_bookmark">เพิ่มที่คั่นหน้า</string>
|
||||
|
||||
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
|
||||
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
|
||||
<string name="chat_back">กลับ</string>
|
||||
<string name="chat_channel_prefix">ช่อง: %1$s</string>
|
||||
<string name="chat_leave">ออก</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">ช่อง geohash ออนไลน์</string>
|
||||
<string name="cd_end_to_end_encryption">การเข้ารหัสแบบปลายทางถึงปลายทาง</string>
|
||||
|
||||
<!-- รูปภาพ & ไฟล์ -->
|
||||
<!-- รูปภาพ & ไฟล์ -->
|
||||
<string name="image_page_of">รูปภาพ %1$d จาก %2$d</string>
|
||||
<string name="image_unavailable">ไม่สามารถใช้รูปภาพได้</string>
|
||||
<string name="image_saved_to_downloads">บันทึกรูปภาพไว้ใน "ดาวน์โหลด" แล้ว</string>
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d คน</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">Mesh กำลังทำงาน — %1$d เพื่อน</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -350,4 +350,41 @@
|
||||
<string name="location_notes_empty_desc">bu yer için ilk notu ekleyen siz olun.</string>
|
||||
<string name="dismiss">kapat</string>
|
||||
<string name="location_notes_input_placeholder">bu yer için bir not ekleyin</string>
|
||||
<string name="mesh_service_notification_content">Mesh çalışıyor — %1$d eş</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -1,5 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO: Ukrainian translations -->
|
||||
<string name="mesh_service_notification_content">Mesh працює — %1$d пірів</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -362,4 +362,41 @@
|
||||
<string name="location_notes_empty_desc">اس جگہ کے لیے پہلا نوٹ شامل کریں۔</string>
|
||||
<string name="dismiss">بند کریں</string>
|
||||
<string name="location_notes_input_placeholder">اس جگہ کے لیے ایک نوٹ شامل کریں</string>
|
||||
<string name="mesh_service_notification_content">میش چل رہا ہے — %1$d ساتھی</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Xóa khỏi yêu thích</string>
|
||||
<string name="cd_add_bookmark">Thêm bookmark</string>
|
||||
|
||||
<!-- Header trò chuyện & khả năng truy cập -->
|
||||
<!-- Header trò chuyện & khả năng truy cập -->
|
||||
<string name="chat_back">quay lại</string>
|
||||
<string name="chat_channel_prefix">Kênh: %1$s</string>
|
||||
<string name="chat_leave">rời khỏi</string>
|
||||
@ -170,7 +170,7 @@
|
||||
<string name="cd_online_geohash_channels">Kênh Geohash trực tuyến</string>
|
||||
<string name="cd_end_to_end_encryption">Mã hóa đầu cuối</string>
|
||||
|
||||
<!-- Hình ảnh & file -->
|
||||
<!-- Hình ảnh & file -->
|
||||
<string name="image_page_of">Hình ảnh %1$d trong %2$d</string>
|
||||
<string name="image_unavailable">Hình ảnh không có sẵn</string>
|
||||
<string name="image_saved_to_downloads">Hình ảnh đã lưu vào \"Downloads\"</string>
|
||||
@ -349,4 +349,41 @@
|
||||
<item quantity="other">%d người</item>
|
||||
</plurals>
|
||||
|
||||
<string name="mesh_service_notification_content">Mesh đang chạy — %1$d ngang hàng</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -14,5 +14,42 @@
|
||||
<string name="location_notes_empty_desc">成为第一个为此地点添加笔记的人。</string>
|
||||
<string name="dismiss">关闭</string>
|
||||
<string name="location_notes_input_placeholder">为此地点添加一条笔记</string>
|
||||
<string name="mesh_service_notification_content">Mesh 运行中 — %1$d 个节点</string>
|
||||
|
||||
<string name="verify_title">验证</string>
|
||||
<string name="verify_my_qr_title">扫描以验证我</string>
|
||||
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
|
||||
<string name="verify_scan_someone">扫描他人的QR码</string>
|
||||
<string name="verify_show_my_qr">显示我的QR码</string>
|
||||
<string name="verify_remove">移除验证</string>
|
||||
<string name="verify_qr_unavailable">QR码不可用</string>
|
||||
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
|
||||
<string name="verify_request_camera">启用相机</string>
|
||||
<string name="verify_paste_label">粘贴验证链接</string>
|
||||
<string name="verify_validate">验证</string>
|
||||
<string name="verify_scanned">已请求验证</string>
|
||||
<string name="security_verification_title">安全验证</string>
|
||||
<string name="fingerprint_their">对方指纹</string>
|
||||
<string name="fingerprint_yours">你的指纹</string>
|
||||
<string name="fingerprint_pending">握手中</string>
|
||||
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
|
||||
<string name="fingerprint_status_verified">已加密且已验证</string>
|
||||
<string name="fingerprint_status_encrypted">已加密</string>
|
||||
<string name="fingerprint_status_handshaking">握手中</string>
|
||||
<string name="fingerprint_status_failed">握手失败</string>
|
||||
<string name="fingerprint_status_uninitialized">未加密</string>
|
||||
<string name="fingerprint_verified_label">已验证</string>
|
||||
<string name="fingerprint_verified_message">你已验证此人身份。</string>
|
||||
<string name="fingerprint_not_verified_label">未验证</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
|
||||
<string name="fingerprint_mark_verified">标记为已验证</string>
|
||||
<string name="fingerprint_start_handshake">开始握手</string>
|
||||
<string name="fingerprint_copy">复制</string>
|
||||
<string name="verify_mutual_match_title">相互验证</string>
|
||||
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
|
||||
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -14,5 +14,42 @@
|
||||
<string name="location_notes_empty_desc">成為第一個為此地點新增筆記的人。</string>
|
||||
<string name="dismiss">關閉</string>
|
||||
<string name="location_notes_input_placeholder">為此地點新增一則筆記</string>
|
||||
<string name="mesh_service_notification_content">Mesh 運行中 — %1$d 個節點</string>
|
||||
|
||||
<string name="verify_title">验证</string>
|
||||
<string name="verify_my_qr_title">扫描以验证我</string>
|
||||
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
|
||||
<string name="verify_scan_someone">扫描他人的QR码</string>
|
||||
<string name="verify_show_my_qr">显示我的QR码</string>
|
||||
<string name="verify_remove">移除验证</string>
|
||||
<string name="verify_qr_unavailable">QR码不可用</string>
|
||||
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
|
||||
<string name="verify_request_camera">启用相机</string>
|
||||
<string name="verify_paste_label">粘贴验证链接</string>
|
||||
<string name="verify_validate">验证</string>
|
||||
<string name="verify_scanned">已请求验证</string>
|
||||
<string name="security_verification_title">安全验证</string>
|
||||
<string name="fingerprint_their">对方指纹</string>
|
||||
<string name="fingerprint_yours">你的指纹</string>
|
||||
<string name="fingerprint_pending">握手中</string>
|
||||
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
|
||||
<string name="fingerprint_status_verified">已加密且已验证</string>
|
||||
<string name="fingerprint_status_encrypted">已加密</string>
|
||||
<string name="fingerprint_status_handshaking">握手中</string>
|
||||
<string name="fingerprint_status_failed">握手失败</string>
|
||||
<string name="fingerprint_status_uninitialized">未加密</string>
|
||||
<string name="fingerprint_verified_label">已验证</string>
|
||||
<string name="fingerprint_verified_message">你已验证此人身份。</string>
|
||||
<string name="fingerprint_not_verified_label">未验证</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
|
||||
<string name="fingerprint_mark_verified">标记为已验证</string>
|
||||
<string name="fingerprint_start_handshake">开始握手</string>
|
||||
<string name="fingerprint_copy">复制</string>
|
||||
<string name="verify_mutual_match_title">相互验证</string>
|
||||
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
|
||||
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@ -375,4 +375,41 @@
|
||||
<string name="location_notes_empty_desc">成为第一个为此地点添加笔记的人。</string>
|
||||
<string name="dismiss">关闭</string>
|
||||
<string name="location_notes_input_placeholder">为此地点添加一条笔记</string>
|
||||
<string name="mesh_service_notification_content">Mesh 运行中 — %1$d 个节点</string>
|
||||
|
||||
<string name="verify_title">验证</string>
|
||||
<string name="verify_my_qr_title">扫描以验证我</string>
|
||||
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
|
||||
<string name="verify_scan_someone">扫描他人的QR码</string>
|
||||
<string name="verify_show_my_qr">显示我的QR码</string>
|
||||
<string name="verify_remove">移除验证</string>
|
||||
<string name="verify_qr_unavailable">QR码不可用</string>
|
||||
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
|
||||
<string name="verify_request_camera">启用相机</string>
|
||||
<string name="verify_paste_label">粘贴验证链接</string>
|
||||
<string name="verify_validate">验证</string>
|
||||
<string name="verify_scanned">已请求验证</string>
|
||||
<string name="security_verification_title">安全验证</string>
|
||||
<string name="fingerprint_their">对方指纹</string>
|
||||
<string name="fingerprint_yours">你的指纹</string>
|
||||
<string name="fingerprint_pending">握手中</string>
|
||||
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
|
||||
<string name="fingerprint_status_verified">已加密且已验证</string>
|
||||
<string name="fingerprint_status_encrypted">已加密</string>
|
||||
<string name="fingerprint_status_handshaking">握手中</string>
|
||||
<string name="fingerprint_status_failed">握手失败</string>
|
||||
<string name="fingerprint_status_uninitialized">未加密</string>
|
||||
<string name="fingerprint_verified_label">已验证</string>
|
||||
<string name="fingerprint_verified_message">你已验证此人身份。</string>
|
||||
<string name="fingerprint_not_verified_label">未验证</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
|
||||
<string name="fingerprint_mark_verified">标记为已验证</string>
|
||||
<string name="fingerprint_start_handshake">开始握手</string>
|
||||
<string name="fingerprint_copy">复制</string>
|
||||
<string name="verify_mutual_match_title">相互验证</string>
|
||||
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
|
||||
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
|
||||
<string name="verify_success_title">已验证</string>
|
||||
<string name="verify_success_body">你已验证 %1$s</string>
|
||||
<string name="verify_success_system_message">已验证 %1$s</string>
|
||||
</resources>
|
||||
|
||||
@ -64,14 +64,14 @@
|
||||
<!-- Mesh Service Foreground Notification -->
|
||||
<string name="mesh_service_channel_name">Mesh Background Service</string>
|
||||
<string name="mesh_service_channel_desc">Keeps the Bluetooth mesh running in the background</string>
|
||||
<string name="mesh_service_notification_content">Mesh running — %1$d users connected</string>
|
||||
<string name="mesh_service_notification_content">Mesh running — %1$d peers</string>
|
||||
|
||||
<!-- Favorites accessibility -->
|
||||
<string name="cd_add_favorite">Add to favorites</string>
|
||||
<string name="cd_remove_favorite">Remove from favorites</string>
|
||||
<string name="cd_add_bookmark">Add bookmark</string>
|
||||
|
||||
<!-- Chat header & accessibility -->
|
||||
<!-- Chat header & accessibility -->
|
||||
<string name="chat_back">back</string>
|
||||
<string name="chat_channel_prefix">channel: %1$s</string>
|
||||
<string name="chat_leave">leave</string>
|
||||
@ -265,6 +265,44 @@
|
||||
<string name="debug_none">none</string>
|
||||
<string name="debug_disconnect">disconnect</string>
|
||||
<string name="debug_recent_scan_results">recent scan results</string>
|
||||
|
||||
<string name="verify_title">verify</string>
|
||||
<string name="verify_my_qr_title">scan to verify me</string>
|
||||
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
|
||||
<string name="verify_scan_someone">scan someone elses qr</string>
|
||||
<string name="verify_show_my_qr">show my qr</string>
|
||||
<string name="verify_remove">remove verification</string>
|
||||
<string name="verify_qr_unavailable">qr unavailable</string>
|
||||
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
|
||||
<string name="verify_request_camera">enable camera</string>
|
||||
<string name="verify_paste_label">paste verification url</string>
|
||||
<string name="verify_validate">validate</string>
|
||||
<string name="verify_scanned">verification requested</string>
|
||||
<string name="security_verification_title">security verification</string>
|
||||
<string name="fingerprint_their">their fingerprint</string>
|
||||
<string name="fingerprint_yours">your fingerprint</string>
|
||||
<string name="fingerprint_pending">handshake pending</string>
|
||||
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
|
||||
<string name="fingerprint_status_verified">encrypted & verified</string>
|
||||
<string name="fingerprint_status_encrypted">encrypted</string>
|
||||
<string name="fingerprint_status_handshaking">handshaking</string>
|
||||
<string name="fingerprint_status_failed">handshake failed</string>
|
||||
<string name="fingerprint_status_uninitialized">not encrypted</string>
|
||||
<string name="fingerprint_verified_label">verified</string>
|
||||
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
|
||||
<string name="fingerprint_not_verified_label">not verified</string>
|
||||
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
|
||||
<string name="fingerprint_mark_verified">mark as verified</string>
|
||||
<string name="fingerprint_start_handshake">start handshake</string>
|
||||
<string name="fingerprint_copy">copy</string>
|
||||
|
||||
<string name="verify_mutual_match_title">Mutual verification</string>
|
||||
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
|
||||
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
|
||||
<string name="verify_success_title">Verified</string>
|
||||
<string name="verify_success_body">You verified %1$s</string>
|
||||
<string name="verify_success_system_message">verified %1$s</string>
|
||||
|
||||
<string name="debug_connect">connect</string>
|
||||
<string name="debug_debug_console">debug console</string>
|
||||
<string name="debug_clear">clear</string>
|
||||
@ -282,6 +320,15 @@
|
||||
<string name="location_explanation">bitchat does NOT track your location.\n\nLocation services are required for Bluetooth scanning and for the Geohash chat feature.</string>
|
||||
<string name="location_needs_for">bitchat needs location services for:</string>
|
||||
<string name="location_needs_bullets">• Bluetooth device scanning\n• Discovering nearby users on mesh network\n• Geohash chat feature\n• No tracking or location collection</string>
|
||||
<string name="background_location_required_title">Background Location Recommended</string>
|
||||
<string name="background_location_required_subtitle">optional, improves mesh reliability</string>
|
||||
<string name="background_location_explanation">Android recommends background location so bitchat can scan for nearby devices while the app is not open. This keeps the mesh alive after reboot.</string>
|
||||
<string name="background_location_settings_tip">When settings opens, choose "Allow all the time".</string>
|
||||
<string name="background_location_needs_for">bitchat uses background location for:</string>
|
||||
<string name="background_location_needs_bullets">- scan for nearby devices while the app is closed\n- reconnect after reboot\n- keep the mesh running in the background</string>
|
||||
<string name="background_location_privacy_note">We NEVER collect or store your location. Your privacy is safe.</string>
|
||||
<string name="grant_background_location">Allow Background Location</string>
|
||||
<string name="skip_background_location">Continue without background location</string>
|
||||
<string name="open_location_settings">Open Location Settings</string>
|
||||
<string name="check_again">Check Again</string>
|
||||
<string name="location_services_unavailable">Location Services Unavailable</string>
|
||||
@ -367,6 +414,8 @@
|
||||
<string name="perm_nearby_devices_system">Allow bitchat to connect to nearby devices</string>
|
||||
<string name="perm_location_desc">Required by Android to discover nearby bitchat users via Bluetooth</string>
|
||||
<string name="perm_location_system">bitchat needs this to scan for nearby devices</string>
|
||||
<string name="perm_background_location_desc">Recommended to scan for nearby devices while the app is in the background</string>
|
||||
<string name="perm_background_location_system">Allow background location access</string>
|
||||
<string name="perm_notifications_desc">Receive notifications when you receive private messages</string>
|
||||
<string name="perm_notifications_system">Allow bitchat to send you notifications</string>
|
||||
<string name="perm_battery_desc">Disable battery optimization to ensure bitchat runs reliably in the background and maintains mesh network connections</string>
|
||||
@ -375,6 +424,7 @@
|
||||
<!-- Permission types -->
|
||||
<string name="perm_type_nearby_devices">Nearby Devices</string>
|
||||
<string name="perm_type_precise_location">Precise Location</string>
|
||||
<string name="perm_type_background_location">Background Location</string>
|
||||
<string name="perm_type_microphone">Microphone</string>
|
||||
<string name="perm_type_notifications">Notifications</string>
|
||||
<string name="perm_type_battery_optimization">Battery Optimization</string>
|
||||
|
||||
@ -8,12 +8,14 @@ import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.ConscryptMode
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.Date
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@ConscryptMode(ConscryptMode.Mode.OFF) // Disable Conscrypt to avoid native library loading issues
|
||||
class FileTransferTest {
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,286 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.os.Build
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.*
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||
class SecurityManagerTest {
|
||||
|
||||
private lateinit var securityManager: SecurityManager
|
||||
private lateinit var fakeEncryptionService: FakeEncryptionService
|
||||
private lateinit var mockDelegate: SecurityManagerDelegate
|
||||
|
||||
private val myPeerID = "1111222233334444"
|
||||
private val otherPeerID = "aaaabbbbccccdddd"
|
||||
private val unknownPeerID = "9999888877776666"
|
||||
|
||||
private val dummyPayload = "Hello World".toByteArray()
|
||||
private val validSignature = ByteArray(64) { 1 }
|
||||
private val invalidSignature = ByteArray(64) { 0 }
|
||||
|
||||
// Key pairs (using dummy bytes for mock verification)
|
||||
private val otherSigningKey = ByteArray(32) { 0xA }
|
||||
private val otherNoiseKey = ByteArray(32) { 0xB }
|
||||
|
||||
// Fake implementation to bypass initialization issues in tests
|
||||
open class FakeEncryptionService : EncryptionService(RuntimeEnvironment.getApplication()) {
|
||||
var shouldVerify: Boolean = true
|
||||
var lastVerifySignature: ByteArray? = null
|
||||
var lastVerifyKey: ByteArray? = null
|
||||
|
||||
override fun initialize() {
|
||||
// Do nothing to avoid KeyStore access in tests
|
||||
}
|
||||
|
||||
override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKeyBytes: ByteArray): Boolean {
|
||||
lastVerifySignature = signature
|
||||
lastVerifyKey = publicKeyBytes
|
||||
|
||||
// Simple logic: if configured to verify, check if signature matches validSignature
|
||||
// We use the signature bytes passed in setup()
|
||||
if (shouldVerify) {
|
||||
return signature.contentEquals(byteArrayOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
fakeEncryptionService = FakeEncryptionService()
|
||||
mockDelegate = mock()
|
||||
|
||||
securityManager = SecurityManager(fakeEncryptionService, myPeerID)
|
||||
securityManager.delegate = mockDelegate
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
if (::securityManager.isInitialized) {
|
||||
securityManager.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet with missing signature`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = null
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet without signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet with invalid signature`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = invalidSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet with invalid signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects packet from unknown peer (no key)`() {
|
||||
whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("Packet from unknown peer should be rejected (cannot verify signature)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - accepts packet with valid signature from known peer`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertTrue("Valid signed packet from known peer should be accepted", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - accepts ANNOUNCE packet from unknown peer (extracts key)`() {
|
||||
val announcement = IdentityAnnouncement(
|
||||
nickname = "New User",
|
||||
noisePublicKey = otherNoiseKey,
|
||||
signingPublicKey = otherSigningKey
|
||||
)
|
||||
val payload = announcement.encode()!!
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = payload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null)
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertTrue("ANNOUNCE from unknown peer should be accepted (key extracted from payload)", result)
|
||||
// Verify we used the correct key
|
||||
assertTrue("Should have used extracted key for verification",
|
||||
fakeEncryptionService.lastVerifyKey.contentEquals(otherSigningKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects ANNOUNCE packet with invalid signature`() {
|
||||
val announcement = IdentityAnnouncement(
|
||||
nickname = "New User",
|
||||
noisePublicKey = otherNoiseKey,
|
||||
signingPublicKey = otherSigningKey
|
||||
)
|
||||
val payload = announcement.encode()!!
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = payload
|
||||
)
|
||||
packet.signature = invalidSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("ANNOUNCE with invalid signature should be rejected", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects ANNOUNCE packet with malformed payload`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = 10u,
|
||||
senderID = unknownPeerID,
|
||||
payload = byteArrayOf(0x00, 0x01, 0x02)
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, unknownPeerID)
|
||||
|
||||
assertFalse("ANNOUNCE with malformed payload should be rejected (cannot extract key)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - ignores own packets`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = myPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = null
|
||||
|
||||
val result = securityManager.validatePacket(packet, myPeerID)
|
||||
|
||||
assertFalse("Own packets should return false (skipped)", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - detects duplicates`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result1 = securityManager.validatePacket(packet, otherPeerID)
|
||||
assertTrue("First packet should be accepted", result1)
|
||||
|
||||
val result2 = securityManager.validatePacket(packet, otherPeerID)
|
||||
assertFalse("Duplicate packet should be rejected", result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - handles ANNOUNCE duplicates correctly`() {
|
||||
val announcement = IdentityAnnouncement(
|
||||
nickname = "New User",
|
||||
noisePublicKey = otherNoiseKey,
|
||||
signingPublicKey = otherSigningKey
|
||||
)
|
||||
val payload = announcement.encode()!!
|
||||
|
||||
// 1. Initial Announce (Fresh)
|
||||
val packet1 = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS, // 7u
|
||||
senderID = unknownPeerID,
|
||||
payload = payload
|
||||
)
|
||||
packet1.signature = validSignature
|
||||
|
||||
whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null)
|
||||
|
||||
assertTrue("First ANNOUNCE should be accepted", securityManager.validatePacket(packet1, unknownPeerID))
|
||||
|
||||
// 2. Relayed Duplicate (Lower TTL)
|
||||
val packet2 = packet1.copy(ttl = (com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS - 1u).toUByte())
|
||||
assertFalse("Relayed duplicate ANNOUNCE should be rejected", securityManager.validatePacket(packet2, unknownPeerID))
|
||||
|
||||
// 3. Direct Duplicate (Max TTL)
|
||||
val packet3 = packet1.copy(ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS)
|
||||
assertTrue("Fresh duplicate ANNOUNCE should be accepted", securityManager.validatePacket(packet3, unknownPeerID))
|
||||
}
|
||||
|
||||
private fun setupKnownPeer(peerID: String, signingKey: ByteArray) {
|
||||
val info = PeerInfo(
|
||||
id = peerID,
|
||||
nickname = "Test User",
|
||||
isConnected = true,
|
||||
isDirectConnection = true,
|
||||
noisePublicKey = ByteArray(32),
|
||||
signingPublicKey = signingKey,
|
||||
isVerifiedNickname = false,
|
||||
lastSeen = System.currentTimeMillis()
|
||||
)
|
||||
whenever(mockDelegate.getPeerInfo(peerID)).thenReturn(info)
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ agp = "8.10.1"
|
||||
kotlin = "2.2.0"
|
||||
compileSdk = "35"
|
||||
minSdk = "26" # API 26 for proper BLE support
|
||||
targetSdk = "34"
|
||||
targetSdk = "35"
|
||||
|
||||
# AndroidX Core
|
||||
core-ktx = "1.16.0"
|
||||
@ -45,6 +45,13 @@ gms-location = "21.3.0"
|
||||
# Security
|
||||
security-crypto = "1.1.0-beta01"
|
||||
|
||||
# QR
|
||||
zxing-core = "3.5.4"
|
||||
|
||||
# CameraX / ML Kit
|
||||
camerax = "1.5.2"
|
||||
mlkit-barcode = "17.3.0"
|
||||
|
||||
# Testing
|
||||
junit = "4.13.2"
|
||||
androidx-test-ext = "1.2.1"
|
||||
@ -54,9 +61,12 @@ mockito-inline = "4.1.0"
|
||||
roboelectric = "4.15"
|
||||
kotlinx-coroutines-test = "1.6"
|
||||
|
||||
lifecycle-process = "2.8.7"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
|
||||
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-process" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
@ -104,6 +114,15 @@ gms-location = { module = "com.google.android.gms:play-services-location", versi
|
||||
# Security
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }
|
||||
|
||||
# QR
|
||||
zxing-core = { module = "com.google.zxing:core", version.ref = "zxing-core" }
|
||||
|
||||
# CameraX / ML Kit
|
||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
|
||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
|
||||
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
|
||||
mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkit-barcode" }
|
||||
|
||||
# Testing
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user