Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
26f7812
Add Discord presence domain models
PastaHimself Jul 14, 2026
76b6bee
Map Flow playback to Discord presence
PastaHimself Jul 14, 2026
e920f2f
Add Discord presence update policy
PastaHimself Jul 14, 2026
24a2ccf
Test Discord presence mapping
PastaHimself Jul 14, 2026
5bce0da
Test Discord presence update policy
PastaHimself Jul 14, 2026
4d24469
Store Discord tokens with Android Keystore
PastaHimself Jul 14, 2026
0054300
feat: add Discord presence transport seam
PastaHimself Jul 14, 2026
d3e82d4
feat: add unavailable Discord transport
PastaHimself Jul 14, 2026
ba53eba
test: define Discord presence coordination
PastaHimself Jul 14, 2026
a56542d
test: align Discord coordinator contract
PastaHimself Jul 14, 2026
d45881a
feat: coordinate Discord presence updates
PastaHimself Jul 14, 2026
b4e6616
test: define no-op Discord transport behavior
PastaHimself Jul 14, 2026
aa0eb22
fix: complete no-op Discord transport
PastaHimself Jul 14, 2026
3d48e67
fix: import Flow collection operator
PastaHimself Jul 14, 2026
ec7643f
docs(discord): define integration design
PastaHimself Jul 14, 2026
7cc5d18
test(discord): define preference and settings state
PastaHimself Jul 14, 2026
34ba97e
feat(discord): persist settings and select playback
PastaHimself Jul 14, 2026
e174ac9
feat(discord): add Kizzy-style Gateway transport
PastaHimself Jul 14, 2026
15a0be9
feat(settings): expose Discord Rich Presence
PastaHimself Jul 14, 2026
b91db13
ci(discord): verify nightly integration
PastaHimself Jul 14, 2026
76c736b
fix(settings): compile Discord presence screen
PastaHimself Jul 14, 2026
22fa9fb
fix(discord): make gateway protocol JVM-testable
PastaHimself Jul 14, 2026
a95cd13
fix(discord): harden connection and settings lifecycle
PastaHimself Jul 14, 2026
6ea4db2
test(discord): cover retry unlink and lifecycle clearing
PastaHimself Jul 14, 2026
d0f6c5c
ci(discord): build exact branch head
PastaHimself Jul 14, 2026
67f58b9
fix(ci): upload signed or unsigned release APK
PastaHimself Jul 14, 2026
26d7241
fix(ci): upload signed or unsigned FOSS APK
PastaHimself Jul 14, 2026
0ef4e8a
fix(discord): capture token after authenticated login
PastaHimself Jul 14, 2026
f78f1d7
fix(discord): normalize quoted WebView token
PastaHimself Jul 14, 2026
12d2513
fix(player): promote video service before playback setup
PastaHimself Jul 14, 2026
1cad2d5
fix: read Discord playback snapshots on main thread
PastaHimself Jul 14, 2026
93a6b0b
test: cover Discord player-thread snapshot reads
PastaHimself Jul 14, 2026
8f03d0f
test: make Discord playback thread regression test JVM-safe
PastaHimself Jul 14, 2026
93e9d39
ci: run Android builds on pasta_vps
PastaHimself Jul 14, 2026
ad7b21a
Update build.yml
PastaHimself Jul 14, 2026
8ce5fcb
Update build.yml
PastaHimself Jul 14, 2026
379fd1d
ci: match the Ubuntu self-hosted runner labels
PastaHimself Jul 14, 2026
c4d8fd5
ci: allow manual Android workflow runs
PastaHimself Jul 14, 2026
6fa8152
test: document Discord playback dispatcher boundary
PastaHimself Jul 14, 2026
34f705e
ci: trigger self-hosted Android verification
PastaHimself Jul 14, 2026
7035cb0
ci: install Android SDK on self-hosted runner
PastaHimself Jul 14, 2026
468113d
test: isolate Discord playback dispatcher boundary
PastaHimself Jul 14, 2026
9bff8e3
test: compare Discord playback thread identity
PastaHimself Jul 14, 2026
856ec2a
ci: return Android builds to GitHub-hosted runners
PastaHimself Jul 14, 2026
5a4e896
Merge main into agent/discord-rich-presence
PastaHimself Jul 14, 2026
fa09673
fix(ci): restore complete Android workflow
PastaHimself Jul 14, 2026
1b58ff7
Merge pull request #1 from PastaHimself/agent/discord-rich-presence
PastaHimself Jul 14, 2026
5b4077b
Merge branch 'A-EDev:main' into main
PastaHimself Jul 14, 2026
8b3ce18
Update build.yml
PastaHimself Jul 14, 2026
bf1178b
Merge branch 'A-EDev:main' into main
PastaHimself Jul 14, 2026
e45ab3f
Merge branch 'A-EDev:main' into main
PastaHimself Jul 15, 2026
1328e53
file that is not needed
PastaHimself Jul 15, 2026
5f88319
file that is not needed
PastaHimself Jul 15, 2026
5fe0ed2
Merge branch 'A-EDev:main' into main
PastaHimself Jul 15, 2026
f0f74d8
Merge branch 'A-EDev:main' into main
PastaHimself Jul 15, 2026
87f5641
Merge branch 'A-EDev:main' into main
PastaHimself Jul 15, 2026
3238517
Merge branch 'A-EDev:main' into main
PastaHimself Jul 16, 2026
db2ccd6
feat(tv): add native Android TV interface
PastaHimself Jul 16, 2026
815643e
feat(tv): integrate TV runtime and settings
github-actions[bot] Jul 16, 2026
bd9c9d2
chore(tv): remove integration bootstrap
PastaHimself Jul 16, 2026
afb87e0
chore(tv): remove integration bootstrap data
PastaHimself Jul 16, 2026
0262e3f
chore(tv): add one-run compile diagnostic
PastaHimself Jul 16, 2026
4480465
chore(tv): reduce diagnostic Gradle output
PastaHimself Jul 16, 2026
dbac1d3
chore(tv): capture compile diagnostics
github-actions[bot] Jul 16, 2026
c8131dc
fix(tv): use Compose focus modifier API
PastaHimself Jul 16, 2026
5040316
chore(tv): capture compile diagnostics
github-actions[bot] Jul 16, 2026
c28480b
fix(tv): use supported Material color token
PastaHimself Jul 16, 2026
d4bf930
fix(tv): use KeyEvent native property directly
PastaHimself Jul 16, 2026
af8c3d3
ci: capture TV compile diagnostics
PastaHimself Jul 16, 2026
950c1b9
fix(tv): re-encode launcher banner for AAPT2
PastaHimself Jul 16, 2026
40c0e10
fix(tv): replace crashing PNG banner with vector resource
PastaHimself Jul 16, 2026
4c27f10
chore: remove temporary TV diagnostics
PastaHimself Jul 16, 2026
fcb0ace
Merge pull request #2 from PastaHimself/feat/android-tv-support
PastaHimself Jul 16, 2026
6e1daa4
Revert "feat(tv): add native Android TV support"
PastaHimself Jul 16, 2026
af94159
Merge pull request #3 from PastaHimself/revert-2-feat/android-tv-support
PastaHimself Jul 16, 2026
dfdb1d9
Rename uploaded APK artifact to flow-nightly-apk
PastaHimself Jul 16, 2026
b02da61
Merge branch 'A-EDev:main' into main
PastaHimself Jul 16, 2026
e42629c
feat(tv): add native Android TV support to upstream PR
PastaHimself Jul 16, 2026
29154a1
Merge branch 'A-EDev:main' into main
PastaHimself Jul 16, 2026
4d22ad1
chore: refresh PR mergeability metadata
PastaHimself Jul 16, 2026
ec95103
chore: refresh PR mergeability after upstream sync
PastaHimself Jul 16, 2026
45b431a
chore: remove mergeability refresh marker
PastaHimself Jul 16, 2026
7ccec2b
chore: refresh PR mergeability after upstream sync
PastaHimself Jul 16, 2026
23f4d81
chore: refresh PR mergeability after upstream sync
PastaHimself Jul 16, 2026
11059b3
Merge upstream main and resolve MainActivity conflict
PastaHimself Jul 16, 2026
6f6ac44
Merge upstream main and resolve MainActivity conflict
PastaHimself Jul 16, 2026
cc8f87c
chore: restore non-conflicting upstream playback changes
PastaHimself Jul 16, 2026
a969127
Merge upstream main and resolve MainActivity conflict
PastaHimself Jul 16, 2026
efded98
chore: restore non-conflicting upstream channel and playback changes
PastaHimself Jul 16, 2026
0eab8c9
chore: refresh PR mergeability
PastaHimself Jul 16, 2026
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
36 changes: 29 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Android CI/CD

