Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fullscreen option to in-line videos #2747

Merged
merged 17 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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>
Comment on lines +299 to +300
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we keep the sizes in multiples of 8 here (reasoning)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same style being used by the Android's internal Media controller widgets, I kept the same dimensions to ensure consistency. But I don't think changing them will have an impact.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good in that case.

</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;
shubham1g5 marked this conversation as resolved.
Show resolved Hide resolved

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())));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should explicitly validate videoView.getVideoPath() is not null and crash with an exception otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation of the of the video path happens before the instantiation of the CommCareMediaController, so this branch would only be executed if the file exists.

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