Skip to content

Commit

Permalink
Merge pull request #2747 from dimagi/add-fullscreen-option-to-inline-…
Browse files Browse the repository at this point in the history
…videos

Add fullscreen option to in-line videos
  • Loading branch information
avazirna authored Apr 29, 2024
2 parents b2a3665 + 8588645 commit 2df7444
Show file tree
Hide file tree
Showing 19 changed files with 290 additions and 47 deletions.
4 changes: 4 additions & 0 deletions app/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@
android:name="org.commcare.activities.SessionAwarePreferenceActivity"
android:theme="@style/PreferenceTheme">
</activity>
<activity
android:name="org.commcare.activities.FullscreenVideoViewActivity"
android:theme="@style/FullscreenTheme">
</activity>
<activity
android:exported="false"
android:name="org.commcare.activities.DotsEntryActivity">
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-hdpi/ic_media_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-ldpi/ic_media_exit_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-ldpi/ic_media_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-mdpi/ic_media_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-xhdpi/ic_media_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/res/drawable-xxhdpi/ic_media_fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions app/res/layout/activity_fullscreen_video_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">

<org.commcare.views.media.CommCareVideoView
android:id="@+id/fullscreen_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
</LinearLayout>
12 changes: 12 additions & 0 deletions app/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,16 @@
<color name="login_edit_text_color">@color/cc_core_text</color>
<color name="login_edit_text_color_error">@color/cc_attention_negative_color</color>
<dimen name="map_button_min_width">120dp</dimen>

<style name="FullScreenVideoButton">
<item name="background">@null</item>
<item name="android:layout_width">71dip</item>
<item name="android:layout_height">52dip</item>
</style>

<style name="FullscreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">"@color/black"</item>
<item name="android:navigationBarColor">@color/black</item>
</style>
</resources>
101 changes: 70 additions & 31 deletions app/src/org/commcare/activities/FormEntryActivity.java

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions app/src/org/commcare/activities/FullscreenVideoViewActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.commcare.activities

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.commcare.dalvik.databinding.ActivityFullscreenVideoViewBinding
import org.commcare.views.media.CommCareMediaController

