Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Run unit tests (standard flavor)
run: ./gradlew testStandardDebugUnitTest

- name: Run unit tests (tor flavor)
run: ./gradlew testTorDebugUnitTest

- name: Upload Test Reports (xml+html)
if: always()
Expand Down Expand Up @@ -86,8 +89,11 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Run lint
run: ./gradlew lintDebug
- name: Run lint (standard flavor)
run: ./gradlew lintStandardDebug

- name: Run lint (tor flavor)
run: ./gradlew lintTorDebug

- name: Upload lint results
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -126,20 +132,38 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Build debug APK
run: ./gradlew assembleDebug
- name: Build standard debug APK
run: ./gradlew assembleStandardDebug

- name: Build tor debug APK
run: ./gradlew assembleTorDebug

- name: Build standard release APK
run: ./gradlew assembleStandardRelease

- name: Build tor release APK
run: ./gradlew assembleTorRelease

- name: Build release APK
run: ./gradlew assembleRelease
- name: Upload standard debug APK
uses: actions/upload-artifact@v4
with:
name: standard-debug-apk
path: app/build/outputs/apk/standard/debug/*.apk

- name: Upload tor debug APK
uses: actions/upload-artifact@v4
with:
name: tor-debug-apk
path: app/build/outputs/apk/tor/debug/*.apk

- name: Upload debug APK
- name: Upload standard release APK
uses: actions/upload-artifact@v4
with:
name: debug-apk
path: app/build/outputs/apk/debug/*.apk
name: standard-release-apk
path: app/build/outputs/apk/standard/release/*.apk

- name: Upload release APK
- name: Upload tor release APK
uses: actions/upload-artifact@v4
with:
name: release-apk
path: app/build/outputs/apk/release/*.apk
name: tor-release-apk
path: app/build/outputs/apk/tor/release/*.apk
60 changes: 47 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,21 @@ jobs:
- name: Grant execute permission for Gradlew
run: chmod +x ./gradlew

- name: Build APK
run: ./gradlew assembleRelease --no-daemon --stacktrace

- name: Build Standard Flavor (lightweight, no Tor)
run: ./gradlew assembleStandardRelease --no-daemon --stacktrace

- name: Build Tor Flavor (full privacy features)
run: ./gradlew assembleTorRelease --no-daemon --stacktrace

- name: List APK files
run: |
echo "APK files built:"
find app/build/outputs/apk/release -name "*.apk" -type f
find app/build/outputs/apk -name "*.apk" -type f

- name: Rename APK
- name: Rename APKs
run: |
mv app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/bitchat.apk
mv app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app/build/outputs/apk/standard/release/bitchat-standard.apk
mv app/build/outputs/apk/tor/release/app-tor-release-unsigned.apk app/build/outputs/apk/tor/release/bitchat-tor.apk

- name: DEBUG
run: |
Expand All @@ -72,11 +76,19 @@ jobs:
# keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
# keyPassword: ${{ secrets.KEY_PASSWORD }}

- name: Upload APK as artifact
- name: Upload Standard APK as artifact
uses: actions/upload-artifact@v4
with:
name: bitchat-standard-apk-${{ github.ref_name }}
path: app/build/outputs/apk/standard/release/*.apk
retention-days: 30
if-no-files-found: error

- name: Upload Tor APK as artifact
uses: actions/upload-artifact@v4
with:
name: bitchat-release-apk-${{ github.ref_name }}
path: app/build/outputs/apk/release/*.apk
name: bitchat-tor-apk-${{ github.ref_name }}
path: app/build/outputs/apk/tor/release/*.apk
retention-days: 30
if-no-files-found: error

Expand All @@ -85,16 +97,38 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Download APK artifact
- name: Download Standard APK artifact
uses: actions/download-artifact@v4
with:
name: bitchat-standard-apk-${{ github.ref_name }}
path: standard

- name: Download Tor APK artifact
uses: actions/download-artifact@v4
with:
name: bitchat-release-apk-${{ github.ref_name }}
path: .
name: bitchat-tor-apk-${{ github.ref_name }}
path: tor

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: bitchat.apk
files: |
standard/bitchat-standard.apk
tor/bitchat-tor.apk
name: Release ${{ github.ref_name }}
body: |
## bitchat Android Release

### Two builds available:

**bitchat-standard.apk** (~4-5MB)
- Lightweight build without Tor
- Direct internet connections
- Recommended for most users

**bitchat-tor.apk** (~140MB)
- Full privacy features with Tor integration
- All traffic routed through Tor network
- Recommended for maximum anonymity
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 changes: 21 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ android {
abortOnError = false
checkReleaseBuilds = false
}

flavorDimensions += "torSupport"

productFlavors {
create("standard") {
dimension = "torSupport"
// Default lightweight build
proguardFiles("proguard-standard.pro")
}

create("tor") {
dimension = "torSupport"
applicationIdSuffix = ".tor"
versionNameSuffix = "-tor"
// Tor-enabled build with full privacy features
proguardFiles("proguard-tor.pro")
}
}

}

dependencies {
Expand Down Expand Up @@ -96,7 +115,8 @@ dependencies {
implementation(libs.okhttp)

// Arti (Tor in Rust) Android bridge - use published AAR with native libs
implementation("info.guardianproject:arti-mobile-ex:1.2.3")
// Only included in tor flavor to reduce APK size for standard builds
"torImplementation"("info.guardianproject:arti-mobile-ex:1.2.3")

// Google Play Services Location
implementation(libs.gms.location)
Expand Down
14 changes: 8 additions & 6 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
-keep class com.bitchat.android.nostr.** { *; }
-keep class com.bitchat.android.identity.** { *; }

# Arti (Tor) ProGuard rules
-keep class info.guardianproject.arti.** { *; }
-keep class org.torproject.jni.** { *; }
-keepnames class org.torproject.jni.**
-dontwarn info.guardianproject.arti.**
-dontwarn org.torproject.jni.**
# Keep TorProvider implementations (flavor-specific)
-keep class com.bitchat.android.net.TorProvider { *; }
-keep class com.bitchat.android.net.StandardTorProvider { *; }
-keep class com.bitchat.android.net.RealTorProvider { *; }
-keep class com.bitchat.android.net.TorProviderFactory { *; }

# Note: Tor-specific ProGuard rules have been moved to proguard-tor.pro
# (applied only to tor flavor builds)
11 changes: 11 additions & 0 deletions app/proguard-standard.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Standard flavor ProGuard rules
# These rules are applied ONLY to the standard flavor build

# Keep standard flavor Tor implementation
-keep class com.bitchat.android.net.StandardTorProvider { *; }

# Preserve line numbers for debugging
-keepattributes SourceFile,LineNumberTable

# No Tor-related rules needed for standard build
# (Guardian Project Arti library not included)
15 changes: 15 additions & 0 deletions app/proguard-tor.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Tor flavor ProGuard rules
# These rules are applied ONLY to the tor flavor build

# Arti (Guardian Project Tor implementation in Rust) ProGuard rules
-keep class info.guardianproject.arti.** { *; }
-keep class org.torproject.jni.** { *; }
-keepnames class org.torproject.jni.**
-dontwarn info.guardianproject.arti.**
-dontwarn org.torproject.jni.**

# Keep Tor-specific classes
-keep class com.bitchat.android.net.RealTorProvider { *; }

# Preserve line numbers for debugging Tor-related crashes
-keepattributes SourceFile,LineNumberTable
12 changes: 8 additions & 4 deletions app/src/main/java/com/bitchat/android/BitchatApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ package com.bitchat.android
import android.app.Application
import com.bitchat.android.nostr.RelayDirectory
import com.bitchat.android.ui.theme.ThemePreferenceManager
import com.bitchat.android.net.TorManager
import com.bitchat.android.net.TorProviderFactory

/**
* Main application class for bitchat Android
*/
class BitchatApplication : Application() {

override fun onCreate() {
super.onCreate()

// Initialize Tor first so any early network goes over Tor
try { TorManager.init(this) } catch (_: Exception) { }
// Uses TorProviderFactory to get the correct implementation based on build flavor
try {
val torProvider = TorProviderFactory.getInstance()
torProvider.init(this)
} catch (_: Exception){}

// Initialize relay directory (loads assets/nostr_relays.csv)
RelayDirectory.initialize(this)
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/com/bitchat/android/net/OkHttpProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ object OkHttpProvider {

private fun baseBuilderForCurrentProxy(): OkHttpClient.Builder {
val builder = OkHttpClient.Builder()
val socks: InetSocketAddress? = TorManager.currentSocksAddress()
// If a SOCKS address is defined, always use it. TorManager sets this as soon as Tor mode is ON,
// Get TorProvider instance and check for SOCKS address
val torProvider = TorProviderFactory.getInstance()
val socks: InetSocketAddress? = torProvider.currentSocksAddress()
// If a SOCKS address is defined, always use it. TorProvider sets this as soon as Tor mode is ON,
// even before bootstrap, to prevent any direct connections from occurring.
if (socks != null) {
val proxy = Proxy(Proxy.Type.SOCKS, socks)
Expand Down
62 changes: 62 additions & 0 deletions app/src/main/java/com/bitchat/android/net/TorProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.bitchat.android.net

import android.app.Activity
import android.app.Application
import kotlinx.coroutines.flow.StateFlow
import java.net.InetSocketAddress

/**
* Abstraction layer for Tor functionality.
*
* Implementations:
* - StandardTorProvider: No-op (standard flavor)
* - RealTorProvider: Guardian Project Arti (tor flavor)
* - DynamicTorProvider: On-demand loading (Phase 2, Play Store)
*/
interface TorProvider {
enum class TorState {
OFF,
STARTING,
BOOTSTRAPPING,
RUNNING,
STOPPING,
ERROR,
NOT_AVAILABLE
}

data class TorStatus(
val mode: TorMode = TorMode.OFF,
val running: Boolean = false,
val bootstrapPercent: Int = 0,
val lastLogLine: String = "",
val state: TorState = TorState.OFF,
val isAvailable: Boolean = false
)

val statusFlow: StateFlow<TorStatus>

fun init(application: Application)
suspend fun applyMode(application: Application, mode: TorMode)
fun currentSocksAddress(): InetSocketAddress?
fun isProxyEnabled(): Boolean
fun isTorAvailable(): Boolean

// TODO: Phase 2: Dynamic Feature Module support (no-op for Phase 1)
fun isModuleInstalled(): Boolean = false
fun requestModuleInstall(activity: Activity, listener: InstallStatusListener) {}

interface InstallStatusListener {
fun onInstallStarted(sessionId: Int)
fun onInstallProgress(bytesDownloaded: Long, totalBytes: Long)
fun onInstallCompleted()
fun onInstallFailed(exception: Exception)
fun onInstallCanceled()
}

enum class InstallStatus {
NOT_INSTALLED,
DOWNLOADING,
INSTALLED,
FAILED
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/bitchat/android/net/TorProviderFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.bitchat.android.net

/**
* Factory to provide the correct TorProvider implementation based on build flavor.
*
* Uses double-checked locking singleton pattern for thread-safe lazy initialization.
*
* Implementations:
* - standard flavor: StandardTorProvider (no Tor)
* - tor flavor: RealTorProvider (full Tor)
* - Phase 2: DynamicTorProvider (on-demand download)
*/
object TorProviderFactory {
@Volatile
private var instance: TorProvider? = null

/**
* Get the singleton TorProvider instance for the current build flavor.
*/
fun getInstance(): TorProvider {
instance?.let { return it }

return synchronized(this) {
instance?.let { return it }
val newInstance = TorProviderFactoryImpl.create()
instance = newInstance
newInstance
}
}

/**
* Reset singleton for testing only.
*/
internal fun resetForTesting() {
synchronized(this) {
instance = null
}
}
}
Loading
Loading