From 9b507b0fc460e0cf1e65709a9134567ea751499c Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 16:46:16 +0900
Subject: [PATCH 01/12] Create build-android.yml
---
.github/workflows/build-android.yml | 45 +++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100644 .github/workflows/build-android.yml
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
new file mode 100644
index 00000000..9d229da2
--- /dev/null
+++ b/.github/workflows/build-android.yml
@@ -0,0 +1,45 @@
+name: Build Questopia APK
+
+on:
+ workflow_dispatch: # lets you click 'Run workflow' by hand
+ push: # builds on every commit to master/main
+ branches: [ master, main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Set up Android SDK (cmdline-tools, platform-tools, licenses)
+ uses: android-actions/setup-android@v3
+ # This action installs the commandline tools and accepts SDK licenses for us.
+
+ - name: Install required SDK packages
+ run: |
+ sdkmanager --sdk_root="$ANDROID_SDK_ROOT" \
+ "platform-tools" \
+ "platforms;android-34" \
+ "build-tools;34.0.0" \
+ "cmake;3.22.1" \
+ "ndk;27.2.12479018"
+
+ - name: Make Gradle wrapper executable
+ run: chmod +x ./gradlew
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug --stacktrace
+
+ - name: Upload APK artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: questopia-debug-apk
+ path: app/build/outputs/apk/debug/*.apk
From a3eaf1a7491a82fe9a41220605bac416a222347b Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:31:29 +0900
Subject: [PATCH 02/12] Update build.gradle
---
app/build.gradle | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/build.gradle b/app/build.gradle
index 7337d143..7b235175 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -79,6 +79,10 @@ android {
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
+
+ // Media3 / ExoPlayer
+ implementation "androidx.media3:media3-exoplayer:1.4.1"
+ implementation "androidx.media3:media3-ui:1.4.1"
implementation libs.appcompat
implementation libs.constraintlayout
@@ -120,4 +124,4 @@ dependencies {
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
-}
\ No newline at end of file
+}
From 6cc558fd7a35dc5c9c2c42b80d96c94e1c37e083 Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:36:53 +0900
Subject: [PATCH 03/12] Update AndroidManifest.xml
---
app/src/main/AndroidManifest.xml | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9b73f13e..f56168a3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,12 @@
+
+
+
+
+
@@ -71,4 +77,4 @@
-
\ No newline at end of file
+
From 48a14ddef72645d7f92422fcf10277bbed78fe28 Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:38:11 +0900
Subject: [PATCH 04/12] Create activity_video_player.xml
---
.../main/res/layout/activity_video_player.xml | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 app/src/main/app/src/main/res/layout/activity_video_player.xml
diff --git a/app/src/main/app/src/main/res/layout/activity_video_player.xml b/app/src/main/app/src/main/res/layout/activity_video_player.xml
new file mode 100644
index 00000000..4774cf93
--- /dev/null
+++ b/app/src/main/app/src/main/res/layout/activity_video_player.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
From b9fd106033a760db73f8851b498bf387470ca7cc Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:39:26 +0900
Subject: [PATCH 05/12] Create VideoPlayerActivity.kt
---
.../android/ui/video/VideoPlayerActivity.kt | 74 +++++++++++++++++++
1 file changed, 74 insertions(+)
create mode 100644 app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
diff --git a/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt b/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
new file mode 100644
index 00000000..fad77f7e
--- /dev/null
+++ b/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
@@ -0,0 +1,74 @@
+package org.qp.android.ui.video
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.PlayerView
+import org.qp.android.R
+
+class VideoPlayerActivity : ComponentActivity() {
+
+ private var player: ExoPlayer? = null
+ private lateinit var playerView: PlayerView
+
+ private val requestPerm = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) startPlayback()
+ else finish() // user denied
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ setContentView(R.layout.activity_video_player)
+ playerView = findViewById(R.id.playerView)
+
+ // check permission on Android 13+ (READ_MEDIA_VIDEO)
+ if (Build.VERSION.SDK_INT >= 33) {
+ val perm = Manifest.permission.READ_MEDIA_VIDEO
+ if (ContextCompat.checkSelfPermission(this, perm)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ requestPerm.launch(perm)
+ return
+ }
+ }
+ startPlayback()
+ }
+
+ private fun startPlayback() {
+ val uriStr = intent?.getStringExtra(EXTRA_URI) ?: run { finish(); return }
+ val uri = Uri.parse(uriStr)
+
+ player = ExoPlayer.Builder(this).build().also { exo ->
+ playerView.player = exo
+ val item = MediaItem.fromUri(uri)
+ exo.setMediaItem(item)
+ exo.prepare()
+ exo.playWhenReady = true
+ }
+
+ playerView.setControllerOnFullScreenModeChangedListener { /* optional */ }
+ playerView.setOnClickListener { /* consume to keep controller visible */ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ playerView.player = null
+ player?.release()
+ player = null
+ }
+
+ companion object {
+ const val EXTRA_URI = "videoUri"
+ }
+}
From 5edfbcace0a44b646b9d75a597cc3c291f00f1bd Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 18:42:11 +0900
Subject: [PATCH 06/12] Update AndroidManifest.xml
---
app/src/main/AndroidManifest.xml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f56168a3..6de3a46a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,6 +36,11 @@
android:dataExtractionRules="@xml/data_extraction_rules"
tools:targetApi="s">
+
+
Date: Sun, 24 Aug 2025 22:08:24 +0900
Subject: [PATCH 07/12] Update LibProxyImpl.java
---
.../qp/android/model/lib/LibProxyImpl.java | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
index 3a97188a..4e95428b 100644
--- a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
+++ b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
@@ -502,7 +502,32 @@ public void onShowMessage(String text) {
@Override
public void onPlayFile(String file, int volume) {
if (!isNotEmptyOrBlank(file)) return;
-
+
+ // Convert to URI so we can check extension
+ Uri uri = Uri.parse(file);
+
+ // Extract extension safely
+ String last = uri.getLastPathSegment();
+ String ext = last != null && last.contains(".")
+ ? last.substring(last.lastIndexOf('.') + 1).toLowerCase()
+ : "";
+
+ // Check if this file is a video
+ boolean isVideo = java.util.Arrays.asList(
+ "mp4", "m4v", "mov", "webm", "3gp", "3gpp", "mkv"
+ ).contains(ext);
+
+ if (isVideo) {
+ // If video: launch custom VideoPlayerActivity
+ Intent i = new Intent(context, org.qp.android.ui.video.VideoPlayerActivity.class);
+ i.putExtra("videoUri", uri.toString());
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // required if not from an Activity
+ context.startActivity(i);
+
+ return; // stop here so we don’t send video to AudioPlayer
+ }
+
+ // Otherwise: handle as audio normally
getAudioPlayer().playFile(file, volume);
}
From 7c1f43f50dbf1757c3f26dd63c3a60145dafe65b Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:27:42 +0900
Subject: [PATCH 08/12] Update build-android.yml
---
.github/workflows/build-android.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index 9d229da2..007c59e3 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -3,7 +3,7 @@ name: Build Questopia APK
on:
workflow_dispatch: # lets you click 'Run workflow' by hand
push: # builds on every commit to master/main
- branches: [ master, main ]
+ branches: [ master, main, feature/video-support ]
jobs:
build:
From 3e60dfd4276d7f055f3dfafe835b04ee277e104f Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:40:20 +0900
Subject: [PATCH 09/12] Update LibProxyImpl.java
---
app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
index 4e95428b..8d256217 100644
--- a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
+++ b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
@@ -21,6 +21,7 @@
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
+import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,6 +36,7 @@
import org.qp.android.model.service.HtmlProcessor;
import org.qp.android.ui.game.GameInterface;
+
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
From eaf98d115eac2af7a5541e6a7c8071ca5c1ac1d2 Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:44:39 +0900
Subject: [PATCH 10/12] Update LibProxyImpl.java
---
app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
index 8d256217..79312b72 100644
--- a/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
+++ b/app/src/main/java/org/qp/android/model/lib/LibProxyImpl.java
@@ -16,12 +16,12 @@
import static org.qp.android.helpers.utils.ThreadUtil.throwIfNotMainThread;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
-import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
From c76e4e247659224f8b50d4696793195770f9a0ba Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 22:59:11 +0900
Subject: [PATCH 11/12] Create VideoPlayerActivity.java
---
.../android/ui/video/VideoPlayerActivity.java | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.java
diff --git a/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.java b/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.java
new file mode 100644
index 00000000..4c79ce24
--- /dev/null
+++ b/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.java
@@ -0,0 +1,37 @@
+package org.qp.android.ui.video;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class VideoPlayerActivity extends AppCompatActivity {
+
+ public static final String EXTRA_VIDEO_URI = "video_uri";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Create VideoView programmatically (you can also use a layout file if you prefer)
+ VideoView videoView = new VideoView(this);
+ setContentView(videoView);
+
+ // Get video URI from intent extras
+ String uriString = getIntent().getStringExtra(EXTRA_VIDEO_URI);
+ if (uriString != null) {
+ Uri videoUri = Uri.parse(uriString);
+ videoView.setVideoURI(videoUri);
+
+ // Optional: add media controls (play/pause/seek bar)
+ MediaController mediaController = new MediaController(this);
+ mediaController.setAnchorView(videoView);
+ videoView.setMediaController(mediaController);
+
+ videoView.start();
+ }
+ }
+}
From 907896f9d14d7510540a4e3c8634b3b788058234 Mon Sep 17 00:00:00 2001
From: tuanakotta <65275173+tuanakotta@users.noreply.github.com>
Date: Sun, 24 Aug 2025 23:03:12 +0900
Subject: [PATCH 12/12] Delete
app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
---
.../android/ui/video/VideoPlayerActivity.kt | 74 -------------------
1 file changed, 74 deletions(-)
delete mode 100644 app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
diff --git a/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt b/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
deleted file mode 100644
index fad77f7e..00000000
--- a/app/src/main/java/org/qp/android/ui/video/VideoPlayerActivity.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package org.qp.android.ui.video
-
-import android.Manifest
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.view.WindowManager
-import androidx.activity.ComponentActivity
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.content.ContextCompat
-import androidx.media3.common.MediaItem
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.ui.PlayerView
-import org.qp.android.R
-
-class VideoPlayerActivity : ComponentActivity() {
-
- private var player: ExoPlayer? = null
- private lateinit var playerView: PlayerView
-
- private val requestPerm = registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { granted ->
- if (granted) startPlayback()
- else finish() // user denied
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- setContentView(R.layout.activity_video_player)
- playerView = findViewById(R.id.playerView)
-
- // check permission on Android 13+ (READ_MEDIA_VIDEO)
- if (Build.VERSION.SDK_INT >= 33) {
- val perm = Manifest.permission.READ_MEDIA_VIDEO
- if (ContextCompat.checkSelfPermission(this, perm)
- != PackageManager.PERMISSION_GRANTED
- ) {
- requestPerm.launch(perm)
- return
- }
- }
- startPlayback()
- }
-
- private fun startPlayback() {
- val uriStr = intent?.getStringExtra(EXTRA_URI) ?: run { finish(); return }
- val uri = Uri.parse(uriStr)
-
- player = ExoPlayer.Builder(this).build().also { exo ->
- playerView.player = exo
- val item = MediaItem.fromUri(uri)
- exo.setMediaItem(item)
- exo.prepare()
- exo.playWhenReady = true
- }
-
- playerView.setControllerOnFullScreenModeChangedListener { /* optional */ }
- playerView.setOnClickListener { /* consume to keep controller visible */ }
- }
-
- override fun onStop() {
- super.onStop()
- playerView.player = null
- player?.release()
- player = null
- }
-
- companion object {
- const val EXTRA_URI = "videoUri"
- }
-}