Merge remote-tracking branch 'upstream/main' into feature/blur-image

# Conflicts:
#	app/src/main/assets/nostr_relays.csv
This commit is contained in:
yet300 2026-01-06 10:47:35 +04:00
commit 5beae6e6d5
76 changed files with 4818 additions and 530 deletions

View File

@ -1,6 +1,7 @@
name: Android CI
on:
workflow_dispatch:
push:
branches: [ "main", "develop" ]
pull_request:

View File

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

View File

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

View File

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

View File

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

View File

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

1 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
2 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
3 v-relay.d02.vrtmrz.net 34.6937 135.502
4 nostr.mikoshi.de relay.thebluepulse.com 47.74 49.4521 12.0917 11.0767
5 nostr.coincards.com relay.guggero.org 53.5501 47.3769 -113.469 8.54169
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
6 relay.lumina.rocks 49.0291 8.35695
7 nostr-rs-relay-ishosta.phamthanh.me relay-freeharmonypeople.space 43.6532 38.7223 -79.3832 -9.13934
8 nostr.snowbla.de nostr-relay.online 60.1699 43.6532 24.9384 -79.3832
9 strfry.felixzieger.de shu05.shugur.net 50.1013 48.8566 8.62643 2.35222
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
10 relay.evanverma.com 40.8302 -74.1299
11 nostr-2.21crypto.ch relay2.angor.io 47.5356 48.1046 8.73209 11.6002
12 relay.arx-ccn.com 50.4754 12.3683
13 relay.davidebtc.me 51.5072 -0.127586
14 temp.iris.to 43.6532 -79.3832
15 nostr.superfriends.online 43.6532 -79.3832
16 vitor.nostr1.com 40.7057 -74.0136
17 relay.moinsen.com 50.4754 12.3683
18 ithurtswhenip.ee 51.223 6.78245
19 nostr.tadryanom.me 43.6532 -79.3832
20 relay.nostr.wirednet.jp 34.706 135.493
21 relay.0xchat.com 1.35208 103.82
22 nostrcheck.tnsor.network 43.6532 -79.3832
23 relay.ngengine.org 43.6532 -79.3832
24 relay.mitchelltribe.com 39.0438 -77.4874
25 relaynostr.breadslice.com wotr.relatr.xyz 43.6532 53.3498 -79.3832 -6.26031
26 nostr.chaima.info relay.chorus.community 51.223 50.1109 6.78245 8.68213
27 nostr.mom orly.ft.hn 50.4754 12.3683
28 nostr.stakey.net 52.3676 4.90414
29 relay.malxte.de 52.52 13.405
30 nostrelay.circum.space 52.3676 4.90414
31 nostrcheck.me 43.6532 -79.3832
32 wot.sudocarlos.com 51.5072 -0.127586
33 relay.routstr.com 43.6532 -79.3832
34 relay.siamdev.cc 13.8434 100.363
35 wot.sovbit.host 64.1466 -21.9426
36 strfry.bonsai.com 37.8715 -122.273
37 freeben666.fr 43.7221 7.15296
38 okn.czas.top 51.267 6.81738
39 relay.nostar.org 43.6532 -79.3832
40 relay.jeffg.fyi 43.6532 -79.3832
41 nostr-02.uid.ovh 43.6532 -79.3832
42 relay.fr13nd5.com 52.5233 13.3426
43 srtrelay.c-stellar.net 43.6532 -79.3832
44 relay.dwadziesciajeden.pl 52.2297 21.0122
45 relay.sigit.io 50.4754 12.3683
46 nostr.girino.org 43.6532 -79.3832
47 bitsat.molonlabe.holdings 51.4012 -1.3147
48 nostream.breadslice.com 1.35208 103.82
49 nostr.rtvslawenia.com 49.4543 11.0746
50 nostr.openhoofd.nl 51.9229 4.40833
51 librerelay.aaroniumii.com 43.6532 -79.3832
52 relay.etch.social 41.2619 -95.8608
53 strfry.felixzieger.de 50.1013 8.62643
54 relay.wavlake.com 41.2619 -95.8608
55 relay.fundstr.me 42.3601 -71.0589
56 slick.mjex.me 39.048 -77.4817
57 relay.upleb.uk 51.9194 19.1451
58 nos4smartnkind.tech 40.1872 44.5152
59 discovery.eu.nostria.app 52.3676 4.90414
60 nostr.tac.lol 47.4748 -122.273
61 wot.nostr.net 43.6532 -79.3832
62 nostr-dev.wellorder.net 45.5201 -122.99
63 fanfares.nostr1.com 40.7128 -74.006
64 relay.bitcoindistrict.org 43.6532 -79.3832
65 nostr-relay.nextblockvending.com 47.2343 -119.853
66 relay-rpi.edufeed.org 49.4521 11.0767
67 relay.nsnip.io 60.1699 24.9384
68 black.nostrcity.club 48.8575 2.35138
69 nostr-02.czas.top 51.2277 6.77346
70 schnorr.me 43.6532 -79.3832
71 relay.fountain.fm 39.0997 -94.5786
72 nostr-verified.wellorder.net 45.5201 -122.99
73 relay.divine.video 43.6532 -79.3832
74 relay.thibautduchene.fr 43.6532 -79.3832
75 nostr.plantroon.com 50.1013 8.62643
76 relay.hasenpfeffr.com 39.0438 -77.4874
77 relay.chakany.systems 43.6532 -79.3832
78 nostr-01.yakihonne.com 1.32123 103.695
79 nostr.azzamo.net 52.2633 21.0283
80 nostr.bond 50.1109 8.68213
81 nostr.coincards.com 53.5501 -113.469
82 relay.nostrhub.tech 49.0291 8.35696
83 relay.cosmicbolt.net 37.3986 -121.964
84 nostr.21crypto.ch 47.5356 8.73209
85 relay-dev.satlantis.io 40.8302 -74.1299
86 offchain.pub 47.6743 -117.112
87 relay.orangepill.ovh 49.1689 -0.358841
88 nostr-relay.gateway.in.th 15.2634 100.344
89 nostr.now 36.55 139.733
90 relay.usefusion.ai 38.7134 -78.1591
91 nostr.lkjsxc.com 43.6532 -79.3832
92 nproxy.kristapsk.lv 60.1699 24.9384
93 relay.nostrverse.net 43.6532 -79.3832
94 relay.zone667.com 60.1699 24.9384
95 relay.origin.land 35.6673 139.751
96 relay.damus.io 43.6532 -79.3832
97 relay.bnos.space 1.35208 103.82
98 relay.endfiat.money 43.6532 -79.3832
99 relay.bullishbounty.com 43.6532 -79.3832
100 wot.nostr.party 36.1627 -86.7816
101 nostr.huszonegy.world 47.4979 19.0402
102 theoutpost.life 64.1476 -21.9392
103 nostr.noones.com 50.1109 8.68213
104 relay.21e6.cz 50.7383 15.0648
105 nostr-relay.xbytez.io 50.6924 3.20113
106 wot.shaving.kiwi 43.6532 -79.3832
107 relay.trustroots.org 43.6532 -79.3832
108 wot.dtonon.com 43.6532 -79.3832
109 nostr.camalolo.com 24.1469 120.684
110 czas.xyz 48.8566 2.35222
111 relayrs.notoshi.win 43.6532 -79.3832
112 skeme.vanderwarker.family 40.8218 -74.45
113 nostr.red5d.dev 43.6532 -79.3832
114 relay.javi.space 43.4633 11.8796
115 relay.nuts.cash 34.0362 -118.443
116 nostr.commonshub.brussels 49.4543 11.0746
117 relay.artx.market 43.652 -79.3633
118 satsage.xyz 37.3986 -121.964
119 relay.nostx.io 43.6532 -79.3832
120 shu02.shugur.net 21.4902 39.2246
121 strfry.ymir.cloud 34.0965 -117.585
122 relay.nostr-check.me 43.6532 -79.3832
123 relay04.lnfi.network 39.0997 -94.5786
124 kotukonostr.onrender.com 37.7775 -122.397
125 nostr.davidebtc.me 51.5072 -0.127586
126 relay2.ngengine.org 43.6532 -79.3832
127 ribo.us.nostria.app 41.5868 -93.625
128 nostr.czas.top 50.1109 8.68213
129 relay.nostrzh.org 43.6532 -79.3832
130 relay.angor.io 48.1046 11.6002
131 alienos.libretechsystems.xyz 55.4724 9.87335
132 vault.iris.to 43.6532 -79.3832
133 relay.mccormick.cx 52.3563 4.95714
134 nostr.quali.chat 60.1699 24.9384
135 wheat.happytavern.co 43.6532 -79.3832
136 relay.olas.app 50.4754 12.3683
137 nostr-relayrs.gateway.in.th 15.2634 100.344
138 relay.snort.social 53.3498 -6.26031
139 nostr-2.21crypto.ch 47.5356 8.73209
140 pyramid.treegaze.com 43.6532 -79.3832
141 relay.smies.me 33.7501 -84.3885
142 wot.dergigi.com 64.1476 -21.9392
143 nostr-relay.zimage.com 34.0549 -118.243
144 nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
145 nostr.jerrynya.fun 31.2304 121.474
146 purplerelay.com 50.1109 8.68213
147 relay.comcomponent.com 34.7062 135.493
148 relay.unitypay.cash 41.4513 -81.7021
149 nostr.mikoshi.de 50.1109 8.68213
150 prl.plus 42.6978 23.3246
151 shu04.shugur.net 25.2604 55.2989
152 nostr-relay-1.trustlessenterprise.com 43.6532 -79.3832
153 nostr.zoracle.org 45.6018 -121.185
154 nostriches.club 43.6532 -79.3832
155 nostr.notribe.net 40.8302 -74.1299
156 nostr.vulpem.com 49.4543 11.0746
157 bucket.coracle.social 37.7775 -122.397
158 wot.yesnostr.net 50.9871 2.12554
159 wot.nostr.place 32.7767 -96.797
160 nostr.mehdibekhtaoui.com 49.4939 -1.54813
161 relay.getsafebox.app 43.6532 -79.3832
162 nostr2.girino.org 43.6532 -79.3832
163 nostr.blankfors.se 60.1699 24.9384
164 premium.primal.net 43.6532 -79.3832
165 nostr.rikmeijer.nl 50.4754 12.3683
166 relay-fra.zombi.cloudrodion.com 48.8566 2.35222
167 adre.su 59.9311 30.3609
168 nostr.n7ekb.net 47.4941 -122.294
169 nostr.hekster.org 37.3986 -121.964
170 relay.jabato.space 52.52 13.405
171 fenrir-s.notoshi.win 43.6532 -79.3832
172 notemine.io 52.2026 20.9397
173 r.bitcoinhold.net 43.6532 -79.3832
174 nostr-pub.wellorder.net 45.5201 -122.99
175 articles.layer3.news 37.3387 -121.885
176 relays.diggoo.com 43.6532 -79.3832
177 relayb.uid.ovh 43.6532 -79.3832
178 nostr.thebiglake.org 32.71 -96.6745
179 chat-relay.zap-work.com 43.6532 -79.3832
180 nr.yay.so 46.2126 6.1154
181 shu01.shugur.net 21.4902 39.2246
182 khatru.nostrver.se 51.1792 5.89444
183 relay.openfarmtools.org 60.1699 24.9384
184 relay01.lnfi.network 39.0997 -94.5786
185 nostrja-kari.heguro.com 43.6532 -79.3832
186 nostr.snowbla.de 60.1699 24.9384
187 relay.satlantis.io 32.8769 -80.0114
188 relay.vrtmrz.net 43.6532 -79.3832
189 purpura.cloud 43.6532 -79.3832
190 orangepiller.org 60.1699 24.9384
191 relay.coinos.io 43.6532 -79.3832
192 relay.layer.systems 49.0291 8.35695
193 relay.toastr.net 40.8054 -74.0241
194 ynostr.yael.at 60.1699 24.9384
195 ribo.eu.nostria.app 52.3676 4.90414
196 relay.nostrcheck.me 43.6532 -79.3832
197 relay.primal.net 43.6532 -79.3832
198 relay-testnet.k8s.layer3.news 37.3387 -121.885
199 relay.threenine.services 51.5524 -0.29686
200 relay.nostr.net 43.6532 -79.3832
201 nostr.middling.mydns.jp 35.8099 140.12
202 soloco.nl 43.6532 -79.3832
203 nostr.4rs.nl 49.0291 8.35696
204 no.str.cr 9.92857 -84.0528
205 espelho.girino.org 43.6532 -79.3832
206 nostr.hifish.org 47.4043 8.57398
207 wot.sebastix.social 51.1792 5.89444
208 relay03.lnfi.network 39.0997 -94.5786
209 relay.nosto.re 51.1792 5.89444
210 relay.nostrdice.com -33.8688 151.209
211 nostr.agentcampfire.com 52.3676 4.90414
212 nostr.night7.space 50.4754 12.3683
213 nostr.nodesmap.com 59.3327 18.0656
214 relayone.soundhsa.com 33.1384 -95.6011
215 relay.illuminodes.com 47.6061 -122.333
216 nostr-rs-relay-ishosta.phamthanh.me 43.6532 -79.3832
217 nostr.robosats.org 64.1476 -21.9392
218 relay.seq1.net 43.6532 -79.3832
219 strfry.shock.network 39.0438 -77.4874
220 nostr.ovia.to 43.6532 -79.3832
221 relay.holzeis.me 43.6532 -79.3832
222 bcast.seutoba.com.br 43.6532 -79.3832
223 relay.libernet.app 43.6532 -79.3832
224 nostr-01.uid.ovh 43.6532 -79.3832
225 nostr.luisschwab.net 43.6532 -79.3832
226 strfry.elswa-dev.online 50.1109 8.68213
227 relay.nostriot.com 41.5695 -83.9786
228 nostr.oxtr.dev 50.4754 12.3683
229 nostr.mom 50.4754 12.3683
230 nostr-03.dorafactory.org 1.35208 103.82
231 relay.camelus.app 45.5201 -122.99
232 strfry.openhoofd.nl 51.9229 4.40833
233 nos.lol 50.4754 12.3683
234 nostr.bitcoiner.social 39.1585 -94.5728
235 relay.islandbitcoin.com 12.8498 77.6545
236 relay.tagayasu.xyz 43.6715 -79.38
237 dev-nostr.bityacht.io 25.0797 121.234
238 relay.nostrhub.fr 48.1045 11.6004
239 relay.satmaxt.xyz 43.6532 -79.3832
240 yabu.me 35.6092 139.73
241 relay.samt.st 40.8302 -74.1299
242 relay02.lnfi.network 39.0997 -94.5786
243 nostr.88mph.life 51.5072 -0.127586
244 dev-relay.lnfi.network 39.0997 -94.5786
245 relay.goodmorningbitcoin.com 43.6532 -79.3832
246 nostrelay.memory-art.xyz 43.6532 -79.3832
247 relay.degmods.com 50.4754 12.3683
248 nostr-02.yakihonne.com 1.32123 103.695
249 nostr.spicyz.io 43.6532 -79.3832
250 relay.minibolt.info 43.6532 -79.3832
251 bcast.girino.org 43.6532 -79.3832
252 nostr.bilthon.dev 25.8128 -80.2377
253 relay.bitcoinartclock.com 50.4754 12.3683
254 cyberspace.nostr1.com 40.7128 -74.006
255 nostr.data.haus 50.4754 12.3683
256 relay.agora.social 50.7383 15.0648
257 relay.wavefunc.live 34.0362 -118.443
258 nostr.casa21.space 43.6532 -79.3832
259 relay.mostro.network 40.8302 -74.1299
260 relay.ditto.pub 43.6532 -79.3832
261 nostr-relay.amethyst.name 39.0438 -77.4874
262 relay.credenso.cafe 43.3601 -80.3127
263 nostr.calitabby.net 39.9268 -75.0246
264 relay.magiccity.live 25.8128 -80.2377
265 nostr.chaima.info 51.223 6.78245
266 wot.brightbolt.net 47.6735 -116.781
267 nostr.sathoarder.com 48.5734 7.75211
268 inbox.azzamo.net 52.2633 21.0283
269 nostr.na.social 43.6532 -79.3832
270 nostr.myshosholoza.co.za 52.3676 4.90414
271 nostr.spaceshell.xyz 43.6532 -79.3832
272 relay.wellorder.net 45.5201 -122.99
273 nostr.0x7e.xyz 47.4988 8.72369
274 nostr-relay.cbrx.io 43.6532 -79.3832
275 relay.btcforplebs.com 43.6532 -79.3832
276 bitcoiner.social 39.1585 -94.5728
277 relaynostr.breadslice.com 43.6532 -79.3832
278 nostr.overmind.lol 43.6532 -79.3832
279 santo.iguanatech.net 40.8302 -74.1299
280 relay.lightning.pub 39.0438 -77.4874
281 nostr.rblb.it 43.7094 10.6582
282 relay.bitcoinveneto.org 64.1466 -21.9426
283 relay.wolfcoil.com 35.6092 139.73
284 nostr-relay.corb.net 38.8353 -104.822
285 relay.nostr.place 32.7767 -96.797
286 wot.soundhsa.com 33.1384 -95.6011
287 relay.npubhaus.com 43.6532 -79.3832
288 nostr-relay.psfoundation.info 39.0438 -77.4874
289 x.kojira.io 43.6532 -79.3832
290 relay.cypherflow.ai 48.8566 2.35222

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,9 @@ enum class OnboardingState {
LOCATION_CHECK,
BATTERY_OPTIMIZATION_CHECK,
PERMISSION_EXPLANATION,
BACKGROUND_LOCATION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
ERROR
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Aus Favoriten entfernen</string>
<string name="cd_add_bookmark">Lesezeichen hinzufügen</string>
<!-- ChatKopf & Barrierefreiheit -->
<!-- ChatKopf &amp; 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">OnlineGeohashKanäle</string>
<string name="cd_end_to_end_encryption">EndezuEndeVerschlüsselung</string>
<!-- Bilder & Dateien -->
<!-- Bilder &amp; 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 &amp; 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>

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Retirer des favoris</string>
<string name="cd_add_bookmark">Ajouter un signet</string>
<!-- Entête de chat & accessibilité -->
<!-- Entête de chat &amp; 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 &amp; 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é &amp; 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>

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">पसंदीदा से हटाएँ</string>
<string name="cd_add_bookmark">बुकमार्क जोड़ें</string>
<!-- चैट हेडर & एक्सेसिबिलिटी -->
<!-- चैट हेडर &amp; एक्सेसिबिलिटी -->
<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 &amp; 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>

View File

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

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">お気に入りから削除</string>
<string name="cd_add_bookmark">ブックマークに追加</string>
<!-- チャットヘッダー & アクセシビリティ -->
<!-- チャットヘッダー &amp; アクセシビリティ -->
<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>
<!-- 画像 & ファイル -->
<!-- 画像 &amp; ファイル -->
<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>

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">რჩეულებიდან წაშლა</string>
<string name="cd_add_bookmark">სანიშნეს დამატება</string>
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
<!-- ჩათის სათაური &amp; ხელმისაწვდომობა -->
<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>
<!-- სურათები & ფაილები -->
<!-- სურათები &amp; ფაილები -->
<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 &amp; 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>

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">즐겨찾기에서 제거</string>
<string name="cd_add_bookmark">북마크 추가</string>
<!-- 채팅 헤더 & 접근성 -->
<!-- 채팅 헤더 &amp; 접근성 -->
<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>
<!-- 이미지 & 파일 -->
<!-- 이미지 &amp; 파일 -->
<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 &amp; 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>

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Verwijderen uit favorieten</string>
<string name="cd_add_bookmark">Bladwijzer toevoegen</string>
<!-- Chatkop & toegankelijkheid -->
<!-- Chatkop &amp; 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 &amp; 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 &amp; 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 &amp; 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>

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">پسندیدہ توں ہٹاؤ</string>
<string name="cd_add_bookmark">بُک مارک پاؤ</string>
<!-- چیٹ ہیڈر & رسائی پذیری -->
<!-- چیٹ ہیڈر &amp; رسائی پذیری -->
<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>
<!-- تصویراں & فائلز -->
<!-- تصویراں &amp; فائلز -->
<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 &amp; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@
<string name="cd_remove_favorite">เอาออกจากรายการโปรด</string>
<string name="cd_add_bookmark">เพิ่มที่คั่นหน้า</string>
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
<!-- ส่วนหัวแชท &amp; การช่วยสำหรับการเข้าถึง -->
<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>
<!-- รูปภาพ & ไฟล์ -->
<!-- รูปภาพ &amp; ไฟล์ -->
<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 &amp; 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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