Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,36 @@ jobs:
name: Twitch.Drops.Miner.Windows
path: Twitch.Drops.Miner.Windows.zip

android:
name: Android
runs-on: ubuntu-latest
needs:
- validate

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle

- name: Build and test Android app
working-directory: android
run: |
chmod +x gradlew
./gradlew testDebugUnitTest assembleDebug --no-daemon

- name: Upload Android APK
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Twitch.Drops.Miner.Android
path: android/app/build/outputs/apk/debug/app-debug.apk

macos:
name: macOS
runs-on: macos-latest
Expand Down Expand Up @@ -409,6 +439,7 @@ jobs:
name: Upload builds to Releases
if: github.event_name != 'pull_request'
needs:
- android
- windows
- linux-pyinstaller
- linux-appimage
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ settings.json
*.app
/.venv-linux-build
/.venv-tui
/graphify-out
/graphify-out

# Android local SDK/build state
/android/.gradle
/android/.kotlin
/android/local.properties
/android/app/build
40 changes: 40 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# TD Miner Android

Command-line Android build. No Android Studio required.

## Build

```powershell
.\gradlew.bat testDebugUnitTest assembleDebug
```

APK:

```text
app/build/outputs/apk/debug/app-debug.apk
```

## Install To Phone

Enable USB debugging, then:

```powershell
$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $adb devices
& $adb install -r app\build\outputs\apk\debug\app-debug.apk
& $adb shell am start -n io.github.himanm.tdminer/.MainActivity
```

## Seed Desktop Cookies For Debug Testing

Do not bake cookies into the APK. For local testing, copy the existing desktop JSON cookie jar into the app-private files directory:

```powershell
& $adb push ..\dist\cookies.jar /sdcard/Download/tdminer-cookies.jar
& $adb shell run-as io.github.himanm.tdminer sh -c "mkdir -p files && cp /sdcard/Download/tdminer-cookies.jar files/cookies.jar"
& $adb shell am start -n io.github.himanm.tdminer/.MainActivity
```

The app stores imported cookies in private `SharedPreferences`. They survive `adb install -r` updates and are cleared by Logout or app uninstall.

The current app is a native responsive shell only. It does not run the real miner core yet.
51 changes: 51 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
namespace = "io.github.himanm.tdminer"
compileSdk = 35

defaultConfig {
applicationId = "io.github.himanm.tdminer"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildFeatures {
compose = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}

dependencies {
implementation("androidx.activity:activity-compose:1.9.3")
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")

testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20240303")
}
43 changes: 43 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<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>

<service
android:name=".MinerForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />

<receiver
android:name=".MinerWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.github.himanm.tdminer.START" />
<action android:name="io.github.himanm.tdminer.STOP" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/miner_widget_info" />
</receiver>
</application>
</manifest>
43 changes: 43 additions & 0 deletions android/app/src/main/java/io/github/himanm/tdminer/CookieStore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.himanm.tdminer

import android.content.Context

interface CookieStore {
fun loadCookies(): String?
fun saveCookies(cookies: String)
fun hasCookies(): Boolean = !loadCookies().isNullOrBlank()
fun loadCookieJar(): TwitchCookieJar? = loadCookies()?.let(TwitchCookieJar::parse)
fun logout()
}

class SharedPrefsCookieStore(context: Context) : CookieStore {
private val prefs = context.getSharedPreferences("tdminer_auth", Context.MODE_PRIVATE)

override fun loadCookies(): String? = prefs.getString(KEY_COOKIES, null)

override fun saveCookies(cookies: String) {
prefs.edit().putString(KEY_COOKIES, cookies).apply()
}

override fun logout() {
prefs.edit().remove(KEY_COOKIES).apply()
}

companion object {
private const val KEY_COOKIES = "cookies"
}
}

class MemoryCookieStore : CookieStore {
private var cookies: String? = null

override fun loadCookies(): String? = cookies

override fun saveCookies(cookies: String) {
this.cookies = cookies
}

override fun logout() {
cookies = null
}
}
41 changes: 41 additions & 0 deletions android/app/src/main/java/io/github/himanm/tdminer/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.github.himanm.tdminer

import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 33) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100)
}
val core = MinerCore(SharedPrefsCookieStore(this))
intent.getStringExtra(EXTRA_COOKIES)?.takeIf { it.isNotBlank() }?.let(core::saveCookies)
setContent {
TDMinerApp(
core = core,
settingsStore = SharedPrefsMinerSettingsStore(this),
onStart = {
startForegroundService(Intent(this, MinerForegroundService::class.java))
MinerWidgetProvider.updateAll(this, true)
},
onStop = {
startService(
Intent(this, MinerForegroundService::class.java).setAction(ACTION_STOP),
)
MinerWidgetProvider.updateAll(this, false)
},
onOpenUrl = { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) },
)
}
}

companion object {
const val EXTRA_COOKIES = "io.github.himanm.tdminer.COOKIES"
}
}
Loading
Loading