diff --git a/.editorconfig b/.editorconfig
index 6dd9661f..8406c0aa 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,2 +1,2 @@
-[*.{kt, kts}]
+[*.{kt,kts}]
max_line_length = 118
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..8ac6b8c4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56e..b86273d9 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 01cc83f4..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,207 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'kotlin-android'
- id 'kotlin-kapt'
- id 'dagger.hilt.android.plugin'
- id 'org.jetbrains.kotlin.plugin.compose'
- id 'org.jlleitschuh.gradle.ktlint' version '10.2.1'
-}
-
-static def versionCodeEpoch() {
- return (new Date().getTime() / 1000).toInteger()
-}
-
-def gitCommit = { ->
- def stdout = new ByteArrayOutputStream()
- exec {
- commandLine 'git', 'rev-parse', '--short=7', 'HEAD'
- standardOutput = stdout
- }
- return stdout.toString().trim()
-}
-
-android {
- compileSdk 36
-
- defaultConfig {
- namespace "org.onionshare.android"
-
- applicationId "org.onionshare.android"
- minSdk 21
- targetSdk 36
- versionCode 26
- versionName "0.2.3-beta"
-
- vectorDrawables {
- useSupportLibrary true
- }
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- testInstrumentationRunnerArguments disableAnalytics: 'true'
- }
-
- buildTypes {
- release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- debug {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- flavorDimensions "releaseType"
- productFlavors {
- stable {
- dimension "releaseType"
- }
- fdroid {
- dimension "releaseType"
- applicationIdSuffix ".fdroid"
- // version codes get multiplied by 10 and an ABI suffix gets added to the code
- // if 'splitApk' property is set
- }
- nightly {
- dimension "releaseType"
- applicationIdSuffix ".nightly"
- versionCode versionCodeEpoch()
- versionNameSuffix " ($gitCommit)"
- }
- }
- splits {
- abi {
- // can not be defined per flavor, so we use a property to turn this on for F-Droid
- enable project.hasProperty('splitApk')
- reset() // Resets the list of ABIs to remove all included by default
- include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
- universalApk false
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- coreLibraryDesugaringEnabled true
- }
- kotlinOptions {
- jvmTarget = '17'
- freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
- }
- buildFeatures {
- compose true
- buildConfig true
- }
- packagingOptions {
- jniLibs {
- useLegacyPackaging = true
- }
- exclude 'META-INF/*'
- // Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
- exclude 'META-INF/licenses/*'
- exclude '**/attach_hotspot_windows.dll'
- }
- lintOptions {
- disable 'MissingTranslation', 'ExtraTranslation', 'MissingQuantity'
- }
-}
-
-configurations {
- tor
-}
-
-dependencies {
- implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
-
- implementation 'androidx.core:core-ktx:1.16.0'
- implementation 'androidx.appcompat:appcompat:1.7.1'
- implementation 'androidx.activity:activity-compose:1.10.1'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.9.1'
- implementation 'com.google.android.material:material:1.12.0'
-
- implementation platform("androidx.compose:compose-bom:$compose_version")
- implementation 'androidx.compose.material3:material3'
- implementation 'androidx.compose.material:material-icons-extended'
- implementation 'androidx.compose.ui:ui-tooling-preview'
-
- implementation "androidx.navigation:navigation-compose:2.9.0"
- implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
- implementation "androidx.profileinstaller:profileinstaller:1.4.1"
- implementation "androidx.security:security-crypto:1.1.0-beta01"
-
- implementation "com.google.dagger:hilt-android:$hilt_version"
- kapt "com.google.dagger:hilt-compiler:$hilt_version"
-
- // Tor
- implementation "org.briarproject:onionwrapper-android:0.1.4"
- tor 'org.briarproject:tor-android:0.4.8.19'
- tor 'org.briarproject:lyrebird-android:0.6.2'
- implementation 'org.briarproject:moat-api:0.3'
- implementation 'com.squareup.okhttp3:okhttp:4.12.0'
-
- def ktor_version = '2.3.13'
- implementation "io.ktor:ktor-server-core:$ktor_version"
- implementation "io.ktor:ktor-server-netty:$ktor_version"
- implementation "io.ktor:ktor-server-pebble:$ktor_version"
- implementation "io.ktor:ktor-server-status-pages:$ktor_version"
- implementation "io.ktor:ktor-server-call-logging:$ktor_version"
- implementation 'org.slf4j:slf4j-api:2.0.16'
- implementation 'org.slf4j:jul-to-slf4j:2.0.5'
- implementation 'com.github.tony19:logback-android:3.0.0'
-
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
-
- debugImplementation 'androidx.compose.ui:ui-tooling'
- debugImplementation 'androidx.compose.ui:ui-test-manifest'
-
- testImplementation 'junit:junit:4.13.2'
- testImplementation 'io.mockk:mockk:1.14.2'
- testImplementation 'org.slf4j:slf4j-jdk14:2.0.7'
- testImplementation 'app.cash.turbine:turbine:1.0.0'
- testImplementation 'org.jetbrains.kotlin:kotlin-test'
- testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
-
- androidTestImplementation 'androidx.test.ext:junit:1.2.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
- androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.8.2"
- androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
-}
-
-// Map for the version code that gives each ABI a value.
-ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
-// For each APK output variant, override versionCode with a combination of ext.abiCodes + variant.versionCode.
-android.applicationVariants.configureEach { variant ->
- // Assigns a different version code for each output APK
- variant.outputs.each { output ->
- // Stores the value of ext.abiCodes that is associated with the ABI for this variant.
- def baseAbiVersionCode =
- // Determines the ABI for this variant and returns the mapped value.
- project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
- if (baseAbiVersionCode != null) {
- output.versionCodeOverride = 10 * variant.versionCode + baseAbiVersionCode
- }
- // leaves version code alone of there's no baseAbiVersionCode
- }
-}
-
-def torLibsDir = 'src/main/jniLibs'
-
-tasks.register('cleanTorBinaries') {
- outputs.dir torLibsDir
- doLast {
- delete fileTree(torLibsDir)
- }
-}
-
-clean.dependsOn cleanTorBinaries
-
-tasks.register('unpackTorBinaries') {
- outputs.dir torLibsDir
- doLast {
- copy {
- from configurations.tor.collect { zipTree(it) }
- into torLibsDir
- }
- }
- dependsOn cleanTorBinaries
-}
-preBuild.dependsOn unpackTorBinaries
-
-apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..320d6fa6
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,225 @@
+import com.android.build.api.variant.FilterConfiguration
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import java.util.Date
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ktlint)
+}
+
+val tor by configurations.creating
+
+fun versionCodeEpoch(): Int = (Date().time / 1000).toInt()
+
+fun gitCommit(): String {
+ val process = ProcessBuilder("git", "rev-parse", "--short=8", "HEAD")
+ .directory(rootDir)
+ .redirectErrorStream(true)
+ .start()
+ process.waitFor()
+ return process.inputStream.use { it.readBytes().decodeToString().trim() }
+}
+
+android {
+ namespace = "org.onionshare.android"
+ compileSdk = 37
+
+ defaultConfig {
+ applicationId = "org.onionshare.android"
+ minSdk = 26
+ targetSdk = 37
+ versionCode = 26
+ versionName = "0.3.0-beta"
+
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["disableAnalytics"] = "true"
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ getByName("debug") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ flavorDimensions += "releaseType"
+
+ productFlavors {
+ create("stable") {
+ dimension = "releaseType"
+ }
+ create("fdroid") {
+ dimension = "releaseType"
+ applicationIdSuffix = ".fdroid"
+ // version codes get multiplied by 10 and an ABI suffix gets added to the code
+ // if 'splitApk' property is set
+ }
+ create("nightly") {
+ dimension = "releaseType"
+ applicationIdSuffix = ".nightly"
+ versionCode = versionCodeEpoch()
+ versionNameSuffix = " (${gitCommit()})"
+ }
+ }
+
+ splits {
+ abi {
+ // can not be defined per flavor, so we use a property to turn this on for F-Droid
+ isEnable = project.hasProperty("splitApk")
+ reset()
+ include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
+ isUniversalApk = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ packaging {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ resources {
+ excludes += setOf(
+ "META-INF/*",
+ // Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
+ "META-INF/licenses/*",
+ "**/attach_hotspot_windows.dll",
+ )
+ }
+ }
+
+ lint {
+ disable += setOf("MissingTranslation", "ExtraTranslation", "MissingQuantity")
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
+ }
+}
+
+dependencies {
+ implementation(libs.kotlin.reflect)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.documentfile)
+ implementation(libs.material)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.material3)
+ implementation(libs.compose.material.icons.extended)
+ implementation(libs.compose.ui.tooling.preview)
+
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.androidx.profileinstaller)
+ implementation(libs.androidx.security.crypto)
+
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ // Tor
+ implementation(libs.briar.onionwrapper.android)
+ add(tor.name, libs.briar.tor.android)
+ add(tor.name, libs.briar.lyrebird.android)
+ implementation(libs.briar.moat.api)
+ implementation(libs.okhttp)
+
+ implementation(libs.ktor.server.core)
+ implementation(libs.ktor.server.netty)
+ implementation(libs.ktor.server.pebble)
+ implementation(libs.ktor.server.status.pages)
+ implementation(libs.ktor.server.call.logging)
+ implementation(libs.slf4j.api)
+ implementation(libs.slf4j.jul.to.slf4j)
+ implementation(libs.logback.android)
+
+ coreLibraryDesugaring(libs.desugar.jdk.libs)
+
+ debugImplementation(libs.compose.ui.tooling)
+ debugImplementation(libs.compose.ui.test.manifest)
+
+ testImplementation(libs.junit4)
+ testImplementation(libs.mockk)
+ testImplementation(libs.slf4j.jdk14)
+ testImplementation(libs.turbine)
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.kotlinx.coroutines.test)
+
+ androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.androidx.test.espresso.core)
+ androidTestImplementation(libs.compose.ui.test.junit4)
+ androidTestImplementation(libs.screengrab)
+}
+
+val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86" to 3, "x86_64" to 4)
+
+androidComponents {
+ onVariants(selector().all()) { variant ->
+ variant.outputs.forEach { output ->
+ val abi = output.filters
+ .find { it.filterType == FilterConfiguration.FilterType.ABI }
+ ?.identifier
+ val baseAbiVersionCode = abiCodes[abi]
+ if (baseAbiVersionCode != null) {
+ output.versionCode.set(output.versionCode.map { 10 * it + baseAbiVersionCode })
+ }
+ }
+ }
+}
+
+val torLibsDir = "src/main/jniLibs"
+
+val cleanTorBinaries = tasks.register("cleanTorBinaries") {
+ outputs.dir(torLibsDir)
+ doLast {
+ project.delete(project.fileTree(torLibsDir))
+ }
+}
+
+tasks.named("clean") {
+ dependsOn(cleanTorBinaries)
+}
+
+val unpackTorBinaries = tasks.register("unpackTorBinaries") {
+ outputs.dir(torLibsDir)
+ dependsOn(cleanTorBinaries)
+ doLast {
+ project.copy {
+ from(tor.resolve().map { project.zipTree(it) })
+ into(torLibsDir)
+ }
+ }
+}
+
+tasks.named("preBuild") {
+ dependsOn(unpackTorBinaries)
+}
+
+apply(from = "${rootProject.rootDir}/gradle/ktlint.gradle")
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 5265e0da..563df593 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -20,6 +20,7 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+-dontoptimize
-dontobfuscate
-keepattributes SourceFile, LineNumberTable, *Annotation*, Signature, InnerClasses, EnclosingMethod
@@ -32,7 +33,8 @@
# Keep Netty classes that are loaded via reflection
-keep class io.netty.channel.** { *; }
--keep class io.netty.buffer.WrappedByteBuf { *; }
+-keep class io.netty.buffer.** { *; }
+-keep class io.netty.util.concurrent.** { *; }
-keep class io.netty.util.ReferenceCountUtil { *; }
-keep class io.netty.handler.codec.MessageToMessageEncoder { *; }
@@ -46,3 +48,14 @@
-dontwarn org.jetbrains.annotations.*
-dontwarn reactor.blockhound.integration.BlockHoundIntegration
-dontwarn javax.mail.**
+-dontwarn javax.naming.ldap.LdapName
+-dontwarn javax.naming.ldap.Rdn
+-dontwarn jdk.jfr.Category
+-dontwarn jdk.jfr.DataAmount
+-dontwarn jdk.jfr.Description
+-dontwarn jdk.jfr.Enabled
+-dontwarn jdk.jfr.Event
+-dontwarn jdk.jfr.FlightRecorder
+-dontwarn jdk.jfr.Label
+-dontwarn jdk.jfr.MemoryAddress
+-dontwarn jdk.jfr.Name
diff --git a/app/src/main/java/org/onionshare/android/files/FileManager.kt b/app/src/main/java/org/onionshare/android/files/FileManager.kt
index 2f23ede9..85eeecd8 100644
--- a/app/src/main/java/org/onionshare/android/files/FileManager.kt
+++ b/app/src/main/java/org/onionshare/android/files/FileManager.kt
@@ -69,21 +69,37 @@ class FileManager @Inject constructor(
private fun addFiles(uris: List) {
val existingFiles = filesState.value.files
+ val existingNames = existingFiles.map { it.basename }.toMutableSet()
val files = uris.mapNotNull { uri ->
// continue if we already have that file
if (existingFiles.any { it.uri == uri }) return@mapNotNull null
val documentFile = DocumentFile.fromSingleUri(ctx, uri) ?: error("Only API < 19")
- val name = documentFile.name ?: uri.getFallBackName() ?: error("Uri has no path $uri")
+ val preliminaryName = documentFile.name ?: uri.getFallBackName() ?: error("Uri has no path $uri")
val size = documentFile.length()
val sizeHuman = if (size == 0L) ctx.getString(R.string.unknown) else {
formatShortFileSize(ctx, size)
}
- SendFile(name, sizeHuman, size, uri, documentFile.type)
+ val name = getName(existingNames, preliminaryName)
+ existingNames.add(name)
+ SendFile(
+ basename = name,
+ size_human = sizeHuman,
+ size = size,
+ uri = uri,
+ mimeType = documentFile.type,
+ )
}
_filesState.value = FilesState(existingFiles + files)
}
+ private fun getName(existingNames: Set, name: String): String {
+ return if (name in existingNames) getName(
+ existingNames = existingNames,
+ name = "${name.substringBeforeLast(".")}-1.${name.substringAfterLast(".")}"
+ ) else name
+ }
+
fun removeFile(file: SendFile) {
// release persistable Uri permission again
file.releaseUriPermission()
@@ -140,7 +156,7 @@ class FileManager @Inject constructor(
LOG.warn("Error while opening file: ", e)
throw FileErrorException(file)
} catch (e: SecurityException) {
- LOG.warn("Error while opening file: SecurityException")
+ LOG.warn("Error while opening file: ", e)
throw FileErrorException(file)
}
}
diff --git a/app/src/main/java/org/onionshare/android/server/WebserverManager.kt b/app/src/main/java/org/onionshare/android/server/WebserverManager.kt
index fa8b50c1..45cf8282 100644
--- a/app/src/main/java/org/onionshare/android/server/WebserverManager.kt
+++ b/app/src/main/java/org/onionshare/android/server/WebserverManager.kt
@@ -12,15 +12,15 @@ import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationStarted
import io.ktor.server.application.ApplicationStopped
import io.ktor.server.application.ApplicationStopping
-import io.ktor.server.application.call
import io.ktor.server.application.install
-import io.ktor.server.engine.ApplicationEngine
+import io.ktor.server.engine.EmbeddedServer
+import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticResources
import io.ktor.server.netty.Netty
import io.ktor.server.pebble.Pebble
import io.ktor.server.pebble.PebbleContent
-import io.ktor.server.plugins.callloging.CallLogging
+import io.ktor.server.plugins.calllogging.CallLogging
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.response.header
import io.ktor.server.response.respond
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.onionshare.android.BuildConfig.DEBUG
import org.slf4j.LoggerFactory
+import java.net.ServerSocket
import java.security.SecureRandom
import java.util.concurrent.RejectedExecutionException
import javax.inject.Inject
@@ -51,28 +52,32 @@ sealed class WebServerState {
class WebserverManager @Inject constructor() {
private val secureRandom = SecureRandom()
- private var server: ApplicationEngine? = null
+ private var server: EmbeddedServer<*, *>? = null
private val _state = MutableStateFlow(WebServerState.Stopped(false))
val state = _state.asStateFlow()
+
@Volatile
var contentPath = ""
private set
- suspend fun start(sendPage: SendPage): Int {
+ fun start(sendPage: SendPage): Int {
_state.value = WebServerState.Starting
contentPath = getRandomPath()
val staticPath = getStaticPath()
val pathMap = mapOf("static_url_path" to staticPath, "content_path" to contentPath)
+ val port = findFreePort()
TrafficStats.setThreadStatsTag(0x42)
val server = embeddedServer(
factory = Netty,
- host = "127.0.0.1",
- port = 0, // will be chosen randomly
- watchPaths = emptyList(),
configure = {
+ connector {
+ host = "127.0.0.1"
+ this.port = port
+ }
// disable response timeout
responseWriteTimeoutSeconds = 0
- }) {
+ }
+ ) {
if (DEBUG) install(CallLogging)
install(Pebble) {
loader(ClasspathLoader().apply { prefix = "assets/templates" })
@@ -85,7 +90,7 @@ class WebserverManager @Inject constructor() {
}
}.also { it.start() }
this.server = server
- return server.resolvedConnectors().first().port
+ return port
}
fun stop(isFinishingDownloading: Boolean = false) {
@@ -103,6 +108,8 @@ class WebserverManager @Inject constructor() {
}
}
+ private fun findFreePort(): Int = ServerSocket(0).use { it.localPort }
+
private fun getRandomPath(): String {
val randomBytes = ByteArray(16).apply { secureRandom.nextBytes(this) }
return Base64.encodeToString(randomBytes, NO_PADDING or URL_SAFE).trimEnd()
@@ -113,14 +120,14 @@ class WebserverManager @Inject constructor() {
}
private fun Application.addListener() {
- environment.monitor.subscribe(ApplicationStarted) {
+ monitor.subscribe(ApplicationStarted) {
_state.value = WebServerState.Started
}
- environment.monitor.subscribe(ApplicationStopping) {
+ monitor.subscribe(ApplicationStopping) {
// only update if we are not already stopping
if (state.value !is WebServerState.Stopping) _state.value = WebServerState.Stopping()
}
- environment.monitor.subscribe(ApplicationStopped) {
+ monitor.subscribe(ApplicationStopped) {
LOG.info("Stopped")
val downloadComplete = (state.value as? WebServerState.Stopping)?.downloadComplete ?: false
_state.value = WebServerState.Stopped(downloadComplete)
diff --git a/app/src/main/java/org/onionshare/android/ui/share/ShareBottomSheet.kt b/app/src/main/java/org/onionshare/android/ui/share/ShareBottomSheet.kt
index 73b86db1..cbe56b7e 100644
--- a/app/src/main/java/org/onionshare/android/ui/share/ShareBottomSheet.kt
+++ b/app/src/main/java/org/onionshare/android/ui/share/ShareBottomSheet.kt
@@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
+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.filled.CheckCircle
import androidx.compose.material.icons.filled.Circle
@@ -109,7 +111,9 @@ fun BottomSheet(state: ShareUiState, onSheetButtonClicked: () -> Unit) {
val bottomPadding = with(LocalDensity.current) {
WindowInsets.safeDrawing.getBottom(this).toDp()
}
- Column(modifier = Modifier.padding(bottom = bottomPadding)) {
+ Column(modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(bottom = bottomPadding)) {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier.padding(16.dp),
@@ -266,6 +270,21 @@ fun ShareBottomSheetSharingPreview() {
}
}
+@Preview(showBackground = true, fontScale = 3.0f)
+@Composable
+fun ShareBottomSheetSharingLargeFontPreview() {
+ OnionshareTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ BottomSheet(
+ state = ShareUiState.Sharing(
+ "http://openpravyvc6spbd4flzn4g2iqu4sxzsizbtb5aqec25t76dnoo5w7yd.onion/eW91IGFyZSBhIG5lcmQ7KQ",
+ ),
+ onSheetButtonClicked = {},
+ )
+ }
+ }
+}
+
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun ShareBottomSheetSharingPreviewNight() {
diff --git a/app/src/main/java/org/onionshare/android/ui/share/ShareUi.kt b/app/src/main/java/org/onionshare/android/ui/share/ShareUi.kt
index 179a468b..f6047712 100644
--- a/app/src/main/java/org/onionshare/android/ui/share/ShareUi.kt
+++ b/app/src/main/java/org/onionshare/android/ui/share/ShareUi.kt
@@ -1,7 +1,5 @@
package org.onionshare.android.ui.share
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -51,9 +49,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
+import androidx.core.net.toUri
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
@@ -271,11 +271,11 @@ fun MainContent(
}
}
-@Preview(showBackground = true)
@Composable
-fun DefaultPreview() {
+@PreviewLightDark
+private fun DefaultPreview() {
val files = listOf(
- SendFile("foo", "23 KiB", 1337L, Uri.parse(""), null)
+ SendFile("foo", "23 KiB", 1337L, "".toUri(), null)
)
OnionshareTheme {
ShareUi(
@@ -289,9 +289,9 @@ fun DefaultPreview() {
}
}
-@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
+@Preview(showBackground = true)
@Composable
-fun NightModePreview() {
+private fun PreviewLargeFont() {
OnionshareTheme {
ShareUi(
navController = rememberNavController(),
diff --git a/app/src/main/java/org/onionshare/android/ui/share/ShareUiSetup.kt b/app/src/main/java/org/onionshare/android/ui/share/ShareUiSetup.kt
index 3ae1729f..a05f714e 100644
--- a/app/src/main/java/org/onionshare/android/ui/share/ShareUiSetup.kt
+++ b/app/src/main/java/org/onionshare/android/ui/share/ShareUiSetup.kt
@@ -69,10 +69,10 @@ private fun onFabClicked(
) {
try {
contentLauncher.launch(arrayOf("*/*"))
- } catch (e: ActivityNotFoundException) {
+ } catch (_: ActivityNotFoundException) {
try {
contentFallbackLauncher.launch("*/*")
- } catch (e: ActivityNotFoundException) {
+ } catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.add_files_not_supported, Toast.LENGTH_SHORT).show()
}
}
@@ -86,7 +86,7 @@ private fun onSheetButtonClicked(
if (viewModel.shareState.value is ShareUiState.AddingFiles && viewModel.needsDozeWhitelisting) {
try {
batteryLauncher.launch(getDozeWhitelistingIntent(context))
- } catch (e: ActivityNotFoundException) {
+ } catch (_: ActivityNotFoundException) {
// this is really unusual (happened once on a Samsung Galaxy A5 with SDK 23), just pray and proceed
viewModel.onSheetButtonClicked()
}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index ff129ef7..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,21 +0,0 @@
-buildscript {
- ext {
- kotlin_version = '2.1.21'
- hilt_version = '2.56.2'
- compose_version = '2025.06.00'
- }
- repositories {
- google()
- mavenCentral()
- }
- dependencies {
- classpath "com.android.tools.build:gradle:8.10.0"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
- classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version"
- }
-}
-
-tasks.register('clean', Delete) {
- delete rootProject.buildDir
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..30603535
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.hilt.android) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.ktlint) apply false
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ google()
+ }
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
+
diff --git a/gradle.properties b/gradle.properties
index 0086716c..328240b0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,13 +6,13 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xms4g -Xmx8g -Dfile.encoding=UTF-8
# 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
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app"s APK
+# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 00000000..fa77b81c
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,99 @@
+[versions]
+agp = "9.2.1"
+kotlin = "2.3.21"
+hilt = "2.59.2"
+ksp = "2.3.7"
+ktlint-plugin = "10.2.1"
+
+compose-bom = "2026.05.01"
+compose-ui-test-junit4 = "1.11.2"
+
+androidx-core-ktx = "1.19.0"
+androidx-appcompat = "1.7.1"
+androidx-activity-compose = "1.13.0"
+androidx-lifecycle-runtime-ktx = "2.10.0"
+androidx-documentfile = "1.1.0"
+androidx-navigation-compose = "2.9.8"
+androidx-hilt-navigation-compose = "1.3.0"
+androidx-profileinstaller = "1.4.1"
+androidx-security-crypto = "1.1.0"
+androidx-test-ext-junit = "1.3.0"
+androidx-test-espresso-core = "3.7.0"
+
+material = "1.14.0"
+desugar-jdk-libs = "2.1.5"
+
+onionwrapper = "0.1.4"
+tor-android = "0.4.9.9"
+lyrebird-android = "0.6.2"
+moat-api = "0.4"
+
+okhttp = "4.12.0"
+ktor = "3.4.3"
+slf4j = "2.0.18"
+logback-android = "3.0.0"
+
+junit4 = "4.13.2"
+mockk = "1.14.11"
+turbine = "1.2.1"
+kotlinx-coroutines-test = "1.11.0"
+screengrab = "2.1.1"
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-plugin" }
+
+[libraries]
+kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
+kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
+kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
+
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" }
+androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidx-documentfile" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" }
+androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt-navigation-compose" }
+androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidx-profileinstaller" }
+androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidx-security-crypto" }
+androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
+androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-espresso-core" }
+
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar-jdk-libs" }
+
+compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
+compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose-ui-test-junit4" }
+
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
+
+briar-onionwrapper-android = { group = "org.briarproject", name = "onionwrapper-android", version.ref = "onionwrapper" }
+briar-tor-android = { group = "org.briarproject", name = "tor-android", version.ref = "tor-android" }
+briar-lyrebird-android = { group = "org.briarproject", name = "lyrebird-android", version.ref = "lyrebird-android" }
+briar-moat-api = { group = "org.briarproject", name = "moat-api", version.ref = "moat-api" }
+
+okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
+ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
+ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
+ktor-server-pebble = { group = "io.ktor", name = "ktor-server-pebble", version.ref = "ktor" }
+ktor-server-status-pages = { group = "io.ktor", name = "ktor-server-status-pages", version.ref = "ktor" }
+ktor-server-call-logging = { group = "io.ktor", name = "ktor-server-call-logging", version.ref = "ktor" }
+slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
+slf4j-jul-to-slf4j = { group = "org.slf4j", name = "jul-to-slf4j", version.ref = "slf4j" }
+slf4j-jdk14 = { group = "org.slf4j", name = "slf4j-jdk14", version.ref = "slf4j" }
+logback-android = { group = "com.github.tony19", name = "logback-android", version.ref = "logback-android" }
+
+junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
+mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
+turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
+screengrab = { group = "tools.fastlane", name = "screengrab", version.ref = "screengrab" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 4db4a865..ab6968a2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
-distributionSha256Sum=7ebdac923867a3cec0098302416d1e3c6c0c729fc4e2e05c10637a8af33a76c5
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip
+distributionSha256Sum=708d2c6ecc97ca9a11838ef64a6c2301151b8dd10387e22dc1a12c30557cab5b
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index ba914d6e..00000000
--- a/settings.gradle
+++ /dev/null
@@ -1,9 +0,0 @@
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-rootProject.name = "OnionShare"
-include ':app'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..8443d756
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,10 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+rootProject.name = "OnionShare"
+include(":app")