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(); + } + } +}