diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
new file mode 100644
index 00000000..007c59e3
--- /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, feature/video-support ]
+
+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
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
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9b73f13e..6de3a46a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,12 @@
+
+
+
+
+
@@ -30,6 +36,11 @@
android:dataExtractionRules="@xml/data_extraction_rules"
tools:targetApi="s">
+
+
-
\ No newline at end of file
+
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 @@
+
+
+
+
+
+
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..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,6 +16,7 @@
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;
@@ -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;
@@ -502,7 +504,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);
}
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();
+ }
+ }
+}