on:
push:
branches: [ main, master, develop ]
branches: [ main, master, develop, agent/discord-rich-presence ]
tags:
- 'v*'
pull_request:
Expand All @@ -19,6 +19,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}

- name: Set up JDK 17
uses: actions/setup-java@v4
Expand Down Expand Up @@ -49,6 +51,16 @@ jobs:
echo "Release keystore secret is not available. Skipping decoding."
fi

- name: Run unit tests
run: >
./gradlew :app:testGithubNightlyUnitTest :app:testFossReleaseUnitTest
--parallel --build-cache --no-daemon --stacktrace

- name: Lint GitHub nightly
run: >
./gradlew :app:lintGithubNightly
--build-cache --no-daemon --stacktrace

# Build all variants in a single Gradle invocation so tasks share the
# configuration phase, classpath resolution and incremental build cache.
# --parallel — lets Gradle run independent subproject tasks concurrently.
Expand All @@ -63,25 +75,35 @@ jobs:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Verify Discord nightly APK
run: |
test -f app/build/outputs/apk/github/nightly/app-github-nightly.apk
sha256sum app/build/outputs/apk/github/nightly/app-github-nightly.apk | tee app/build/outputs/apk/github/nightly/app-github-nightly.apk.sha256

- name: Upload Release APK
uses: actions/upload-artifact@v4
with:
name: flow-release-apk
path: app/build/outputs/apk/github/release/app-github-release.apk
path: app/build/outputs/apk/github/release/*.apk
if-no-files-found: error
retention-days: 14

- name: Upload Nightly APK
uses: actions/upload-artifact@v4
with:
name: flow-nightly-apk
path: app/build/outputs/apk/github/nightly/app-github-nightly.apk
path: |
app/build/outputs/apk/github/nightly/app-github-nightly.apk
app/build/outputs/apk/github/nightly/app-github-nightly.apk.sha256
if-no-files-found: error
retention-days: 14

- name: Upload FOSS Release APK
uses: actions/upload-artifact@v4
with:
name: flow-foss-release-apk
path: app/build/outputs/apk/foss/release/app-foss-release.apk
path: app/build/outputs/apk/foss/release/*.apk
if-no-files-found: error
retention-days: 14

release:
Expand All @@ -104,7 +126,7 @@ jobs:
- name: Download Nightly APK
uses: actions/download-artifact@v4
with:
name: flow-nightly-apk
name: flow-discord-rpc-nightly-apk
path: dist/

- name: Download FOSS Release APK
Expand All @@ -124,7 +146,7 @@ jobs:
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Create Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.VERSION }}
name: Flow ${{ steps.get_version.outputs.VERSION }}
Expand Down Expand Up @@ -265,4 +287,4 @@ jobs:
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ android {
dimension = "version"
isDefault = true
buildConfigField("Boolean", "UPDATER_ENABLED", "true")
buildConfigField("String", "DISCORD_APPLICATION_ID", "\"1526515771021328514\"")
}
create("foss") {
dimension = "version"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.aedev.flow.discord

import android.content.Context
import okhttp3.OkHttpClient

class DiscordPlatformTransportFactory : DiscordPresenceTransportFactory {
override fun create(
context: Context,
okHttpClient: OkHttpClient,
tokenStore: DiscordTokenStore,
): DiscordPresenceTransport = NoOpDiscordPresenceTransport()
}
9 changes: 9 additions & 0 deletions app/src/github/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="io.github.aedev.flow.discord.DiscordLoginActivity"
android:exported="false"
android:theme="@style/Theme.Flow" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.github.aedev.flow.discord

import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.view.WindowManager
import android.webkit.CookieManager
import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity

class DiscordLoginActivity : ComponentActivity() {
private var completed = false

@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)

val webView = WebView(this).apply {
setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.userAgentString = settings.userAgentString.replace("; wv", "")
webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
requestTokenWhenAuthenticated(view, url)
return false
}

override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
requestTokenWhenAuthenticated(view, url)
}
}
loadUrl(DISCORD_LOGIN_URL)
}
setContentView(webView)
}

override fun onDestroy() {
if (!completed && isFinishing) {
DiscordLoginBroker.fail("Discord connection was cancelled.")
}
super.onDestroy()
}

private fun requestTokenWhenAuthenticated(webView: WebView, url: String) {
if (!completed && DiscordLoginTokenExtractor.isAuthenticatedAppUrl(url)) {
readToken(webView)
}
}

private fun readToken(webView: WebView) {
webView.evaluateJavascript(TOKEN_SCRIPT) { encodedResult ->
val token = DiscordLoginTokenExtractor.decodeJavascriptValue(encodedResult)

if (token != null && !completed) {
completed = true
DiscordLoginBroker.complete(token)
webView.clearHistory()
webView.clearCache(true)
WebStorage.getInstance().deleteAllData()
CookieManager.getInstance().removeAllCookies(null)
finish()
}
}
}

private companion object {
const val DISCORD_LOGIN_URL = "https://discord.com/login"
const val TOKEN_SCRIPT = "(function(){" +
"var frame=document.createElement('iframe');" +
"frame.style.display='none';" +
"document.body.appendChild(frame);" +
"var token=frame.contentWindow.localStorage.getItem('token');" +
"frame.remove();" +
"return token;" +
"})()"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.aedev.flow.discord

import kotlinx.coroutines.CompletableDeferred

internal object DiscordLoginBroker {
private var pending: CompletableDeferred<Result<String>>? = null

@Synchronized
fun begin(): CompletableDeferred<Result<String>>? {
if (pending?.isActive == true) return null
return CompletableDeferred<Result<String>>().also { pending = it }
}

@Synchronized
fun complete(token: String) {
pending?.complete(Result.success(token))
pending = null
}

@Synchronized
fun fail(message: String) {
pending?.complete(Result.failure(IllegalStateException(message)))
pending = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.aedev.flow.discord

import java.net.URI
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.jsonPrimitive

internal object DiscordLoginTokenExtractor {
fun isAuthenticatedAppUrl(url: String): Boolean = runCatching {
val uri = URI(url)
uri.scheme.equals("https", ignoreCase = true) &&
uri.host.equals("discord.com", ignoreCase = true) &&
uri.path.trimEnd('/') == "/app"
}.getOrDefault(false)

fun decodeJavascriptValue(value: String): String? = runCatching {
val element = Json.parseToJsonElement(value)
if (element is JsonNull) null else element.jsonPrimitive.content
}.getOrNull()
?.trim()
?.removeSurrounding("\"")
?.takeIf { it.isNotEmpty() && it != "null" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.github.aedev.flow.discord

import android.content.Context
import io.github.aedev.flow.BuildConfig
import okhttp3.OkHttpClient

class DiscordPlatformTransportFactory : DiscordPresenceTransportFactory {
override fun create(
context: Context,
okHttpClient: OkHttpClient,
tokenStore: DiscordTokenStore,
): DiscordPresenceTransport = KizzyDiscordPresenceTransport(
client = okHttpClient,
tokenStore = tokenStore,
applicationId = BuildConfig.DISCORD_APPLICATION_ID,
)
}
Loading