Skip to content

Commit b56f067

Browse files
Merge pull request #54 from Android-PowerUser/human-operator
Human operator
2 parents 32f04a6 + 2502b14 commit b56f067

33 files changed

+2691
-123
lines changed

.github/workflows/manual.yml

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,140 @@
11
name: Android Build
22

33
on:
4-
# push:
5-
# branches: [ main, develop ]
6-
# pull_request:
7-
# branches: [ main ]
4+
push:
5+
branches: [ human-operator, main ]
86
workflow_dispatch: # Ermöglicht manuelle Ausführung des Workflows
97

108
jobs:
9+
detect-changes:
10+
runs-on: ubuntu-latest
11+
outputs:
12+
app_changed: ${{ steps.changes.outputs.app }}
13+
humanoperator_changed: ${{ steps.changes.outputs.humanoperator }}
14+
shared_changed: ${{ steps.changes.outputs.shared }}
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 2 # Letzten 2 Commits holen für Diff
20+
21+
- name: Detect changed files
22+
id: changes
23+
run: |
24+
# Bei workflow_dispatch immer alles bauen
25+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
26+
echo "app=true" >> $GITHUB_OUTPUT
27+
echo "humanoperator=true" >> $GITHUB_OUTPUT
28+
echo "shared=true" >> $GITHUB_OUTPUT
29+
echo "Manual dispatch - building all modules"
30+
exit 0
31+
fi
32+
33+
# Geänderte Dateien im letzten Commit ermitteln
34+
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
35+
36+
# Falls kein vorheriger Commit existiert (erster Commit), alles bauen
37+
if [ -z "$CHANGED_FILES" ]; then
38+
echo "app=true" >> $GITHUB_OUTPUT
39+
echo "humanoperator=true" >> $GITHUB_OUTPUT
40+
echo "shared=true" >> $GITHUB_OUTPUT
41+
echo "No previous commit found - building all modules"
42+
exit 0
43+
fi
44+
45+
echo "Changed files:"
46+
echo "$CHANGED_FILES"
47+
48+
# Prüfen ob shared/root files geändert wurden (build.gradle, settings.gradle, etc.)
49+
SHARED_CHANGED=false
50+
if echo "$CHANGED_FILES" | grep -qE '^(build\.gradle|settings\.gradle|gradle\.properties|gradle/|buildSrc/)'; then
51+
SHARED_CHANGED=true
52+
fi
53+
54+
# Prüfen ob app/ Dateien geändert wurden
55+
APP_CHANGED=false
56+
if echo "$CHANGED_FILES" | grep -q '^app/'; then
57+
APP_CHANGED=true
58+
fi
59+
60+
# Prüfen ob humanoperator/ Dateien geändert wurden
61+
HUMANOPERATOR_CHANGED=false
62+
if echo "$CHANGED_FILES" | grep -q '^humanoperator/'; then
63+
HUMANOPERATOR_CHANGED=true
64+
fi
65+
66+
echo "app=$APP_CHANGED" >> $GITHUB_OUTPUT
67+
echo "humanoperator=$HUMANOPERATOR_CHANGED" >> $GITHUB_OUTPUT
68+
echo "shared=$SHARED_CHANGED" >> $GITHUB_OUTPUT
69+
70+
echo "Results: app=$APP_CHANGED, humanoperator=$HUMANOPERATOR_CHANGED, shared=$SHARED_CHANGED"
71+
1172
build:
73+
needs: detect-changes
1274
runs-on: ubuntu-latest
75+
env:
76+
BUILD_APP: ${{ needs.detect-changes.outputs.app_changed == 'true' || needs.detect-changes.outputs.shared_changed == 'true' }}
77+
BUILD_HUMANOPERATOR: ${{ needs.detect-changes.outputs.humanoperator_changed == 'true' || needs.detect-changes.outputs.shared_changed == 'true' }}
1378

1479
steps:
1580
- name: Checkout code
16-
uses: actions/checkout@v3
81+
uses: actions/checkout@v4
1782

18-
- name: Set up JDK
19-
uses: actions/setup-java@v3
83+
- name: Set up JDK 17
84+
uses: actions/setup-java@v4
2085
with:
2186
java-version: '17'
2287
distribution: 'temurin'
2388
cache: gradle
2489

