mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Add benchmark flavor and CI workflow to build/upload APK per commit
Introduces a `benchmark` product flavor that mirrors the `free` online network stack but uses a distinct applicationId suffix (`.benchmark`) so it installs side-by-side with a production build. A new GitHub Actions workflow builds the benchmark APK on every push and uploads it as an artifact, making per-commit changes installable and reviewable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015X6g3nwsWu6HbuwpA2ALu8
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
name: Benchmark APK
|
||||
|
||||
# Builds the `benchmark` flavor APK on every push so the changes in each commit
|
||||
# can be downloaded, installed, and reviewed. The benchmark flavor uses a distinct
|
||||
# applicationId suffix, so it installs side-by-side with a production build.
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Grant execute permission to Gradle wrapper
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Create keystore.properties
|
||||
run: touch keystore.properties
|
||||
|
||||
- name: Build benchmark APK
|
||||
run: ./gradlew assembleBenchmarkDebug --no-daemon --stacktrace
|
||||
|
||||
- name: Upload benchmark APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: amber-benchmark-${{ github.sha }}
|
||||
path: app/build/outputs/apk/benchmark/debug/app-benchmark-universal-debug.apk
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -141,6 +141,14 @@ android {
|
||||
register("offline") {
|
||||
dimension = "version"
|
||||
}
|
||||
// Online variant (same network stack as `free`) built and uploaded by CI on
|
||||
// every push so changes can be installed and reviewed per commit. The distinct
|
||||
// applicationId suffix lets it sit side-by-side with a production install.
|
||||
register("benchmark") {
|
||||
dimension = "version"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
versionNameSuffix = "-BENCHMARK"
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
@@ -238,6 +246,12 @@ dependencies {
|
||||
"freeImplementation"(libs.kmptor.runtime)
|
||||
"freeImplementation"(libs.kmptor.resource.exec)
|
||||
|
||||
// benchmark mirrors the `free` online network stack
|
||||
"benchmarkImplementation"(libs.okhttp)
|
||||
"benchmarkImplementation"(libs.okhttpCoroutines)
|
||||
"benchmarkImplementation"(libs.kmptor.runtime)
|
||||
"benchmarkImplementation"(libs.kmptor.resource.exec)
|
||||
|
||||
// Load images from the web.
|
||||
implementation(libs.coil.compose)
|
||||
// view gifs
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<queries>
|
||||
<package android:name="org.torproject.android"/>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".Amber">
|
||||
|
||||
<service
|
||||
android:name=".service.ConnectivityService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Run a foreground service to keep the connection to the relays active"
|
||||
/>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SignerActivity"
|
||||
android:exported="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.NostrSigner.Modal">
|
||||
<intent-filter>
|
||||
<action android:name="com.greenart7c3.nostrsigner.MAIN_ACTIVITY" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</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="nostrsigner" />
|
||||
</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="nostrconnect" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.greenart7c3.nostrsigner.service
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationChannelGroupCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.greenart7c3.nostrsigner.MainActivity
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
|
||||
import io.matthewnelson.kmp.tor.resource.exec.tor.ResourceLoaderTorExec
|
||||
import io.matthewnelson.kmp.tor.runtime.Action.Companion.startDaemonAsync
|
||||
import io.matthewnelson.kmp.tor.runtime.Action.Companion.stopDaemonAsync
|
||||
import io.matthewnelson.kmp.tor.runtime.RuntimeEvent
|
||||
import io.matthewnelson.kmp.tor.runtime.TorRuntime
|
||||
import io.matthewnelson.kmp.tor.runtime.core.OnEvent
|
||||
import io.matthewnelson.kmp.tor.runtime.core.config.TorOption
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object TorManager {
|
||||
private const val TAG = "TorManager"
|
||||
private const val TOR_NOTIFICATION_ID = 3
|
||||
private const val TOR_CHANNEL_ID = "TorChannel"
|
||||
private const val TOR_CHANNEL_GROUP_ID = "TorGroup"
|
||||
|
||||
@Volatile private var appContext: Context? = null
|
||||
|
||||
@Volatile
|
||||
private var torRuntime: TorRuntime? = null
|
||||
|
||||
@Volatile
|
||||
private var portCollectorStarted = false
|
||||
|
||||
private val _isRunning = MutableStateFlow(false)
|
||||
val isRunning: StateFlow<Boolean> = _isRunning.asStateFlow()
|
||||
|
||||
private val _socksPort = MutableStateFlow(0)
|
||||
val socksPort: StateFlow<Int> = _socksPort.asStateFlow()
|
||||
|
||||
fun init(context: Context) {
|
||||
if (appContext != null) return
|
||||
appContext = context.applicationContext
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val ctx = appContext ?: return
|
||||
val group = NotificationChannelGroupCompat.Builder(TOR_CHANNEL_GROUP_ID)
|
||||
.setName(ctx.getString(R.string.builtin_tor_title))
|
||||
.setDescription(ctx.getString(R.string.tor_status_channel_description))
|
||||
.build()
|
||||
val channel = NotificationChannelCompat.Builder(TOR_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
|
||||
.setName(ctx.getString(R.string.builtin_tor_title))
|
||||
.setDescription(ctx.getString(R.string.tor_status_channel_description))
|
||||
.setSound(null, null)
|
||||
.setGroup(group.id)
|
||||
.build()
|
||||
val nm = NotificationManagerCompat.from(ctx)
|
||||
nm.createNotificationChannelGroup(group)
|
||||
nm.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun showNotification(text: String) {
|
||||
val ctx = appContext ?: return
|
||||
if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return
|
||||
val contentIntent = Intent(ctx, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
val pendingIntent = PendingIntent.getActivity(ctx, 0, contentIntent, PendingIntent.FLAG_MUTABLE)
|
||||
val notification = NotificationCompat.Builder(ctx, TOR_CHANNEL_ID)
|
||||
.setContentTitle(ctx.getString(R.string.builtin_tor_title))
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_tor)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
NotificationManagerCompat.from(ctx).notify(TOR_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
fun cancelNotification() {
|
||||
appContext?.let { NotificationManagerCompat.from(it).cancel(TOR_NOTIFICATION_ID) }
|
||||
}
|
||||
|
||||
fun showRetrying() {
|
||||
appContext?.let { showNotification(it.getString(R.string.tor_retrying)) }
|
||||
}
|
||||
|
||||
fun start(context: Context, scope: CoroutineScope) {
|
||||
if (appContext == null) {
|
||||
appContext = context.applicationContext
|
||||
createNotificationChannel()
|
||||
}
|
||||
if (!portCollectorStarted) {
|
||||
portCollectorStarted = true
|
||||
scope.launch(Dispatchers.IO) {
|
||||
_socksPort.collect { port ->
|
||||
if (port > 0) {
|
||||
HttpClientManager.setDefaultProxyOnPort(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (torRuntime != null) return
|
||||
showNotification(context.getString(R.string.tor_starting))
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val workDir = context.filesDir.resolve("kmptor")
|
||||
val cacheDir = context.cacheDir.resolve("kmptor")
|
||||
|
||||
val env = TorRuntime.Environment.Builder(workDir, cacheDir, ResourceLoaderTorExec::getOrCreate)
|
||||
|
||||
val runtime = TorRuntime.Builder(env) {
|
||||
// Log all runtime events for debugging
|
||||
RuntimeEvent.entries().forEach { event ->
|
||||
observerStatic(event, OnEvent.Executor.Immediate) { data ->
|
||||
Log.d(TAG, data.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Observe SOCKS listeners to get the assigned port
|
||||
observerStatic(RuntimeEvent.LISTENERS, OnEvent.Executor.Immediate) { listeners ->
|
||||
val addr = listeners.socks.firstOrNull()
|
||||
if (addr != null) {
|
||||
try {
|
||||
val port = addr.port.value
|
||||
Log.i(TAG, "Built-in Tor SOCKS proxy on port $port")
|
||||
_socksPort.value = port
|
||||
_isRunning.value = true
|
||||
appContext?.let { showNotification(it.getString(R.string.builtin_tor_active)) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to read Tor SOCKS port", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config {
|
||||
TorOption.__SocksPort.configure { auto() }
|
||||
}
|
||||
}
|
||||
|
||||
torRuntime = runtime
|
||||
runtime.startDaemonAsync()
|
||||
Log.i(TAG, "Built-in Tor started")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start built-in Tor", e)
|
||||
torRuntime = null
|
||||
_isRunning.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val runtime = torRuntime ?: return
|
||||
torRuntime = null
|
||||
_isRunning.value = false
|
||||
_socksPort.value = 0
|
||||
cancelNotification()
|
||||
try {
|
||||
runBlocking(Dispatchers.IO) { runtime.stopDaemonAsync() }
|
||||
Log.i(TAG, "Built-in Tor stopped")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error stopping built-in Tor", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user