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" - } -}