90+
- name: Decode google-services.json (app)
91+
env:
92+
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON_APP }}
93+
run: printf '%s' "$GOOGLE_SERVICES_JSON" > app/google-services.json
94+
95+
- name: Decode google-services.json (humanoperator)
96+
env:
97+
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON_HUMANOPERATOR }}
98+
run: printf '%s' "$GOOGLE_SERVICES_JSON" > humanoperator/google-services.json
99+
100+
- name: Create local.properties
101+
run: echo "sdk.dir=$ANDROID_HOME" > local.properties
102+
103+
- name: Fix gradle.properties for CI
104+
run: |
105+
sed -i '/org.gradle.java.home=/d' gradle.properties
106+
sed -i 's/org.gradle.jvmargs=.*/org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m/' gradle.properties
107+
sed -i 's/kotlin.daemon.jvmargs=.*/kotlin.daemon.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m/' gradle.properties
108+
25109
- name: Grant execute permission for gradlew
26110
run: chmod +x gradlew
27111

28-
- name: Build with Gradle
29-
run: ./gradlew assembleRelease
112+
- name: Build app module (debug)
113+
if: env.BUILD_APP == 'true'
114+
run: ./gradlew :app:assembleDebug
30115

31-
- name: Upload APK
116+
- name: Build humanoperator module (debug)
117+
if: env.BUILD_HUMANOPERATOR == 'true'
118+
run: ./gradlew :humanoperator:assembleDebug
119+
120+
- name: Upload app APK
121+
if: env.BUILD_APP == 'true'
32122
uses: actions/upload-artifact@v4
33123
with:
34-
name: app-release
35-
path: app/build/outputs/apk/release/app-release-unsigned.apk
124+
name: app-debug
125+
path: app/build/outputs/apk/debug/app-debug.apk
126+
127+
- name: Upload humanoperator APK
128+
if: env.BUILD_HUMANOPERATOR == 'true'
129+
uses: actions/upload-artifact@v4
130+
with:
131+
name: humanoperator-debug
132+
path: humanoperator/build/outputs/apk/debug/humanoperator-debug.apk
133+
134+
- name: Build summary
135+
run: |
136+
echo "### Build Summary" >> $GITHUB_STEP_SUMMARY
137+
echo "| Module | Built |" >> $GITHUB_STEP_SUMMARY
138+
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
139+
echo "| app | ${{ env.BUILD_APP }} |" >> $GITHUB_STEP_SUMMARY
140+
echo "| humanoperator | ${{ env.BUILD_HUMANOPERATOR }} |" >> $GITHUB_STEP_SUMMARY

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.maxTokenizationLineLength": 20000
3+
}

app/build.gradle.kts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ plugins {
55
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
66
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
77
id("kotlin-parcelize")
8+
id("com.google.gms.google-services")
9+
}
10+
11+
// Redirect build output to C: drive (NTFS) to avoid corrupted ExFAT build cache
12+
if (System.getenv("CI") == null) {
13+
layout.buildDirectory = file("C:/GradleBuild/app")
814
}
915

