diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 697b91a5..7ee54989 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,7 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
alias(libs.plugins.android.application)
- alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.compose)
}
@@ -65,11 +66,8 @@ android {
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
@@ -86,6 +84,12 @@ android {
}
}
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+}
+
dependencies {
// Core Android dependencies
implementation(libs.androidx.core.ktx)
@@ -143,7 +147,7 @@ dependencies {
implementation(libs.androidx.security.crypto)
// EXIF orientation handling for images
- implementation("androidx.exifinterface:exifinterface:1.3.7")
+ implementation(libs.androidx.exifinterface)
// Testing
testImplementation(libs.bundles.testing)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f83ecefb..8c732bbc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,8 @@
+
+
diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt
index e7e02203..23235068 100644
--- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt
+++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt
@@ -39,6 +39,26 @@ class PermissionManager(private val context: Context) {
}
}
+ /**
+ * Runtime permissions for the Wi‑Fi Aware transport, version-gated because neither exists
+ * at minSdk 26 — requesting an unknown permission comes back permanently denied.
+ *
+ * ACCESS_LOCAL_NETWORK is defensive: Android 17 gates local network access, and the
+ * transport reaches peers over link-local IPv6 sockets. It is granted separately from
+ * NEARBY_WIFI_DEVICES but shares its permission group, so the two prompt only once.
+ */
+ fun wifiAwarePermissions(): List {
+ val permissions = mutableListOf()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES)
+ }
+ // API 37 == Android 17; no named VERSION_CODES constant is available yet.
+ if (Build.VERSION.SDK_INT >= 37) {
+ permissions.add(Manifest.permission.ACCESS_LOCAL_NETWORK)
+ }
+ return permissions
+ }
+
/**
* Check if this is the first time the user is launching the app
*/
@@ -87,7 +107,7 @@ class PermissionManager(private val context: Context) {
// Wi‑Fi Aware: Android 13+ requires NEARBY_WIFI_DEVICES runtime permission
if (shouldRequireWifiAwarePermission()) {
- permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES)
+ permissions.addAll(wifiAwarePermissions())
}
// Notification permission intentionally excluded to keep it optional
@@ -232,7 +252,7 @@ class PermissionManager(private val context: Context) {
// Wi‑Fi Aware category (Android 13+)
if (shouldRequireWifiAwarePermission()) {
- val wifiAwarePermissions = listOf(Manifest.permission.NEARBY_WIFI_DEVICES)
+ val wifiAwarePermissions = wifiAwarePermissions()
categories.add(
PermissionCategory(
type = PermissionType.WIFI_AWARE,
diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
index 65b19182..be950316 100644
--- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
@@ -37,6 +37,11 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.res.stringResource
import com.bitchat.android.R
import androidx.compose.ui.platform.LocalContext
+import android.content.pm.PackageManager
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import com.bitchat.android.onboarding.PermissionManager
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
@@ -128,6 +133,37 @@ fun DebugSettingsSheet(
val bleEnabled by manager.bleEnabled.collectAsState()
val wifiAwareEnabled by manager.wifiAwareEnabled.collectAsState()
val wifiAwareVerbose by manager.wifiAwareVerbose.collectAsState()
+
+ // Onboarding only asks for these when the toggle is already on, and it defaults to off,
+ // so enabling from here has to request them or the controller never starts.
+ val wifiAwarePermissions = remember { PermissionManager(context).wifiAwarePermissions() }
+ val wifiAwarePermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { _ ->
+ // Check live state, not the result map — already-held permissions are filtered out
+ // before launching. Only NEARBY_WIFI_DEVICES blocks startup; the other is defensive.
+ val nearbyPermission = android.Manifest.permission.NEARBY_WIFI_DEVICES
+ val nearbyGranted = nearbyPermission !in wifiAwarePermissions ||
+ ContextCompat.checkSelfPermission(context, nearbyPermission) ==
+ PackageManager.PERMISSION_GRANTED
+ if (nearbyGranted) {
+ manager.setWifiAwareEnabled(true)
+ } else {
+ manager.addDebugMessage(
+ DebugMessage.SystemMessage("Wi‑Fi Aware needs the Nearby devices permission")
+ )
+ }
+ }
+ val enableWifiAware: () -> Unit = {
+ val missing = wifiAwarePermissions.filter {
+ ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
+ }
+ if (missing.isEmpty()) {
+ manager.setWifiAwareEnabled(true)
+ } else {
+ wifiAwarePermissionLauncher.launch(missing.toTypedArray())
+ }
+ }
val wifiAwareDiscovered by manager.wifiAwareDiscovered.collectAsState()
val wifiAwareConnected by manager.wifiAwareConnected.collectAsState()
val wifiAwareSupported by com.bitchat.android.wifiaware.WifiAwareController.supported.collectAsState()
@@ -345,7 +381,9 @@ fun DebugSettingsSheet(
Switch(
checked = wifiAwareEnabled && wifiAwareSupported,
enabled = wifiSwitchEnabled,
- onCheckedChange = { manager.setWifiAwareEnabled(it) }
+ onCheckedChange = { on ->
+ if (on) enableWifiAware() else manager.setWifiAwareEnabled(false)
+ }
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -643,7 +681,7 @@ fun DebugSettingsSheet(
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
AssistChip(
- onClick = { manager.setWifiAwareEnabled(true) },
+ onClick = enableWifiAware,
enabled = wifiAwareSupported,
label = { Text("Start") }
)
diff --git a/build.gradle.kts b/build.gradle.kts
index 44082684..44c5199e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
- alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.compose) apply false
}
diff --git a/gradle.properties b/gradle.properties
index 4c4ae04f..c5ca74c1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,9 +9,6 @@
# Specifies the JVM arguments used for the daemon process.
android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
-
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b6a5411a..4a8ad015 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,72 +1,72 @@
[versions]
# Android and Kotlin
-agp = "8.10.1"
-kotlin = "2.2.0"
-compileSdk = "35"
+agp = "9.3.1"
+kotlin = "2.4.10"
+compileSdk = "37"
minSdk = "26" # API 26 for proper BLE support
-targetSdk = "35"
+targetSdk = "37"
# AndroidX Core
-core-ktx = "1.16.0"
-lifecycle-runtime = "2.9.1"
-activity-compose = "1.10.1"
+core-ktx = "1.19.0"
+lifecycle-runtime = "2.11.0"
+activity-compose = "1.13.0"
appcompat = "1.7.1"
# Compose
-compose-bom = "2025.06.01"
+compose-bom = "2026.06.01"
# Navigation
-navigation-compose = "2.9.1"
+navigation-compose = "2.9.8"
# Accompanist
accompanist-permissions = "0.37.3"
# Cryptography
-bouncycastle = "1.70"
-tink-android = "1.10.0"
+bouncycastle = "1.85"
+tink-android = "1.23.0"
# JSON
-gson = "2.13.1"
+gson = "2.14.0"
# Coroutines
-kotlinx-coroutines = "1.10.2"
+kotlinx-coroutines = "1.11.0"
# Bluetooth
-nordic-ble = "2.6.1"
+nordic-ble = "2.11.0"
# WebSocket
-okhttp = "4.12.0"
+okhttp = "5.4.0"
tor-android-binary = "0.4.4.6"
# Google Play Services
-gms-location = "21.3.0"
+gms-location = "21.4.0"
# Security
-security-crypto = "1.1.0-beta01"
+security-crypto = "1.1.0"
# QR
zxing-core = "3.5.4"
+# EXIF
+exifinterface = "1.4.2"
+
# CameraX / ML Kit
-camerax = "1.5.2"
+camerax = "1.6.1"
mlkit-barcode = "17.3.0"
# Testing
junit = "4.13.2"
-androidx-test-ext = "1.2.1"
-espresso = "3.6.1"
-mockito-kotlin = "4.1.0"
-mockito-inline = "4.1.0"
-roboelectric = "4.15"
-kotlinx-coroutines-test = "1.6"
-
-lifecycle-process = "2.8.7"
+androidx-test-ext = "1.3.0"
+espresso = "3.7.0"
+mockito-kotlin = "6.3.0"
+mockito-core = "5.23.0"
+roboelectric = "4.15" # 4.16+ drops the AndroidKeyStore shim; breaks EncryptionServiceTest
[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-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-runtime" }
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" }
@@ -90,7 +90,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
# Cryptography
-bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" }
+bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
google-tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink-android" }
# JSON
@@ -117,6 +117,9 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi
# QR
zxing-core = { module = "com.google.zxing:core", version.ref = "zxing-core" }
+# EXIF
+androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
+
# 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" }
@@ -130,14 +133,13 @@ androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core",
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" }
-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" }
+mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-core" }
roboelectric = { module = "org.robolectric:robolectric", version.ref = "roboelectric"}
-kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "kotlin-parcelize" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
@@ -165,7 +167,7 @@ testing = [
"androidx-test-ext-junit",
"androidx-test-espresso-core",
"mockito-kotlin",
- "mockito-inline",
+ "mockito-core",
"roboelectric",
"kotlinx-coroutines-test"
]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f853b1..a351597e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME