Compare commits

..
6 Commits
Author SHA1 Message Date
GGBandClaude Sonnet 5 3412357526 Fix Menu popup, DPAD room-list navigation, and cleartext leftover
- Menu key: WebView refuses to forward Android "system" keycodes
  (KEYCODE_MENU among them) to page JS, confirmed via diagnostic
  logging (downConsumed=false even though the key reaches
  dispatchKeyEvent). Relay it as KEYCODE_X instead - room.js on the
  website side treats 'x' as an alias for the ContextMenu trigger.
- DPAD center/up/down/media keys are now only remapped inside a room
  (shouldIntercept() gates on isRoomList()) - the room-list page uses
  plain ArrowUp/ArrowDown/Enter and never moved focus when those keys
  arrived pre-remapped as Space/P/N.
- Removed a leftover cleartextTrafficPermitted="true" left over from
  local LAN debugging against a docker instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-17 19:55:12 +02:00
GGBandClaude Sonnet 5 5a8f593b98 Add remote-control key mapping, fullscreen tracking, back-button navigation
dispatchKeyEvent() intercepts DPAD/media keys and re-dispatches real
KeyEvents into the WebView per the design spec's mapping table:
- DPAD center / Play-Pause -> Space
- DPAD Left/Right tap -> Arrow (5s seek); held -> Shift+Arrow (30s
  seek), throttled to ~1 event/200ms since Android's own key-repeat
  rate would otherwise flood the room's sync socket
- DPAD Up/Down + media Prev/Next -> P/N (title change, not
  ArrowUp/Down - those are explicit no-ops in the website's TV mode
  since volume is TV/AVR-controlled)
- Back: room list -> no-op; in a room + fullscreen -> Escape
  (browser exits fullscreen natively); in a room + not fullscreen ->
  navigate to /tv (native WebView navigation, not a website reaction)
- Menu (KEYCODE_MENU) deliberately NOT intercepted - passed through
  unchanged, matching the already-confirmed-working raw-browser test

Fullscreen state comes from WebChromeClient.onShowCustomView/
onHideCustomView (needed for HTML5 video fullscreen support anyway).
Current-page tracking comes from WebViewClient.onPageFinished.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-16 22:45:09 +02:00
GGBandClaude Sonnet 5 e3f9a07a67 Add app icon and TV banner
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-16 22:44:12 +02:00
GGBandClaude Sonnet 5 1b6ed31683 Correct plan's JAVA_HOME guidance to the actual working JDK
The plan assumed Android Studio's bundled JBR (JDK 25) would work
for Gradle 8.7 - it doesn't (crashes at startup). Task 1's actual
build used a separately-installed Temurin 21 instead, scoped
per-command to this project only. Updating the remaining tasks'
commands to match what was actually verified working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-16 22:43:39 +02:00
GGBandClaude Sonnet 5 379ae78bbf Bootstrap KwabaTV Gradle project, minimal WebView shell
Gradle wrapper pinned to 8.7, AGP 8.5.2, compileSdk 34/minSdk 21/
targetSdk 34 - the tested combination from the original app.
MainActivity is a placeholder that just loads BuildConfig.WEBSITE_URL
(the new /tv room-list route); the real key-mapping logic is a
separate follow-up commit.

Note for future builds on this machine: Android Studio's bundled JBR
is JDK 25, which Gradle 8.7 cannot run on at all (crashes at startup
with a bare version-string error). Built and verified with a
separately-installed Temurin JDK 21 instead
(C:\Users\GGB\jdks\jdk-21.0.12.1+1) - set JAVA_HOME to that for any
Gradle command in this repo, not the Android Studio JBR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-16 22:40:19 +02:00
GGBandClaude Sonnet 5 515bab293f Add KwabaTV rebuild implementation plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHgWjmEJXsJEi1EVAoTp4K
2026-09-16 22:34:07 +02:00
16 changed files with 1133 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
*.iml
.gradle/
local.properties
.idea/
.DS_Store
build/
captures/
.externalNativeBuild/
.cxx/
*.apk
+37
View File
@@ -0,0 +1,37 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "de.ggb.kwabatv"
compileSdk = 34
defaultConfig {
applicationId = "de.ggb.kwabatv"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
buildConfigField("String", "WEBSITE_URL", "\"https://kwaba.whodovoodoo.pasteaf.de/tv\"")
}
buildTypes {
release {
isMinifyEnabled = false
}
}
buildFeatures {
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
+27
View File
@@ -0,0 +1,27 @@
<?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-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher_art"
android:banner="@drawable/banner_tv_art"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,149 @@
package de.ggb.kwabatv
import android.app.Activity
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.view.KeyEvent
import android.view.View
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
/**
* Reine WebView-Huelle. Fernbedienungstasten werden in dispatchKeyEvent()
* abgefangen und als echte (nicht per JS simulierte) KeyEvents an das
* WebView weitergereicht - Chromium verlangt fuer die Fullscreen-API und
* den einmaligen Autoplay-Ton-Klick eine vertrauenswuerdige Nutzereingabe,
* die ein simuliertes KeyboardEvent nicht liefert.
*
* KEYCODE_MENU wird NICHT als KEYCODE_MENU ins WebView weitergereicht,
* sondern als KEYCODE_X. Grund (per Diagnose-Log bestaetigt): Androids
* WebView.dispatchKeyEvent() liefert "System"-Keycodes (KEYCODE_MENU,
* HOME, BACK, CALL, VOLUME_*, ...) grundsaetzlich nicht an den Seiteninhalt
* aus - sendKey(KEYCODE_MENU) kam zwar in dispatchKeyEvent() an, aber
* webView.dispatchKeyEvent() meldete downConsumed=false/upConsumed=false
* und JS bekam weder keydown- noch contextmenu-Events. Ein gewoehnlicher
* Buchstaben-Keycode wie KEYCODE_P (fuer DPAD_UP) wird dagegen anstandslos
* durchgereicht (downConsumed=true). Die Website (room.js) behandelt 'x'
* deshalb als Alias fuer die 'ContextMenu'-Taste, ausschliesslich fuer
* diesen App-Anwendungsfall.
*
* DPAD Center/Up/Down (und die entsprechenden Medientasten) werden NUR
* innerhalb eines Raums remapped (Space/P/N - siehe Design-Spec). Auf der
* Raumliste (/tv) haben sie dort keine Bedeutung: die Raumliste hat ihr
* eigenes Tastaturschema (ArrowUp/ArrowDown/Enter, siehe tv-rooms.js auf
* der Website) und muss die echten, unveraenderten Tasten bekommen, sonst
* bewegt sich der Fokus zwischen den Raum-Kacheln nie (genau der Fehler,
* der beim ersten Hardware-Test auftrat: die Raumliste laed, aber keine
* Taste tut etwas, weil DPAD_DOWN/CENTER als 'n'/Space statt als
* ArrowDown/Enter ankamen).
*/
class MainActivity : Activity() {
private lateinit var webView: WebView
private var fullscreenView: View? = null
private var currentPath: String = "/tv"
private var lastThrottledSeekAt = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
currentPath = Uri.parse(url).path ?: "/tv"
}
}
webView.webChromeClient = object : WebChromeClient() {
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
fullscreenView = view
}
override fun onHideCustomView() {
fullscreenView = null
}
override fun onPermissionRequest(request: PermissionRequest) {
// Kein Mikrofon/Kamera im TV-Modus - Fire-TV-Sticks haben
// ohnehin meist keine Kamera, siehe Fixed Product Decisions.
request.deny()
}
}
webView.loadUrl(BuildConfig.WEBSITE_URL)
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
private fun isFullscreen() = fullscreenView != null
private fun isRoomList() = currentPath == "/tv"
private fun sendKey(keyCode: Int, shift: Boolean = false) {
val metaState = if (shift) KeyEvent.META_SHIFT_ON else 0
val now = SystemClock.uptimeMillis()
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, metaState))
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, metaState))
}
/** Tippen = 5s-Schritt (kein Shift), gehalten = 30s-Schritt (mit Shift),
* gedrosselt auf ~1 Ereignis pro 200ms - Androids eigene Tastenwieder-
* holung feuert schneller als das und wuerde sonst den Sync-Socket des
* Raums mit Sprungbefehlen ueberfluten, die an alle Zuschauer gehen. */
private fun handleSeek(keyCode: Int, isRepeat: Boolean) {
if (!isRepeat) { sendKey(keyCode); return }
val now = SystemClock.uptimeMillis()
if (now - lastThrottledSeekAt < SEEK_THROTTLE_MS) return
lastThrottledSeekAt = now
sendKey(keyCode, shift = true)
}
/** Ob dispatchKeyEvent diese Taste ueberhaupt anfasst - fuer ACTION_DOWN
* und ACTION_UP gleich ausgewertet, damit nie nur die eine Haelfte eines
* Tastendrucks abgefangen wird (sonst haengt Chromiums interner
* Tastenstatus fest, weil ein Keydown ohne passendes Keyup ankaeme). */
private fun shouldIntercept(keyCode: Int): Boolean = when (keyCode) {
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MENU -> true
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_MEDIA_NEXT -> !isRoomList()
else -> false
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (!shouldIntercept(event.keyCode)) return super.dispatchKeyEvent(event)
if (event.action != KeyEvent.ACTION_DOWN) return true // ACTION_UP der abgefangenen Taste verschlucken, ACTION_DOWN hat schon reagiert
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> sendKey(KeyEvent.KEYCODE_SPACE)
KeyEvent.KEYCODE_DPAD_LEFT -> handleSeek(KeyEvent.KEYCODE_DPAD_LEFT, event.repeatCount > 0)
KeyEvent.KEYCODE_DPAD_RIGHT -> handleSeek(KeyEvent.KEYCODE_DPAD_RIGHT, event.repeatCount > 0)
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS -> sendKey(KeyEvent.KEYCODE_P)
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT -> sendKey(KeyEvent.KEYCODE_N)
KeyEvent.KEYCODE_MENU -> sendKey(KeyEvent.KEYCODE_X)
KeyEvent.KEYCODE_BACK -> when {
isRoomList() -> { /* verschluckt, keine weitere Aktion */ }
isFullscreen() -> sendKey(KeyEvent.KEYCODE_ESCAPE)
else -> webView.loadUrl(BuildConfig.WEBSITE_URL)
}
}
return true
}
companion object {
private const val SEEK_THROTTLE_MS = 200L
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">KwabaTV</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
+4
View File
@@ -0,0 +1,4 @@
plugins {
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
@@ -0,0 +1,528 @@
# KwabaTV Rebuild Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rebuild the deleted `KwabaTV` Android app as a thin WebView shell pointed at the new `/tv` room list, forwarding real remote-control key events to the website's TV-mode key handling.
**Architecture:** A single-Activity app (`MainActivity`, plain `android.app.Activity`, no AndroidX — a WebView-only app needs none of it) that loads `BuildConfig.WEBSITE_URL` and intercepts DPAD/media-key `KeyEvent`s in `dispatchKeyEvent()`, re-dispatching real (not JS-simulated) `KeyEvent`s into the `WebView` with the mapping from the design spec. Fullscreen state comes from `WebChromeClient.onShowCustomView`/`onHideCustomView`; current-page tracking comes from `WebViewClient.onPageFinished`.
**Tech Stack:** Kotlin, Android Gradle Plugin 8.5.2, Gradle 8.7, `compileSdk 34` / `minSdk 21` / `targetSdk 34` — the same combination documented as tested in the original (deleted) app. No test framework: this is a 4-file native app with no business logic to unit-test; verification is real `gradlew assembleDebug` builds (compiler-checked, unlike anything in this session's website work) plus manual sideload testing on the user's real Fire TV hardware.
## Global Constraints
- Package `de.ggb.kwabatv`, `compileSdk 34` / `minSdk 21` / `targetSdk 34`, AGP 8.5.2, Gradle 8.7 — from the design spec, carried over from the original app's tested configuration.
- `BuildConfig.WEBSITE_URL = "https://kwaba.whodovoodoo.pasteaf.de/tv"` — the room list, not a specific room (per the design spec's rejection of a baked-in room ID).
- No AndroidX, no Jetpack Compose, no ExoPlayer, no other dependencies beyond the Android SDK + Kotlin stdlib — this app is a WebView shell with no other UI, per the original app's minimal-dependency approach (confirmed still appropriate: the website already handles all playback/UI, WebView rendering is sufficient).
- Every remote-control key must be forwarded via `webView.dispatchKeyEvent()` with a real `KeyEvent` object — never `evaluateJavascript()`-simulated events — because Chromium requires a trusted user gesture for autoplay-unlock and the Fullscreen API.
- `KEYCODE_MENU` is passed through UNCHANGED (not intercepted/reconstructed) — the user already confirmed a real Fire TV remote's Menu button, received by a plain browser tab on the actual TV mode page, opens the popup. Passing the event through untouched (rather than intercepting and reconstructing it) is the closest possible match to that already-proven behavior.
- `KEYCODE_BACK`'s 3-state behavior (room list → no-op; in a room + fullscreen → send `Escape`; in a room + not fullscreen → navigate to `/tv`) from the design spec — implemented natively, not by relying on any website-side reaction to a key event.
- Held DPAD Left/Right must be throttled to roughly one synthetic event per 200ms even though Android's own hardware key-repeat fires faster — un-throttled, this would flood the room's sync socket with seek commands broadcast to every viewer.
- Debug builds (`gradlew assembleDebug`) are used throughout this plan for iteration and hardware sideload testing — Android auto-signs debug builds, no keystore needed, and Fire TV sideloading doesn't care whether an APK is a "debug" or "release" build. A signed release build (via Android Studio's `Generate Signed Bundle / APK` wizard, per the original app's own documented process) is a separate, later step this plan doesn't cover — out of scope until the app is otherwise working.
- `JAVA_HOME` for all Gradle commands in this plan: `C:\Users\GGB\jdks\jdk-21.0.12.1+1` (a separately-installed Temurin 21, added specifically for this project). **Not** Android Studio's bundled JBR (JDK 25) — Gradle 8.7 crashes at startup on JDK 25 (`What went wrong: 25.0.3`, a bare version-string error, not a real message). Set `JAVA_HOME` inline per-command (e.g. `JAVA_HOME="C:\Users\GGB\jdks\jdk-21.0.12.1+1" ./gradlew.bat ...`) — scoped to this project's Gradle invocations only, never as a system-wide or user-profile environment variable, so Android Studio's own JBR stays what the IDE itself uses.
- Reuse the branding assets already in `C:\Projekte\kwaba\Claude outputs\` (`ic_launcher_art.png`, 512×512; `banner_tv_art.png`, 640×360) rather than regenerating them.
---
### Task 1: Gradle project scaffold + first successful build
**Files:**
- Create: `settings.gradle.kts`
- Create: `build.gradle.kts` (root)
- Create: `gradle.properties`
- Create: `app/build.gradle.kts`
- Create: `app/src/main/AndroidManifest.xml`
- Create: `app/src/main/res/xml/network_security_config.xml`
- Create: `app/src/main/res/values/strings.xml`
- Create: `app/src/main/java/de/ggb/kwabatv/MainActivity.kt` (minimal placeholder for this task — just loads the URL, no key handling yet; Task 3 replaces its contents)
- Create (generated by the Gradle wrapper bootstrap, not hand-authored): `gradlew`, `gradlew.bat`, `gradle/wrapper/gradle-wrapper.properties`, `gradle/wrapper/gradle-wrapper.jar`
**Interfaces:**
- Consumes: nothing (first task).
- Produces: a `gradlew` wrapper pinned to Gradle 8.7 that every later task's build commands use; `BuildConfig.WEBSITE_URL` (used by Task 3's real `MainActivity`).
- [ ] **Step 1: Write `settings.gradle.kts`**
```kotlin
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "KwabaTV"
include(":app")
```
- [ ] **Step 2: Write the root `build.gradle.kts`**
```kotlin
plugins {
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
```
- [ ] **Step 3: Write `gradle.properties`**
```
org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=false
kotlin.code.style=official
```
(`android.useAndroidX=false` is deliberate — this app has no AndroidX dependency to begin with, per Global Constraints, so nothing needs the AndroidX artifact namespace.)
- [ ] **Step 4: Write `app/build.gradle.kts`**
```kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "de.ggb.kwabatv"
compileSdk = 34
defaultConfig {
applicationId = "de.ggb.kwabatv"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
buildConfigField("String", "WEBSITE_URL", "\"https://kwaba.whodovoodoo.pasteaf.de/tv\"")
}
buildTypes {
release {
isMinifyEnabled = false
}
}
buildFeatures {
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
```
- [ ] **Step 5: Write the manifest**
Write `app/src/main/AndroidManifest.xml`:
```xml
<?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-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
```
(`android:icon`/`android:banner` are deliberately omitted here — Task 2 adds the real branding drawables and those two attributes together, so this task's manifest never references a resource that doesn't exist yet. The `android.hardware.touchscreen` feature declared as NOT required is what lets Fire TV / the Amazon Appstore accept this as a TV app rather than rejecting it as phone-only.)
- [ ] **Step 6: Write the network security config**
Write `app/src/main/res/xml/network_security_config.xml`:
```xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
```
(System trust anchors only — the domain runs Let's Encrypt, a public CA, so no certificate pinning is needed.)
- [ ] **Step 7: Write the app name string**
Write `app/src/main/res/values/strings.xml`:
```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">KwabaTV</string>
</resources>
```
- [ ] **Step 8: Write a minimal placeholder `MainActivity.kt`**
Write `app/src/main/java/de/ggb/kwabatv/MainActivity.kt`:
```kotlin
package de.ggb.kwabatv
import android.app.Activity
import android.os.Bundle
import android.webkit.WebView
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.loadUrl(BuildConfig.WEBSITE_URL)
}
}
```
(Deliberately minimal — this task's only goal is proving the whole toolchain builds and installs. Task 3 replaces this file's contents with the full key-mapping/fullscreen/back-button logic.)
- [ ] **Step 9: Bootstrap the Gradle wrapper**
There is no `gradle` binary on this machine's PATH and no pre-existing wrapper to reuse (the original app's folder was deleted). Download the real Gradle 8.7 distribution once, use it to generate the wrapper files for this project, then discard the downloaded copy — every later command in this plan uses the generated `./gradlew`, never a system-wide Gradle.
```powershell
cd C:\Projekte\kwabatv
Invoke-WebRequest -Uri "https://services.gradle.org/distributions/gradle-8.7-bin.zip" -OutFile "$env:TEMP\gradle-8.7-bin.zip"
Expand-Archive -Path "$env:TEMP\gradle-8.7-bin.zip" -DestinationPath "$env:TEMP\gradle-8.7" -Force
$env:JAVA_HOME = "C:\Users\GGB\jdks\jdk-21.0.12.1+1"
& "$env:TEMP\gradle-8.7\gradle-8.7\bin\gradle.bat" wrapper --gradle-version 8.7 --distribution-type bin
Remove-Item -Recurse -Force "$env:TEMP\gradle-8.7", "$env:TEMP\gradle-8.7-bin.zip"
```
Expected: `gradlew`, `gradlew.bat`, and `gradle/wrapper/gradle-wrapper.properties` + `gradle-wrapper.jar` now exist in `C:\Projekte\kwabatv`.
- [ ] **Step 10: First build**
```powershell
cd C:\Projekte\kwabatv
$env:JAVA_HOME = "C:\Users\GGB\jdks\jdk-21.0.12.1+1"
.\gradlew.bat assembleDebug
```
Expected: `BUILD SUCCESSFUL`, and `app\build\outputs\apk\debug\app-debug.apk` exists. This is a real compiler-checked build (unlike anything in this session's prior website work, which had no equivalent local feedback loop) — a genuine failure here means a real mistake in the files above, not an environment limitation.
If Android SDK components are missing (a fresh SDK install often lacks the exact `compileSdk 34` platform or build-tools), the error will name exactly what's missing; install it via `sdkmanager` (under the SDK's `cmdline-tools` if present) or open the project once in Android Studio and let it prompt to install the missing SDK components, then retry.
- [ ] **Step 11: Commit**
```bash
cd C:\Projekte\kwabatv
git add settings.gradle.kts build.gradle.kts gradle.properties gradlew gradlew.bat gradle/ app/build.gradle.kts app/src/main/AndroidManifest.xml app/src/main/res/xml/network_security_config.xml app/src/main/res/values/strings.xml app/src/main/java/de/ggb/kwabatv/MainActivity.kt
git commit -m "Bootstrap KwabaTV Gradle project, minimal WebView shell"
```
---
### Task 2: Branding assets
**Files:**
- Create: `app/src/main/res/drawable/ic_launcher_art.png` (copied from `C:\Projekte\kwaba\Claude outputs\ic_launcher_art.png`)
- Create: `app/src/main/res/drawable/banner_tv_art.png` (copied from `C:\Projekte\kwaba\Claude outputs\banner_tv_art.png`)
- Modify: `app/src/main/AndroidManifest.xml` (add `android:icon`/`android:banner`)
**Interfaces:**
- Consumes: Task 1's manifest and successful build.
- Produces: nothing later tasks depend on programmatically — purely visual.
- [ ] **Step 1: Copy the assets**
```powershell
Copy-Item "C:\Projekte\kwaba\Claude outputs\ic_launcher_art.png" "C:\Projekte\kwabatv\app\src\main\res\drawable\ic_launcher_art.png"
Copy-Item "C:\Projekte\kwaba\Claude outputs\banner_tv_art.png" "C:\Projekte\kwabatv\app\src\main\res\drawable\banner_tv_art.png"
```
- [ ] **Step 2: Wire them into the manifest**
In `app/src/main/AndroidManifest.xml`, the `<application>` tag currently reads:
```xml
<application
android:allowBackup="true"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
```
Change to:
```xml
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher_art"
android:banner="@drawable/banner_tv_art"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
```
- [ ] **Step 3: Rebuild to confirm the resources compile**
```powershell
cd C:\Projekte\kwabatv
$env:JAVA_HOME = "C:\Users\GGB\jdks\jdk-21.0.12.1+1"
.\gradlew.bat assembleDebug
```
Expected: `BUILD SUCCESSFUL`. A resource-linking error here would name the exact bad reference (e.g. a typo'd drawable name) — fix and retry rather than guessing.
- [ ] **Step 4: Commit**
```bash
cd C:\Projekte\kwabatv
git add app/src/main/res/drawable/ic_launcher_art.png app/src/main/res/drawable/banner_tv_art.png app/src/main/AndroidManifest.xml
git commit -m "Add app icon and TV banner"
```
---
### Task 3: Key mapping, fullscreen tracking, back-button navigation
**Files:**
- Modify: `app/src/main/java/de/ggb/kwabatv/MainActivity.kt` (replace the placeholder from Task 1 entirely)
**Interfaces:**
- Consumes: `BuildConfig.WEBSITE_URL` (Task 1).
- Produces: nothing later tasks depend on programmatically — Task 4 is manual hardware verification of this task's behavior.
- [ ] **Step 1: Write the full `MainActivity.kt`**
Replace the entire contents of `app/src/main/java/de/ggb/kwabatv/MainActivity.kt` with:
```kotlin
package de.ggb.kwabatv
import android.app.Activity
import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.view.KeyEvent
import android.view.View
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
/**
* Reine WebView-Huelle. Fernbedienungstasten werden in dispatchKeyEvent()
* abgefangen und als echte (nicht per JS simulierte) KeyEvents an das
* WebView weitergereicht - Chromium verlangt fuer die Fullscreen-API und
* den einmaligen Autoplay-Ton-Klick eine vertrauenswuerdige Nutzereingabe,
* die ein simuliertes KeyboardEvent nicht liefert.
*
* KEYCODE_MENU wird bewusst NICHT abgefangen: das Oeffnen des TV-Menue-
* Popups wurde bereits gegen die echte Website in einem normalen Browser
* auf echter Fire-TV-Hardware bestaetigt, ohne dass diese App beteiligt
* war - unveraendertes Durchreichen ist die naechste Annaeherung an genau
* dieses schon erprobte Verhalten.
*/
class MainActivity : Activity() {
private lateinit var webView: WebView
private var fullscreenView: View? = null
private var currentPath: String = "/tv"
private var lastThrottledSeekAt = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
currentPath = Uri.parse(url).path ?: "/tv"
}
}
webView.webChromeClient = object : WebChromeClient() {
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
fullscreenView = view
}
override fun onHideCustomView() {
fullscreenView = null
}
override fun onPermissionRequest(request: PermissionRequest) {
// Kein Mikrofon/Kamera im TV-Modus - Fire-TV-Sticks haben
// ohnehin meist keine Kamera, siehe Fixed Product Decisions.
request.deny()
}
}
webView.loadUrl(BuildConfig.WEBSITE_URL)
}
override fun onDestroy() {
webView.destroy()
super.onDestroy()
}
private fun isFullscreen() = fullscreenView != null
private fun isRoomList() = currentPath == "/tv"
private fun sendKey(keyCode: Int, shift: Boolean = false) {
val metaState = if (shift) KeyEvent.META_SHIFT_ON else 0
val now = SystemClock.uptimeMillis()
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, metaState))
webView.dispatchKeyEvent(KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, metaState))
}
/** Tippen = 5s-Schritt (kein Shift), gehalten = 30s-Schritt (mit Shift),
* gedrosselt auf ~1 Ereignis pro 200ms - Androids eigene Tastenwieder-
* holung feuert schneller als das und wuerde sonst den Sync-Socket des
* Raums mit Sprungbefehlen ueberfluten, die an alle Zuschauer gehen. */
private fun handleSeek(keyCode: Int, isRepeat: Boolean) {
if (!isRepeat) { sendKey(keyCode); return }
val now = SystemClock.uptimeMillis()
if (now - lastThrottledSeekAt < SEEK_THROTTLE_MS) return
lastThrottledSeekAt = now
sendKey(keyCode, shift = true)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.action != KeyEvent.ACTION_DOWN) {
return when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_MEDIA_NEXT,
KeyEvent.KEYCODE_BACK -> true // ACTION_UP derselben Tasten verschlucken, wir haben schon auf ACTION_DOWN reagiert
else -> super.dispatchKeyEvent(event)
}
}
return when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
sendKey(KeyEvent.KEYCODE_SPACE)
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
handleSeek(KeyEvent.KEYCODE_DPAD_LEFT, event.repeatCount > 0)
true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
handleSeek(KeyEvent.KEYCODE_DPAD_RIGHT, event.repeatCount > 0)
true
}
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
sendKey(KeyEvent.KEYCODE_P)
true
}
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT -> {
sendKey(KeyEvent.KEYCODE_N)
true
}
KeyEvent.KEYCODE_BACK -> {
when {
isRoomList() -> { /* verschluckt, keine weitere Aktion */ }
isFullscreen() -> sendKey(KeyEvent.KEYCODE_ESCAPE)
else -> webView.loadUrl(BuildConfig.WEBSITE_URL)
}
true
}
else -> super.dispatchKeyEvent(event)
}
}
companion object {
private const val SEEK_THROTTLE_MS = 200L
}
}
```
- [ ] **Step 2: Rebuild**
```powershell
cd C:\Projekte\kwabatv
$env:JAVA_HOME = "C:\Users\GGB\jdks\jdk-21.0.12.1+1"
.\gradlew.bat assembleDebug
```
Expected: `BUILD SUCCESSFUL`.
- [ ] **Step 3: Sideload onto real hardware**
Either of the two methods documented in the original app's own README (`Claude outputs/README.md`) work unchanged for a debug APK:
- **ADB** (fastest if the device is reachable on the local network): on the Fire TV Stick, enable Einstellungen → Mein Fire TV → Entwickleroptionen → ADB-Debugging, note its IP, then from this machine:
```powershell
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" connect <Fire-TV-IP>:5555
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" install -r C:\Projekte\kwabatv\app\build\outputs\apk\debug\app-debug.apk
```
- **Downloader app**: host `app-debug.apk` somewhere reachable (e.g. `python -m http.server` on the local network) and enter that URL in the Fire TV Stick's Downloader app.
- [ ] **Step 4: Commit**
```bash
cd C:\Projekte\kwabatv
git add app/src/main/java/de/ggb/kwabatv/MainActivity.kt
git commit -m "Add remote-control key mapping, fullscreen tracking, back-button navigation"
```
---
### Task 4: Hardware verification
**Files:** none (verification only).
**Interfaces:**
- Consumes: the installed app from Task 3.
- Produces: nothing new — this task is the feature's acceptance check, against the design spec's key-mapping table.
- [ ] **Step 1: Confirm the app launches into the room list**
Launch KwabaTV from the Fire TV home screen (it should appear there via the `LEANBACK_LAUNCHER` category). Confirm it loads the `/tv` room list (prompts for login on first launch if no session exists yet — log in once via the on-screen keyboard).
- [ ] **Step 2: Verify each mapping from the design spec's table**
With a room list or a room open as appropriate:
- DPAD OK/Center or Play/Pause media key → Play/Pause in a room.
- DPAD Left/Right tap → 5s seek. DPAD Left/Right held → confirm it seeks in 30s steps roughly every 0.2s, not a flood of smaller jumps (this is the one behavior that can't be checked by reading code alone — confirm by ear/eye against the video's visible timestamp).
- DPAD Up/Down (and Prev/Next media keys, if the remote has them) → previous/next title.
- Menu button → the TV menu popup opens (already confirmed once via a plain browser; confirm it also works through this app).
- Back button: in the room list → nothing happens. In a room, not fullscreen → returns to the room list. In a room, fullscreen → exits fullscreen only, stays in the room.
- Home button → native Fire TV home (unaffected by this app, nothing to verify beyond "it still works").
- [ ] **Step 3: Cross-check against the second device**
Repeat Step 2's checks on the other available remote/hardware combination, since different remotes may send different keycodes for the same physical buttons (especially Prev/Next media keys, which not every remote has).
- [ ] **Step 4: Record completion**
No code changes in this task. Note any mapping that didn't behave as expected, with the exact remote/button and observed vs. expected behavior — that becomes a small follow-up fix against Task 3, not a new task. If everything in Step 2 checks out on at least one device, the rebuild is functionally complete per the approved design spec; Step 3's cross-device check is a robustness bonus, not a blocker.
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=false
kotlin.code.style=official
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+249
View File
@@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+92
View File
@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+15
View File
@@ -0,0 +1,15 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "KwabaTV"
include(":app")