/**
* Activity to view inline videos in fullscreen mode, it returns the last time position to the
* calling activity
*
* @author avazirna
*/
class FullscreenVideoViewActivity : AppCompatActivity() {

private lateinit var viewBinding: ActivityFullscreenVideoViewBinding
private var lastPosition = -1

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityFullscreenVideoViewBinding.inflate(layoutInflater)
setContentView(viewBinding.root)

// Get video URI from intent, crash if no URI is available
intent.data?.let { viewBinding.fullscreenVideoView.setVideoURI(intent.data) }
?: throw RuntimeException("Video file not found!");
lastPosition = restoreLastPosition(savedInstanceState)

viewBinding.fullscreenVideoView.setMediaController(CommCareMediaController(this, true))
viewBinding.fullscreenVideoView.setOnPreparedListener {
if (lastPosition != -1) {
viewBinding.fullscreenVideoView.seekTo(lastPosition)
}
viewBinding.fullscreenVideoView.start()
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (lastPosition != -1) {
outState.putInt(CommCareMediaController.INLINE_VIDEO_TIME_POSITION, lastPosition)
}
}

override fun onPause() {
super.onPause()
if (viewBinding.fullscreenVideoView != null) {
viewBinding.fullscreenVideoView.pause()
lastPosition = viewBinding.fullscreenVideoView.currentPosition
}
}

override fun onBackPressed() {
setResultIntent()
super.onBackPressed()
}

private fun setResultIntent() {
val i = Intent()
i.putExtra(
CommCareMediaController.INLINE_VIDEO_TIME_POSITION,
viewBinding.fullscreenVideoView.currentPosition
)
this.setResult(RESULT_OK, i)
}

override fun onDestroy() {
super.onDestroy()
viewBinding.fullscreenVideoView.stopPlayback()
}

// priority is given to lastPosition saved state
private fun restoreLastPosition(savedInstanceState: Bundle?): Int {
val intentExtras = intent.extras
if (savedInstanceState != null &&
savedInstanceState.containsKey(CommCareMediaController.INLINE_VIDEO_TIME_POSITION)) {
return savedInstanceState.getInt(CommCareMediaController.INLINE_VIDEO_TIME_POSITION)
} else if (intentExtras != null &&
intentExtras.containsKey(CommCareMediaController.INLINE_VIDEO_TIME_POSITION)) {
return intentExtras.getInt(CommCareMediaController.INLINE_VIDEO_TIME_POSITION)
}
return -1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class FormEntryConstants {
public static final int INTENT_COMPOUND_CALLOUT = 13;
public static final int INTENT_LOCATION_PERMISSION = 14;
public static final int INTENT_LOCATION_EXCEPTION = 15;
public static final int VIEW_VIDEO_FULLSCREEN = 16;

public static final String NAV_STATE_NEXT = "next";
public static final String NAV_STATE_DONE = "done";
Expand Down
76 changes: 72 additions & 4 deletions app/src/org/commcare/views/media/CommCareMediaController.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
package org.commcare.views.media;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.MediaController;

import androidx.appcompat.app.AppCompatActivity;

import org.commcare.activities.FullscreenVideoViewActivity;
import org.commcare.activities.components.FormEntryConstants;
import org.commcare.dalvik.R;
import org.commcare.utils.AndroidUtil;
import org.commcare.utils.FileUtil;

import java.io.File;

/**
* Custom MediaController which provides a workaround to the issue where hide and show aren't working while adding it in the view hierarchy.
* Custom MediaController which provides a workaround to the issue where hide and show aren't
* working while adding it in the view hierarchy.
* Note: Use only when you're manually adding MediaController in the view hierarchy.
* Used here {@link MediaLayout}
* @author $|-|!˅@M
*/
public class CommCareMediaController extends MediaController {

public static final String INLINE_VIDEO_TIME_POSITION = "inline-video-time-position";

// A mock to superclass' isShowing property.
// {@link https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/MediaController.java#96}
private boolean _isShowing = false;
private ImageButton fullscreenBtn;
private boolean fullscreenMode;

public CommCareMediaController(Context context, AttributeSet attrs) {
public CommCareMediaController(Context context, AttributeSet attrs, boolean fullscreenMode) {
super(context, attrs);
this.fullscreenMode = fullscreenMode;
}

public CommCareMediaController(Context context, boolean useFastForward) {
public CommCareMediaController(Context context, boolean useFastForward, boolean fullscreenMode) {
super(context, useFastForward);
this.fullscreenMode = fullscreenMode;
}

public CommCareMediaController(Context context) {
public CommCareMediaController(Context context, boolean fullscreenMode) {
super(context);
this.fullscreenMode = fullscreenMode;
}

@Override
Expand All @@ -50,4 +73,49 @@ public void hide() {
ViewGroup parent = (ViewGroup) this.getParent();
parent.setVisibility(View.GONE);
}

@Override
public void setAnchorView(View view) {
super.setAnchorView(view);

int videoViewId = fullscreenMode ? R.id.fullscreen_video_view : R.id.inline_video_view;
CommCareVideoView videoView = view.findViewById(videoViewId);

if (videoView != null) {
addFullscreenButton(videoView);
}
}

private void addFullscreenButton(CommCareVideoView videoView) {
if (fullscreenBtn == null) {
fullscreenBtn = new ImageButton(getContext(), null, R.style.FullScreenVideoButton);
fullscreenBtn.setId(AndroidUtil.generateViewId());
if (fullscreenMode) {
fullscreenBtn.setImageResource(R.drawable.ic_media_exit_fullscreen);
} else {
fullscreenBtn.setImageResource(R.drawable.ic_media_fullscreen);
}
fullscreenBtn.setOnClickListener(view1 -> {
// if in fullscreen mode, we exit
if (fullscreenMode) {
Intent i = new Intent();
i.putExtra(CommCareMediaController.INLINE_VIDEO_TIME_POSITION, videoView.getCurrentPosition());
((AppCompatActivity)getContext()).setResult(Activity.RESULT_OK, i);
((AppCompatActivity)getContext()).finish();
} else {
Intent intent = new Intent(getContext(), FullscreenVideoViewActivity.class);
intent.setData(FileUtil.getUriForExternalFile(getContext(),
new File(videoView.getVideoPath())));
if (videoView.isPlaying()) {
intent.putExtra(INLINE_VIDEO_TIME_POSITION, videoView.getCurrentPosition());
}
((AppCompatActivity) getContext()).startActivityForResult(intent,
FormEntryConstants.VIEW_VIDEO_FULLSCREEN);
}
});
}
FrameLayout.LayoutParams frameParams = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.END);
this.addView(fullscreenBtn, frameParams);
}
}
13 changes: 13 additions & 0 deletions app/src/org/commcare/views/media/CommCareVideoView.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.commcare.views.media;

import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet;
import android.widget.VideoView;

Expand All @@ -18,6 +19,8 @@ public class CommCareVideoView extends VideoView {
private long duration;
private long startTime;

private String videoPath;

public CommCareVideoView(Context context) {
super(context);
}
Expand Down Expand Up @@ -54,6 +57,16 @@ protected void onDetachedFromWindow() {
}
}

@Override
public void setVideoPath(String videoPath){
super.setVideoPath(videoPath);
this.videoPath = videoPath;
}

public String getVideoPath() {
return videoPath;
}

public interface VideoDetachedListener {
void onVideoDetached(long duration);
}
Expand Down
31 changes: 19 additions & 12 deletions app/src/org/commcare/views/media/MediaLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@

import java.io.File;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import kotlinx.coroutines.Dispatchers;

Expand Down Expand Up @@ -108,7 +108,8 @@ public static MediaLayout buildComprehensiveLayout(Context context,
final String ttsText,
int questionIndex) {
MediaLayout mediaLayout = new MediaLayout(context);
mediaLayout.setAVT(text, audioURI, imageURI, videoURI, bigImageURI, qrCodeContent, inlineVideoURI, false, questionIndex);
mediaLayout.setAVT(text, audioURI, imageURI, videoURI, bigImageURI, qrCodeContent, inlineVideoURI, false,
questionIndex);
// Show TTS view only when audioURI is not present
if (ttsText != null && audioURI == null) {
mediaLayout.showTtsButton(ttsText);
Expand Down Expand Up @@ -184,7 +185,8 @@ private void setupStandardAudio(String audioURI, int questionIndex) {
private void setupVideoButton(String videoURI) {
if (videoURI != null) {
boolean mediaPresent = FileUtil.referenceFileExists(videoURI);
videoButton.setImageResource(mediaPresent ? android.R.drawable.ic_media_play : R.drawable.update_download_icon);
videoButton.setImageResource(
mediaPresent ? android.R.drawable.ic_media_play : R.drawable.update_download_icon);
if (!mediaPresent) {
AndroidUtil.showToast(getContext(), R.string.video_download_prompt);
}
Expand All @@ -207,7 +209,8 @@ private void setupVideoButton(String videoURI) {
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
getContext().startActivity(i);
FormEntryActivity.mFormController.getFormAnalyticsHelper().recordVideoPlaybackStart(videoFile);
FormEntryActivity.mFormController.getFormAnalyticsHelper()
.recordVideoPlaybackStart(videoFile);
} catch (ActivityNotFoundException e) {
Toast.makeText(getContext(),
getContext().getString(R.string.activity_not_found, "view video"),
Expand All @@ -224,7 +227,8 @@ private void downloadMissingVideo(ImageButton videoButton, String videoURI) {
MissingMediaDownloadHelper.requestMediaDownload(videoURI, Dispatchers.getDefault(), result -> {
if (result instanceof MissingMediaDownloadResult.Success) {
boolean mediaPresent = FileUtil.referenceFileExists(videoURI);
videoButton.setImageResource(mediaPresent ? android.R.drawable.ic_media_play : R.drawable.update_download_icon);
videoButton.setImageResource(
mediaPresent ? android.R.drawable.ic_media_play : R.drawable.update_download_icon);
AndroidUtil.showToast(getContext(), R.string.media_download_completed);
videoButton.setVisibility(VISIBLE);
} else if (result instanceof MissingMediaDownloadResult.InProgress) {
Expand Down Expand Up @@ -307,12 +311,12 @@ private void setupInlineVideoView(String inlineVideoURI) {
setupInlineVideoView(inlineVideoURI);
});
} else {
final CommCareMediaController ctrl = new CommCareMediaController(this.getContext());
final CommCareMediaController ctrl = new CommCareMediaController(this.getContext(), false);
ctrl.setId(AndroidUtil.generateViewId());
videoView.setOnPreparedListener(mediaPlayer -> {
//Since MediaController will create a default set of controls and put them in a window floating above your application(From AndroidDocs)
//It would never follow the parent view's animation or scroll.
//So, adding the MediaController to the view hierarchy here.
// Since MediaController will create a default set of controls and put them in a window
// floating above your application(From AndroidDocs). It would never follow the parent view's
// animation or scroll. So, adding the MediaController to the view hierarchy here.
FrameLayout frameLayout = (FrameLayout)ctrl.getParent();
((ViewGroup)frameLayout.getParent()).removeView(frameLayout);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
Expand All @@ -336,7 +340,8 @@ private void setupInlineVideoView(String inlineVideoURI) {
if (duration == 0) {
return;
}
FirebaseAnalyticsUtil.reportInlineVideoPlayEvent(videoFilename, FileUtil.getDuration(videoFile), duration);
FirebaseAnalyticsUtil.reportInlineVideoPlayEvent(videoFilename,
FileUtil.getDuration(videoFile), duration);
});

videoView.setOnClickListener(v -> ViewUtil.hideVirtualKeyboard((AppCompatActivity)getContext()));
Expand Down Expand Up @@ -367,7 +372,8 @@ private void makeVideoViewVisible() {
videoView.setLayoutParams(params);
}

private void showMissingMediaView(String mediaUri, String errorMessage, boolean allowDownload, @Nullable Runnable completion) {
private void showMissingMediaView(String mediaUri, String errorMessage, boolean allowDownload,
@Nullable Runnable completion) {
missingMediaView.setVisibility(VISIBLE);
missingMediaStatus.setText(errorMessage);
missingMediaStatus.setVisibility(VISIBLE);
Expand All @@ -378,7 +384,8 @@ private void showMissingMediaView(String mediaUri, String errorMessage, boolean
progressBar.setVisibility(VISIBLE);
downloadIcon.setVisibility(INVISIBLE);
downloadIcon.setEnabled(false);
missingMediaStatus.setText(StringUtils.getStringRobust(getContext(), R.string.media_download_in_progress));
missingMediaStatus.setText(
StringUtils.getStringRobust(getContext(), R.string.media_download_in_progress));

MissingMediaDownloadHelper.requestMediaDownload(mediaUri, Dispatchers.getDefault(), result -> {
if (result instanceof MissingMediaDownloadResult.Success) {
Expand Down

0 comments on commit 2df7444

Please sign in to comment.