1016
android {
1117
namespace = "com.google.ai.sample"
1218
compileSdk = 35
1319

1420
defaultConfig {
15-
applicationId = "com.google.ai.sample"
21+
applicationId = "io.github.android_poweruser"
1622
minSdk = 26
1723
targetSdk = 35
1824
versionCode = 1
@@ -96,4 +102,14 @@ dependencies {
96102

97103
// Camera Core to potentially fix missing JNI lib issue
98104
implementation("androidx.camera:camera-core:1.4.0")
105+
106+
// WebRTC
107+
implementation("io.getstream:stream-webrtc-android:1.1.1")
108+
109+
// WebSocket for signaling
110+
implementation("com.squareup.okhttp3:okhttp:4.12.0")
111+
112+
// Firebase
113+
implementation(platform("com.google.firebase:firebase-bom:32.7.2"))
114+
implementation("com.google.firebase:firebase-database")
99115
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
4+
<uses-permission android:name="android.permission.INTERNET" />
45
<!-- Storage permissions -->
56
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
67
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

app/src/main/kotlin/com/google/ai/sample/ApiKeyDialog.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ fun ApiKeyDialog(
8888
ApiProvider.GOOGLE -> "https://makersuite.google.com/app/apikey"
8989
ApiProvider.CEREBRAS -> "https://cloud.cerebras.ai/"
9090
ApiProvider.VERCEL -> "https://vercel.com/ai-gateway"
91-
ApiProvider.HUMAN_EXPERT -> return@Button // No API key needed
91+
ApiProvider.HUMAN_EXPERT -> return@Button
9292
}
9393
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
9494
context.startActivity(intent)

app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.google.ai.client.generativeai.GenerativeModel
99
import com.google.ai.client.generativeai.type.generationConfig
1010
import com.google.ai.sample.feature.live.LiveApiManager
1111
import com.google.ai.sample.feature.multimodal.PhotoReasoningViewModel
12+
import com.google.ai.sample.util.GenerationSettingsPreferences
1213

1314
// Model options
1415
enum class ApiProvider {
@@ -44,24 +45,32 @@ enum class ModelOption(
4445
"https://huggingface.co/na5h13/gemma-3n-E4B-it-litert-lm/resolve/main/gemma-3n-E4B-it-int4.litertlm?download=true",
4546
"4.92 GB"
4647
),
47-
HUMAN_EXPERT("Human Expert", "human-expert", ApiProvider.HUMAN_EXPERT)
48+
HUMAN_EXPERT("Human Expert", "human-expert", ApiProvider.HUMAN_EXPERT);
49+
50+
/** Whether this model supports TopK/TopP/Temperature settings */
51+
val supportsGenerationSettings: Boolean
52+
get() = this != HUMAN_EXPERT
4853
}
4954

5055
val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
5156
override fun <T : ViewModel> create(
5257
viewModelClass: Class<T>,
5358
extras: CreationExtras
5459
): T {
55-
val config = generationConfig {
56-
temperature = 0.0f
57-
}
58-
5960
// Get the application context from extras
6061
val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
62+
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
63+
64+
// Load per-model generation settings
65+
val genSettings = GenerationSettingsPreferences.loadSettings(application.applicationContext, currentModel.modelName)
66+
val config = generationConfig {
67+
temperature = genSettings.temperature
68+
topP = genSettings.topP
69+
topK = genSettings.topK
70+
}
6171

6272
// Get the API key from MainActivity
6373
val mainActivity = MainActivity.getInstance()
64-
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
6574
val apiKey = if (currentModel == ModelOption.GEMMA_3N_E4B_IT || currentModel == ModelOption.HUMAN_EXPERT) {
6675
"offline-no-key-needed" // Dummy key for offline/human expert models
6776
} else {
@@ -75,8 +84,6 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
7584
return with(viewModelClass) {
7685
when {
7786
isAssignableFrom(PhotoReasoningViewModel::class.java) -> {
78-
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
79-
8087
if (currentModel.modelName.contains("live")) {
8188
// Live API models
8289
val liveApiManager = LiveApiManager(apiKey, currentModel.modelName)

app/src/main/kotlin/com/google/ai/sample/MainActivity.kt

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ class MainActivity : ComponentActivity() {
120120
// MediaProjection
121121
private lateinit var mediaProjectionManager: MediaProjectionManager
122122
private lateinit var mediaProjectionLauncher: ActivityResultLauncher<Intent>
123+
private lateinit var webRtcMediaProjectionLauncher: ActivityResultLauncher<Intent>
123124

124125
private var currentScreenInfoForScreenshot: String? = null
125126

126127
private lateinit var navController: NavHostController
127128
private var isProcessingExplicitScreenshotRequest: Boolean = false
128129
private var onMediaProjectionPermissionGranted: (() -> Unit)? = null
130+
private var onWebRtcMediaProjectionResult: ((Int, Intent) -> Unit)? = null
129131

130132
private val screenshotRequestHandler = object : BroadcastReceiver() {
131133
override fun onReceive(context: Context?, intent: Intent?) {
@@ -187,15 +189,28 @@ class MainActivity : ComponentActivity() {
187189
// This should be guaranteed by its placement in onCreate.
188190
if (!::mediaProjectionManager.isInitialized) {
189191
Log.e(TAG, "requestMediaProjectionPermission: mediaProjectionManager not initialized!")
190-
// Optionally, initialize it here as a fallback, though it indicates an issue with onCreate ordering
191-
// mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
192-
// Toast.makeText(this, "Error: Projection manager not ready. Please try again.", Toast.LENGTH_SHORT).show()
193192
return
194193
}
195194
val intent = mediaProjectionManager.createScreenCaptureIntent()
196195
mediaProjectionLauncher.launch(intent)
197196
}
198197

198+
/**
199+
* Request a fresh MediaProjection permission specifically for WebRTC (Human Expert).
200+
* This does NOT start ScreenCaptureService - the result is passed directly to the callback.
201+
*/
202+
fun requestMediaProjectionForWebRTC(onResult: (Int, Intent) -> Unit) {
203+
Log.d(TAG, "Requesting MediaProjection permission for WebRTC")
204+
onWebRtcMediaProjectionResult = onResult
205+
206+
if (!::mediaProjectionManager.isInitialized) {
207+
Log.e(TAG, "requestMediaProjectionForWebRTC: mediaProjectionManager not initialized!")
208+
return
209+
}
210+
val intent = mediaProjectionManager.createScreenCaptureIntent()
211+
webRtcMediaProjectionLauncher.launch(intent)
212+
}
213+
199214
fun takeAdditionalScreenshot() {
200215
if (ScreenCaptureService.isRunning()) {
201216
Log.d(TAG, "MainActivity: Instructing ScreenCaptureService to take an additional screenshot.")
@@ -286,7 +301,7 @@ class MainActivity : ComponentActivity() {
286301

287302
when (currentTrialState) {
288303
TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED -> {
289-
trialInfoMessage = "Your 30-minute trial period has ended. Please subscribe to the app to continue using it."
304+
trialInfoMessage = "Please support the development of the app so that you can continue using it \uD83C\uDF89"
290305
showTrialInfoDialog = true
291306
Log.d(TAG, "updateTrialState: Set message to \'$trialInfoMessage\', showTrialInfoDialog = true (EXPIRED)")
292307
}
@@ -444,6 +459,10 @@ class MainActivity : ComponentActivity() {
444459
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
445460
val shouldTakeScreenshotOnThisStart = this@MainActivity.isProcessingExplicitScreenshotRequest
446461
Log.i(TAG, "MediaProjection permission granted. Starting ScreenCaptureService. Explicit request: $shouldTakeScreenshotOnThisStart")
462+
463+
// Notify ViewModel about the permission grant (for Human Expert WebRTC)
464+
photoReasoningViewModel?.onMediaProjectionPermissionGranted(result.resultCode, result.data!!)
465+
447466
val serviceIntent = Intent(this, ScreenCaptureService::class.java).apply {
448467
action = ScreenCaptureService.ACTION_START_CAPTURE
449468
putExtra(ScreenCaptureService.EXTRA_RESULT_CODE, result.resultCode)
@@ -487,6 +506,21 @@ class MainActivity : ComponentActivity() {
487506
}
488507
}
489508

509+
// Separate WebRTC MediaProjection launcher - does NOT start ScreenCaptureService
510+
webRtcMediaProjectionLauncher = registerForActivityResult(
511+
ActivityResultContracts.StartActivityForResult()
512+
) { result ->
513+
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
514+
Log.i(TAG, "WebRTC MediaProjection permission granted.")
515+
onWebRtcMediaProjectionResult?.invoke(result.resultCode, result.data!!)
516+
onWebRtcMediaProjectionResult = null
517+
} else {
518+
Log.w(TAG, "WebRTC MediaProjection permission denied.")
519+
Toast.makeText(this, "Screen capture permission denied", Toast.LENGTH_SHORT).show()
520+
onWebRtcMediaProjectionResult = null
521+
}
522+
}
523+
490524
// Keyboard visibility listener
491525
val rootView = findViewById<View>(android.R.id.content)
492526
onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
@@ -1222,7 +1256,7 @@ fun TrialExpiredDialog(
12221256
)
12231257
Spacer(modifier = Modifier.height(16.dp))
12241258
Text(
1225-
text = "Your 7-day trial period has ended. Please subscribe to the app to continue using it.",
1259+
text = "Please support the development of the app so that you can continue using it \uD83C\uDF89",
12261260
style = MaterialTheme.typography.bodyMedium,
12271261
modifier = Modifier.align(Alignment.CenterHorizontally)
12281262
)

0 commit comments

Comments
 (0)