|
|
|
@ -1,16 +1,34 @@
|
|
|
|
|
package org.joinmastodon.android.ui.photoviewer;
|
|
|
|
|
|
|
|
|
|
import android.Manifest;
|
|
|
|
|
import android.annotation.SuppressLint;
|
|
|
|
|
import android.app.Activity;
|
|
|
|
|
import android.app.DownloadManager;
|
|
|
|
|
import android.content.ContentResolver;
|
|
|
|
|
import android.content.ContentValues;
|
|
|
|
|
import android.content.Intent;
|
|
|
|
|
import android.content.pm.PackageManager;
|
|
|
|
|
import android.content.res.ColorStateList;
|
|
|
|
|
import android.graphics.Insets;
|
|
|
|
|
import android.graphics.PixelFormat;
|
|
|
|
|
import android.graphics.Rect;
|
|
|
|
|
import android.graphics.SurfaceTexture;
|
|
|
|
|
import android.graphics.drawable.ColorDrawable;
|
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
|
import android.media.AudioManager;
|
|
|
|
|
import android.media.MediaPlayer;
|
|
|
|
|
import android.media.MediaScannerConnection;
|
|
|
|
|
import android.net.Uri;
|
|
|
|
|
import android.os.Build;
|
|
|
|
|
import android.os.Environment;
|
|
|
|
|
import android.os.SystemClock;
|
|
|
|
|
import android.provider.MediaStore;
|
|
|
|
|
import android.provider.Settings;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
import android.view.DisplayCutout;
|
|
|
|
|
import android.view.Gravity;
|
|
|
|
|
import android.view.KeyEvent;
|
|
|
|
|
import android.view.MenuItem;
|
|
|
|
|
import android.view.Surface;
|
|
|
|
|
import android.view.TextureView;
|
|
|
|
|
import android.view.View;
|
|
|
|
@ -19,27 +37,47 @@ import android.view.ViewTreeObserver;
|
|
|
|
|
import android.view.WindowInsets;
|
|
|
|
|
import android.view.WindowManager;
|
|
|
|
|
import android.widget.FrameLayout;
|
|
|
|
|
import android.widget.ImageButton;
|
|
|
|
|
import android.widget.ImageView;
|
|
|
|
|
import android.widget.ProgressBar;
|
|
|
|
|
import android.widget.SeekBar;
|
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
import android.widget.Toast;
|
|
|
|
|
import android.widget.Toolbar;
|
|
|
|
|
|
|
|
|
|
import org.joinmastodon.android.R;
|
|
|
|
|
import org.joinmastodon.android.api.MastodonAPIController;
|
|
|
|
|
import org.joinmastodon.android.model.Attachment;
|
|
|
|
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.OutputStream;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Locale;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
|
import androidx.viewpager2.widget.ViewPager2;
|
|
|
|
|
import me.grishka.appkit.imageloader.ImageCache;
|
|
|
|
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
|
|
|
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
|
|
|
|
import me.grishka.appkit.utils.BindableViewHolder;
|
|
|
|
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
|
|
|
|
import me.grishka.appkit.utils.V;
|
|
|
|
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
|
|
|
|
import okio.BufferedSink;
|
|
|
|
|
import okio.Okio;
|
|
|
|
|
import okio.Sink;
|
|
|
|
|
import okio.Source;
|
|
|
|
|
|
|
|
|
|
public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
private static final String TAG="PhotoViewer";
|
|
|
|
|
public static final int PERMISSION_REQUEST=926;
|
|
|
|
|
|
|
|
|
|
private Activity activity;
|
|
|
|
|
private List<Attachment> attachments;
|
|
|
|
@ -48,10 +86,28 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
private Listener listener;
|
|
|
|
|
|
|
|
|
|
private FrameLayout windowView;
|
|
|
|
|
private FragmentRootLinearLayout uiOverlay;
|
|
|
|
|
private ViewPager2 pager;
|
|
|
|
|
private ColorDrawable background=new ColorDrawable(0xff000000);
|
|
|
|
|
private ArrayList<MediaPlayer> players=new ArrayList<>();
|
|
|
|
|
private int screenOnRefCount=0;
|
|
|
|
|
private Toolbar toolbar;
|
|
|
|
|
private View toolbarWrap;
|
|
|
|
|
private SeekBar videoSeekBar;
|
|
|
|
|
private TextView videoTimeView;
|
|
|
|
|
private ImageButton videoPlayPauseButton;
|
|
|
|
|
private View videoControls;
|
|
|
|
|
private boolean uiVisible=true;
|
|
|
|
|
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
|
|
|
|
|
private Runnable uiAutoHider=()->{
|
|
|
|
|
if(uiVisible)
|
|
|
|
|
toggleUI();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private boolean videoPositionNeedsUpdating;
|
|
|
|
|
private Runnable videoPositionUpdater=this::updateVideoPosition;
|
|
|
|
|
private int videoDuration, videoInitialPosition, videoLastTimeUpdatePosition;
|
|
|
|
|
private long videoInitialPositionTime;
|
|
|
|
|
|
|
|
|
|
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
|
|
|
|
|
this.activity=activity;
|
|
|
|
@ -75,7 +131,26 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
|
|
|
|
Log.w(TAG, "dispatchApplyWindowInsets() called with: insets = ["+insets+"]");
|
|
|
|
|
if(Build.VERSION.SDK_INT>=29){
|
|
|
|
|
DisplayCutout cutout=insets.getDisplayCutout();
|
|
|
|
|
if(cutout!=null){
|
|
|
|
|
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
|
|
|
|
|
Insets tappable=insets.getTappableElementInsets();
|
|
|
|
|
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
|
|
|
|
|
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
|
|
|
|
|
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
|
|
|
|
|
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
|
|
|
|
|
videoControls.setPadding(leftInset, 0, rightInset, 0);
|
|
|
|
|
}else{
|
|
|
|
|
toolbarWrap.setPadding(0, 0, 0, 0);
|
|
|
|
|
videoControls.setPadding(0, 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
uiOverlay.dispatchApplyWindowInsets(insets);
|
|
|
|
|
int bottomInset=insets.getSystemWindowInsetBottom();
|
|
|
|
|
if(bottomInset>0 && bottomInset<V.dp(36)){
|
|
|
|
|
uiOverlay.setPadding(uiOverlay.getPaddingLeft(), uiOverlay.getPaddingTop(), uiOverlay.getPaddingRight(), V.dp(36));
|
|
|
|
|
}
|
|
|
|
|
return insets.consumeSystemWindowInsets();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
@ -84,15 +159,47 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
pager=new ViewPager2(activity);
|
|
|
|
|
pager.setAdapter(new PhotoViewAdapter());
|
|
|
|
|
pager.setCurrentItem(index, false);
|
|
|
|
|
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onPageSelected(int position){
|
|
|
|
|
onPageChanged(position);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
windowView.addView(pager);
|
|
|
|
|
pager.setMotionEventSplittingEnabled(false);
|
|
|
|
|
|
|
|
|
|
uiOverlay=activity.getLayoutInflater().inflate(R.layout.photo_viewer_ui, windowView).findViewById(R.id.photo_viewer_overlay);
|
|
|
|
|
uiOverlay.setStatusBarColor(0x80000000);
|
|
|
|
|
uiOverlay.setNavigationBarColor(0x80000000);
|
|
|
|
|
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
|
|
|
|
|
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
|
|
|
|
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
|
|
|
|
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
|
|
|
toolbar.setOnMenuItemClickListener(item->{
|
|
|
|
|
saveCurrentFile();
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
uiOverlay.setAlpha(0f);
|
|
|
|
|
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
|
|
|
|
|
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
|
|
|
|
|
videoTimeView=uiOverlay.findViewById(R.id.time);
|
|
|
|
|
videoPlayPauseButton=uiOverlay.findViewById(R.id.play_pause_btn);
|
|
|
|
|
if(attachments.get(index).type!=Attachment.Type.VIDEO){
|
|
|
|
|
videoControls.setVisibility(View.GONE);
|
|
|
|
|
}else{
|
|
|
|
|
videoDuration=(int)Math.round(attachments.get(index).getDuration()*1000);
|
|
|
|
|
videoLastTimeUpdatePosition=-1;
|
|
|
|
|
updateVideoTimeText(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
|
|
|
|
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
|
|
|
|
|
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
|
|
|
|
|
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
|
|
|
|
wlp.format=PixelFormat.TRANSLUCENT;
|
|
|
|
|
wlp.setTitle(activity.getString(R.string.media_viewer));
|
|
|
|
|
if(Build.VERSION.SDK_INT>=28)
|
|
|
|
|
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
|
|
|
|
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
|
|
|
|
wm.addView(windowView, wlp);
|
|
|
|
|
|
|
|
|
@ -112,6 +219,44 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
videoPlayPauseButton.setOnClickListener(v->{
|
|
|
|
|
MediaPlayer player=findCurrentVideoPlayer();
|
|
|
|
|
if(player!=null){
|
|
|
|
|
if(player.isPlaying())
|
|
|
|
|
pauseVideo();
|
|
|
|
|
else
|
|
|
|
|
resumeVideo();
|
|
|
|
|
hideUiDelayed();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
videoSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
|
|
|
|
if(fromUser){
|
|
|
|
|
float p=progress/10000f;
|
|
|
|
|
updateVideoTimeText(Math.round(p*videoDuration));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onStartTrackingTouch(SeekBar seekBar){
|
|
|
|
|
stopUpdatingVideoPosition();
|
|
|
|
|
if(!uiVisible) // If dragging started during hide animation
|
|
|
|
|
toggleUI();
|
|
|
|
|
windowView.removeCallbacks(uiAutoHider);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onStopTrackingTouch(SeekBar seekBar){
|
|
|
|
|
MediaPlayer player=findCurrentVideoPlayer();
|
|
|
|
|
if(player!=null){
|
|
|
|
|
float progress=seekBar.getProgress()/10000f;
|
|
|
|
|
player.seekTo(Math.round(progress*player.getDuration()));
|
|
|
|
|
}
|
|
|
|
|
hideUiDelayed();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
@ -127,15 +272,22 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
@Override
|
|
|
|
|
public void onSetBackgroundAlpha(float alpha){
|
|
|
|
|
background.setAlpha(Math.round(alpha*255f));
|
|
|
|
|
uiOverlay.setAlpha(Math.max(0f, alpha*2f-1f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onStartSwipeToDismiss(){
|
|
|
|
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), false);
|
|
|
|
|
if(!uiVisible){
|
|
|
|
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
|
|
|
|
|
}else{
|
|
|
|
|
windowView.removeCallbacks(uiAutoHider);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onStartSwipeToDismissTransition(float velocityY){
|
|
|
|
|
pauseVideo();
|
|
|
|
|
// stop receiving input events to allow the user to interact with the underlying UI while the animation is still running
|
|
|
|
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
|
|
|
|
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
|
|
|
@ -163,17 +315,74 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
@Override
|
|
|
|
|
public void onSwipeToDismissCanceled(){
|
|
|
|
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
|
|
|
|
if(!uiVisible){
|
|
|
|
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
|
|
|
|
}else if(attachments.get(currentIndex).type==Attachment.Type.VIDEO){
|
|
|
|
|
hideUiDelayed();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onDismissed(){
|
|
|
|
|
for(MediaPlayer player:players)
|
|
|
|
|
player.release();
|
|
|
|
|
if(!players.isEmpty()){
|
|
|
|
|
activity.getSystemService(AudioManager.class).abandonAudioFocus(audioFocusListener);
|
|
|
|
|
}
|
|
|
|
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
|
|
|
|
wm.removeView(windowView);
|
|
|
|
|
listener.photoViewerDismissed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onSingleTap(){
|
|
|
|
|
toggleUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void toggleUI(){
|
|
|
|
|
if(uiVisible){
|
|
|
|
|
uiOverlay.animate()
|
|
|
|
|
.alpha(0f)
|
|
|
|
|
.setDuration(250)
|
|
|
|
|
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
|
|
|
|
.withEndAction(()->uiOverlay.setVisibility(View.GONE))
|
|
|
|
|
.start();
|
|
|
|
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
|
|
|
|
}else{
|
|
|
|
|
uiOverlay.setVisibility(View.VISIBLE);
|
|
|
|
|
uiOverlay.animate()
|
|
|
|
|
.alpha(1f)
|
|
|
|
|
.setDuration(300)
|
|
|
|
|
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
|
|
|
|
.start();
|
|
|
|
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
|
|
|
|
|
if(attachments.get(currentIndex).type==Attachment.Type.VIDEO)
|
|
|
|
|
hideUiDelayed(5000);
|
|
|
|
|
}
|
|
|
|
|
uiVisible=!uiVisible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void hideUiDelayed(){
|
|
|
|
|
hideUiDelayed(2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void hideUiDelayed(long delay){
|
|
|
|
|
windowView.removeCallbacks(uiAutoHider);
|
|
|
|
|
windowView.postDelayed(uiAutoHider, delay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onPageChanged(int index){
|
|
|
|
|
currentIndex=index;
|
|
|
|
|
Attachment att=attachments.get(index);
|
|
|
|
|
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
|
|
|
|
if(att.type==Attachment.Type.VIDEO){
|
|
|
|
|
videoSeekBar.setSecondaryProgress(0);
|
|
|
|
|
videoDuration=(int)Math.round(att.getDuration()*1000);
|
|
|
|
|
videoLastTimeUpdatePosition=-1;
|
|
|
|
|
updateVideoTimeText(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* To be called when the list containing photo views is scrolled
|
|
|
|
|
* @param x
|
|
|
|
@ -189,6 +398,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
|
|
|
|
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
|
|
|
|
wm.updateViewLayout(windowView, wlp);
|
|
|
|
|
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
|
|
|
|
}
|
|
|
|
|
screenOnRefCount++;
|
|
|
|
|
}
|
|
|
|
@ -201,6 +411,185 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
|
|
|
|
wlp.flags&=~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
|
|
|
|
wm.updateViewLayout(windowView, wlp);
|
|
|
|
|
activity.getSystemService(AudioManager.class).abandonAudioFocus(audioFocusListener);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void onPause(){
|
|
|
|
|
pauseVideo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveCurrentFile(){
|
|
|
|
|
if(Build.VERSION.SDK_INT>=29){
|
|
|
|
|
doSaveCurrentFile();
|
|
|
|
|
}else{
|
|
|
|
|
if(activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
|
|
|
|
|
listener.onRequestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE});
|
|
|
|
|
}else{
|
|
|
|
|
doSaveCurrentFile();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void onRequestPermissionsResult(String[] permissions, int[] results){
|
|
|
|
|
if(results[0]==PackageManager.PERMISSION_GRANTED){
|
|
|
|
|
doSaveCurrentFile();
|
|
|
|
|
}else if(!activity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
|
|
|
|
|
new M3AlertDialogBuilder(activity)
|
|
|
|
|
.setTitle(R.string.permission_required)
|
|
|
|
|
.setMessage(R.string.storage_permission_to_download)
|
|
|
|
|
.setPositiveButton(R.string.open_settings, (dialog, which)->activity.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", activity.getPackageName(), null))))
|
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
|
.show();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String mimeTypeForFileName(String fileName){
|
|
|
|
|
int extOffset=fileName.lastIndexOf('.');
|
|
|
|
|
if(extOffset>0){
|
|
|
|
|
return switch(fileName.substring(extOffset+1).toLowerCase()){
|
|
|
|
|
case "jpg", "jpeg" -> "image/jpeg";
|
|
|
|
|
case "png" -> "image/png";
|
|
|
|
|
case "gif" -> "image/gif";
|
|
|
|
|
case "webp" -> "image/webp";
|
|
|
|
|
case "mp4" -> "video/mp4";
|
|
|
|
|
case "webm" -> "video/webm";
|
|
|
|
|
default -> null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private OutputStream destinationStreamForFile(Attachment att) throws IOException{
|
|
|
|
|
String fileName=Uri.parse(att.url).getLastPathSegment();
|
|
|
|
|
if(Build.VERSION.SDK_INT>=29){
|
|
|
|
|
ContentValues values=new ContentValues();
|
|
|
|
|
// values.put(MediaStore.Downloads.DOWNLOAD_URI, att.url);
|
|
|
|
|
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
|
|
|
|
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
|
|
|
|
|
String mime=mimeTypeForFileName(fileName);
|
|
|
|
|
if(mime!=null)
|
|
|
|
|
values.put(MediaStore.MediaColumns.MIME_TYPE, mime);
|
|
|
|
|
ContentResolver cr=activity.getContentResolver();
|
|
|
|
|
Uri itemUri=cr.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
|
|
|
|
|
return cr.openOutputStream(itemUri);
|
|
|
|
|
}else{
|
|
|
|
|
return new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void doSaveCurrentFile(){
|
|
|
|
|
Attachment att=attachments.get(pager.getCurrentItem());
|
|
|
|
|
if(att.type==Attachment.Type.IMAGE){
|
|
|
|
|
UrlImageLoaderRequest req=new UrlImageLoaderRequest(att.url);
|
|
|
|
|
try{
|
|
|
|
|
File file=ImageCache.getInstance(activity).getFile(req);
|
|
|
|
|
if(file==null){
|
|
|
|
|
saveViaDownloadManager(att);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
MastodonAPIController.runInBackground(()->{
|
|
|
|
|
try(Source src=Okio.source(file); Sink sink=Okio.sink(destinationStreamForFile(att))){
|
|
|
|
|
BufferedSink buf=Okio.buffer(sink);
|
|
|
|
|
buf.writeAll(src);
|
|
|
|
|
buf.flush();
|
|
|
|
|
activity.runOnUiThread(()->Toast.makeText(activity, R.string.file_saved, Toast.LENGTH_SHORT).show());
|
|
|
|
|
if(Build.VERSION.SDK_INT<29){
|
|
|
|
|
String fileName=Uri.parse(att.url).getLastPathSegment();
|
|
|
|
|
File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
|
|
|
|
|
MediaScannerConnection.scanFile(activity, new String[]{dstFile.getAbsolutePath()}, new String[]{mimeTypeForFileName(fileName)}, null);
|
|
|
|
|
}
|
|
|
|
|
}catch(IOException x){
|
|
|
|
|
Log.w(TAG, "doSaveCurrentFile: ", x);
|
|
|
|
|
activity.runOnUiThread(()->Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}catch(IOException x){
|
|
|
|
|
Log.w(TAG, "doSaveCurrentFile: ", x);
|
|
|
|
|
Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
|
|
|
|
|
}
|
|
|
|
|
}else{
|
|
|
|
|
saveViaDownloadManager(att);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void saveViaDownloadManager(Attachment att){
|
|
|
|
|
DownloadManager.Request req=new DownloadManager.Request(Uri.parse(att.url));
|
|
|
|
|
activity.getSystemService(DownloadManager.class).enqueue(req);
|
|
|
|
|
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT).show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onAudioFocusChanged(int change){
|
|
|
|
|
if(change==AudioManager.AUDIOFOCUS_LOSS || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK){
|
|
|
|
|
pauseVideo();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private MediaPlayer findCurrentVideoPlayer(){
|
|
|
|
|
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
|
|
|
|
|
if(rv.findViewHolderForAdapterPosition(pager.getCurrentItem()) instanceof GifVViewHolder vvh && vvh.playerReady){
|
|
|
|
|
return vvh.player;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void pauseVideo(){
|
|
|
|
|
MediaPlayer player=findCurrentVideoPlayer();
|
|
|
|
|
if(player==null || !player.isPlaying())
|
|
|
|
|
return;
|
|
|
|
|
player.pause();
|
|
|
|
|
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
|
|
|
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
|
|
|
|
stopUpdatingVideoPosition();
|
|
|
|
|
windowView.removeCallbacks(uiAutoHider);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void resumeVideo(){
|
|
|
|
|
MediaPlayer player=findCurrentVideoPlayer();
|
|
|
|
|
if(player==null || player.isPlaying())
|
|
|
|
|
return;
|
|
|
|
|
player.start();
|
|
|
|
|
videoPlayPauseButton.setImageResource(R.drawable.ic_pause_24);
|
|
|
|
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.pause));
|
|
|
|
|
startUpdatingVideoPosition(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void startUpdatingVideoPosition(MediaPlayer player){
|
|
|
|
|
videoInitialPosition=player.getCurrentPosition();
|
|
|
|
|
videoInitialPositionTime=SystemClock.uptimeMillis();
|
|
|
|
|
videoDuration=player.getDuration();
|
|
|
|
|
videoPositionNeedsUpdating=true;
|
|
|
|
|
windowView.postOnAnimation(videoPositionUpdater);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void stopUpdatingVideoPosition(){
|
|
|
|
|
videoPositionNeedsUpdating=false;
|
|
|
|
|
windowView.removeCallbacks(videoPositionUpdater);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String formatTime(int timeSec, boolean includeHours){
|
|
|
|
|
if(includeHours)
|
|
|
|
|
return String.format(Locale.getDefault(), "%d:%02d:%02d", timeSec/3600, timeSec%3600/60, timeSec%60);
|
|
|
|
|
else
|
|
|
|
|
return String.format(Locale.getDefault(), "%d:%02d", timeSec/60, timeSec%60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateVideoPosition(){
|
|
|
|
|
if(videoPositionNeedsUpdating){
|
|
|
|
|
int currentPosition=videoInitialPosition+(int)(SystemClock.uptimeMillis()-videoInitialPositionTime);
|
|
|
|
|
videoSeekBar.setProgress(Math.round((float)currentPosition/videoDuration*10000f));
|
|
|
|
|
updateVideoTimeText(currentPosition);
|
|
|
|
|
windowView.postOnAnimation(videoPositionUpdater);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
|
|
|
private void updateVideoTimeText(int currentPosition){
|
|
|
|
|
int currentPositionSec=currentPosition/1000;
|
|
|
|
|
if(currentPositionSec!=videoLastTimeUpdatePosition){
|
|
|
|
|
videoLastTimeUpdatePosition=currentPositionSec;
|
|
|
|
|
boolean includeHours=videoDuration>=3600_000;
|
|
|
|
|
videoTimeView.setText(formatTime(currentPositionSec, includeHours)+" / "+formatTime(videoDuration/1000, includeHours));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -240,6 +629,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
Drawable getPhotoViewCurrentDrawable(int index);
|
|
|
|
|
|
|
|
|
|
void photoViewerDismissed();
|
|
|
|
|
void onRequestPermissions(String[] permissions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class PhotoViewAdapter extends RecyclerView.Adapter<BaseHolder>{
|
|
|
|
@ -334,13 +724,15 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class GifVViewHolder extends BaseHolder implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, TextureView.SurfaceTextureListener{
|
|
|
|
|
private class GifVViewHolder extends BaseHolder implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
|
|
|
|
|
MediaPlayer.OnVideoSizeChangedListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnInfoListener, MediaPlayer.OnSeekCompleteListener, TextureView.SurfaceTextureListener{
|
|
|
|
|
public TextureView textureView;
|
|
|
|
|
public FrameLayout wrap;
|
|
|
|
|
public MediaPlayer player;
|
|
|
|
|
private Surface surface;
|
|
|
|
|
private boolean playerReady;
|
|
|
|
|
private boolean keepingScreenOn;
|
|
|
|
|
private ProgressBar progressBar;
|
|
|
|
|
|
|
|
|
|
public GifVViewHolder(){
|
|
|
|
|
textureView=new TextureView(activity);
|
|
|
|
@ -348,6 +740,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
zoomPanView.addView(wrap, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
|
|
|
|
wrap.addView(textureView);
|
|
|
|
|
|
|
|
|
|
progressBar=new ProgressBar(activity);
|
|
|
|
|
progressBar.setIndeterminateTintList(ColorStateList.valueOf(0xffffffff));
|
|
|
|
|
zoomPanView.addView(progressBar, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
|
|
|
|
|
|
|
|
|
textureView.setSurfaceTextureListener(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -359,6 +755,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
params.width=item.getWidth();
|
|
|
|
|
params.height=item.getHeight();
|
|
|
|
|
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
|
|
|
|
|
progressBar.setVisibility(item.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
|
|
|
|
if(itemView.isAttachedToWindow()){
|
|
|
|
|
reset();
|
|
|
|
|
prepareAndStartPlayer();
|
|
|
|
@ -369,6 +766,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
public void onPrepared(MediaPlayer mp){
|
|
|
|
|
Log.d(TAG, "onPrepared() called with: mp = ["+mp+"]");
|
|
|
|
|
playerReady=true;
|
|
|
|
|
progressBar.setVisibility(View.GONE);
|
|
|
|
|
if(surface!=null)
|
|
|
|
|
startPlayer();
|
|
|
|
|
}
|
|
|
|
@ -398,19 +796,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
|
|
|
|
|
private void startPlayer(){
|
|
|
|
|
player.setSurface(surface);
|
|
|
|
|
player.setLooping(true);
|
|
|
|
|
player.start();
|
|
|
|
|
if(item.type==Attachment.Type.VIDEO){
|
|
|
|
|
incKeepScreenOn();
|
|
|
|
|
keepingScreenOn=true;
|
|
|
|
|
if(getAbsoluteAdapterPosition()==currentIndex){
|
|
|
|
|
player.start();
|
|
|
|
|
startUpdatingVideoPosition(player);
|
|
|
|
|
hideUiDelayed();
|
|
|
|
|
}
|
|
|
|
|
}else{
|
|
|
|
|
keepingScreenOn=false;
|
|
|
|
|
player.setLooping(true);
|
|
|
|
|
player.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onError(MediaPlayer mp, int what, int extra){
|
|
|
|
|
Log.e(TAG, "gif player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
|
|
|
|
Log.e(TAG, "video player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -420,6 +823,13 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
players.add(player);
|
|
|
|
|
player.setOnPreparedListener(this);
|
|
|
|
|
player.setOnErrorListener(this);
|
|
|
|
|
player.setOnVideoSizeChangedListener(this);
|
|
|
|
|
if(item.type==Attachment.Type.VIDEO){
|
|
|
|
|
player.setOnBufferingUpdateListener(this);
|
|
|
|
|
player.setOnInfoListener(this);
|
|
|
|
|
player.setOnSeekCompleteListener(this);
|
|
|
|
|
player.setOnCompletionListener(this);
|
|
|
|
|
}
|
|
|
|
|
try{
|
|
|
|
|
player.setDataSource(activity, Uri.parse(item.url));
|
|
|
|
|
player.prepareAsync();
|
|
|
|
@ -438,5 +848,53 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|
|
|
|
keepingScreenOn=false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onVideoSizeChanged(MediaPlayer mp, int width, int height){
|
|
|
|
|
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
|
|
|
|
|
params.width=width;
|
|
|
|
|
params.height=height;
|
|
|
|
|
zoomPanView.updateLayout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onBufferingUpdate(MediaPlayer mp, int percent){
|
|
|
|
|
if(getAbsoluteAdapterPosition()==currentIndex){
|
|
|
|
|
videoSeekBar.setSecondaryProgress(percent*100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onInfo(MediaPlayer mp, int what, int extra){
|
|
|
|
|
return switch(what){
|
|
|
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
|
|
|
|
progressBar.setVisibility(View.VISIBLE);
|
|
|
|
|
stopUpdatingVideoPosition();
|
|
|
|
|
yield true;
|
|
|
|
|
}
|
|
|
|
|
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
|
|
|
|
progressBar.setVisibility(View.GONE);
|
|
|
|
|
startUpdatingVideoPosition(player);
|
|
|
|
|
yield true;
|
|
|
|
|
}
|
|
|
|
|
default -> false;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onSeekComplete(MediaPlayer mp){
|
|
|
|
|
if(getAbsoluteAdapterPosition()==currentIndex && player.isPlaying())
|
|
|
|
|
startUpdatingVideoPosition(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onCompletion(MediaPlayer mp){
|
|
|
|
|
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
|
|
|
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
|
|
|
|
stopUpdatingVideoPosition();
|
|
|
|
|
if(!uiVisible)
|
|
|
|
|
toggleUI();
|
|
|
|
|
windowView.removeCallbacks(uiAutoHider);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|