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