From a537c49e06aa99558839c41667dadce5e6be694b Mon Sep 17 00:00:00 2001 From: Arne Date: Fri, 6 Sep 2024 18:44:59 +0200 Subject: [PATCH] Apply monocles in app media preview --- build.gradle | 4 + .../persistance/FileBackend.java | 6 +- .../siacs/conversations/ui/util/ViewUtil.java | 46 +- .../conversations/utils/Compatibility.java | 2 +- src/main/res/drawable/ic_menu_white_24dp.xml | 5 + .../drawable/ic_open_in_new_white_24dp.xml | 5 + src/main/res/layout/activity_media_viewer.xml | 63 ++ src/main/res/menu/media_viewer.xml | 18 + src/main/res/values/strings.xml | 1 + src/monocleschat/AndroidManifest.xml | 9 +- .../de/monocles/chat/MediaViewerActivity.java | 612 ++++++++++++++++++ src/monocleschat/res/values/strings.xml | 1 + 12 files changed, 758 insertions(+), 14 deletions(-) create mode 100644 src/main/res/drawable/ic_menu_white_24dp.xml create mode 100644 src/main/res/drawable/ic_open_in_new_white_24dp.xml create mode 100644 src/main/res/layout/activity_media_viewer.xml create mode 100644 src/main/res/menu/media_viewer.xml create mode 100644 src/monocleschat/java/de/monocles/chat/MediaViewerActivity.java diff --git a/build.gradle b/build.gradle index f873f4642..3fd0e24e6 100644 --- a/build.gradle +++ b/build.gradle @@ -125,6 +125,10 @@ dependencies { implementation 'net.fellbaum:jemoji:1.4.1' implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation 'com.github.bumptech.glide:glide:4.16.0' + implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' + implementation 'com.google.android.exoplayer:extension-mediasession:2.19.1' } ext { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index f0c2419c8..3a600b347 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1150,7 +1150,7 @@ public class FileBackend { } } - private static int getRotation(final InputStream inputStream) throws IOException { + public static int getRotation(final InputStream inputStream) throws IOException { final ExifInterface exif = new ExifInterface(inputStream); final int orientation = exif.getAttributeInt( @@ -2287,6 +2287,10 @@ public class FileBackend { } } + public boolean deleteFile(File file) { + return file.delete(); + } + private static class Dimensions { public final int width; public final int height; diff --git a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java index 1cc630ad3..de338f98a 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java @@ -12,10 +12,12 @@ import android.widget.Toast; import java.io.File; import java.util.List; +import de.monocles.chat.MediaViewerActivity; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.persistance.FileBackend; +import me.drakeet.support.toast.ToastCompat; public class ViewUtil { @@ -37,24 +39,46 @@ public class ViewUtil { view(context, file, mime); } - private static void view(Context context, File file, String mime) { - Log.d(Config.LOGTAG,"viewing "+file.getAbsolutePath()+" "+mime); - final Intent openIntent = new Intent(Intent.ACTION_VIEW); + public static void view(Context context, File file, String mime) { + Log.d(Config.LOGTAG, "viewing " + file.getAbsolutePath() + " " + mime); final Uri uri; try { uri = FileBackend.getUriForFile(context, file); } catch (SecurityException e) { Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); - Toast.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); + ToastCompat.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); return; } - openIntent.setDataAndType(uri, mime); - openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - context.startActivity(openIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + // use internal viewer for images and videos + if (mime.startsWith("image/")) { + final Intent intent = new Intent(context, MediaViewerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("image", Uri.fromFile(file)); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.cant_open_file, Toast.LENGTH_LONG).show(); + } + } else if (mime.startsWith("video/")) { + final Intent intent = new Intent(context, MediaViewerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("video", Uri.fromFile(file)); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.cant_open_file, Toast.LENGTH_LONG).show(); + } + } else { + final Intent openIntent = new Intent(Intent.ACTION_VIEW); + openIntent.setDataAndType(uri, mime); + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + context.startActivity(openIntent); + } catch (final ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + } } } - } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index f4bdd8a5a..c7ba0f36a 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -35,7 +35,7 @@ public class Compatibility { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; } - private static boolean runsTwentyFour() { + public static boolean runsTwentyFour() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } diff --git a/src/main/res/drawable/ic_menu_white_24dp.xml b/src/main/res/drawable/ic_menu_white_24dp.xml new file mode 100644 index 000000000..cceb1b9ba --- /dev/null +++ b/src/main/res/drawable/ic_menu_white_24dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/res/drawable/ic_open_in_new_white_24dp.xml b/src/main/res/drawable/ic_open_in_new_white_24dp.xml new file mode 100644 index 000000000..d1319406c --- /dev/null +++ b/src/main/res/drawable/ic_open_in_new_white_24dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/res/layout/activity_media_viewer.xml b/src/main/res/layout/activity_media_viewer.xml new file mode 100644 index 000000000..e1fb7053b --- /dev/null +++ b/src/main/res/layout/activity_media_viewer.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/media_viewer.xml b/src/main/res/menu/media_viewer.xml new file mode 100644 index 000000000..7218e6fa0 --- /dev/null +++ b/src/main/res/menu/media_viewer.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 56d1bee59..516f84703 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1082,4 +1082,5 @@ Show less avatars Show only needed avatars in the chats Store media only in cache + Can\'t open file diff --git a/src/monocleschat/AndroidManifest.xml b/src/monocleschat/AndroidManifest.xml index 03cf2c916..64888cd73 100644 --- a/src/monocleschat/AndroidManifest.xml +++ b/src/monocleschat/AndroidManifest.xml @@ -19,7 +19,14 @@ - + { + if (this.xmppConnectionService.getFileBackend().deleteFile(mFile)) { + finish(); + } + }); + builder.create().show(); + } + + private void open() { + Uri uri; + try { + uri = FileBackend.getUriForFile(this, mFile); + } catch (SecurityException e) { + Log.d(Config.LOGTAG, "No permission to access " + mFile.getAbsolutePath(), e); + ToastCompat.makeText(this, this.getString(R.string.no_permission_to_access_x, mFile.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); + return; + } + String mime = MimeUtils.guessMimeTypeFromUri(this, uri); + Intent openIntent = new Intent(Intent.ACTION_VIEW); + openIntent.setDataAndType(uri, mime); + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + PackageManager manager = this.getPackageManager(); + List info = manager.queryIntentActivities(openIntent, 0); + if (info.size() == 0) { + openIntent.setDataAndType(uri, "*/*"); + } + if (player != null && isVideo) { + openIntent.putExtra("position", player.getCurrentPosition()); + } + try { + this.startActivity(openIntent); + } catch (ActivityNotFoundException e) { + ToastCompat.makeText(this, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); + } + } + + @Override + protected void refreshUiReal() { + + } + + @Override + public void onStart() { + super.onStart(); + Intent intent = getIntent(); + if (intent != null) { + if (intent.hasExtra("image")) { + mFileUri = intent.getParcelableExtra("image"); + mFile = new File(mFileUri.getPath()); + if (mFileUri != null && mFile.exists() && mFile.length() > 0) { + try { + isImage = true; + DisplayImage(mFile, mFileUri); + } catch (Exception e) { + isImage = false; + Log.d(Config.LOGTAG, "Illegal exeption :" + e); + ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.error_file_not_found), ToastCompat.LENGTH_SHORT).show(); + finish(); + } + } else { + ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.file_deleted), ToastCompat.LENGTH_SHORT).show(); + } + } else if (intent.hasExtra("video")) { + mFileUri = intent.getParcelableExtra("video"); + mFile = new File(mFileUri.getPath()); + if (mFileUri != null && mFile.exists() && mFile.length() > 0) { + try { + isVideo = true; + DisplayVideo(mFileUri); + } catch (Exception e) { + isVideo = false; + Log.d(Config.LOGTAG, "Illegal exeption :" + e); + ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.error_file_not_found), ToastCompat.LENGTH_SHORT).show(); + finish(); + } + } else { + ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.file_deleted), ToastCompat.LENGTH_SHORT).show(); + } + } + } + if (isDeletableFile(mFile)) { + binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_delete, R.drawable.ic_delete_24dp) + .setLabel(R.string.delete) + .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) + .create() + ); + } + binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_open, R.drawable.ic_open_in_new_white_24dp) + .setLabel(R.string.open_with) + .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) + .create() + ); + binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_share, R.drawable.ic_share_24dp) + .setLabel(R.string.share) + .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) + .create() + ); + + if (isDeletableFile(mFile)) { + binding.speedDial.setOnActionSelectedListener(actionItem -> { + switch (actionItem.getId()) { + case R.id.action_share: + share(); + break; + case R.id.action_open: + open(); + break; + case R.id.action_delete: + deleteFile(); + break; + default: + return false; + } + return false; + }); + } else { + binding.speedDial.setOnActionSelectedListener(actionItem -> { + switch (actionItem.getId()) { + case R.id.action_share: + share(); + break; + case R.id.action_open: + open(); + break; + default: + return false; + } + return false; + }); + } + binding.speedDial.getMainFab().setSupportImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.white))); + } + + private void DisplayImage(final File file, final Uri uri) { + final boolean gif = "image/gif".equalsIgnoreCase(getMimeType(file.toString())); + final boolean bmp = "image/bmp".equalsIgnoreCase(getMimeType(file.toString())) || "image/x-ms-bmp".equalsIgnoreCase(getMimeType(file.toString())); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(new File(file.getPath()).getAbsolutePath(), options); + height = options.outHeight; + width = options.outWidth; + aspect = new Rational(width, height); + rotation = getRotation(Uri.parse("file://" + file.getAbsolutePath())); + Log.d(Config.LOGTAG, "Image height: " + height + ", width: " + width + ", rotation: " + rotation + " aspect: " + aspect); + if (useAutoRotateScreen()) { + rotateScreen(width, height, rotation); + } + try { + if (gif) { + binding.messageGifView.setVisibility(View.VISIBLE); + binding.messageGifView.setImageURI(uri); + binding.messageGifView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); + } else { + binding.messageImageView.setVisibility(View.VISIBLE); + binding.messageImageView.setImage(ImageSource.uri(uri).tiling(!bmp)); + binding.messageImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); + binding.messageImageView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); + } + } catch (Exception e) { + ToastCompat.makeText(this, getString(R.string.error_file_not_found), ToastCompat.LENGTH_LONG).show(); + e.printStackTrace(); + } + } + + private void DisplayVideo(final Uri uri) { + try { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + retriever.setDataSource(uri.getPath()); + Bitmap bitmap = null; + try { + bitmap = retriever.getFrameAtTime(0); + height = bitmap.getHeight(); + width = bitmap.getWidth(); + } catch (Exception e) { + height = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); + width = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); + } finally { + if (bitmap != null) { + bitmap.recycle(); + } + } + try { + rotation = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); + } catch (Exception e) { + rotation = 0; + } + aspect = new Rational(width, height); + Log.d(Config.LOGTAG, "Video height: " + height + ", width: " + width + ", rotation: " + rotation + ", aspect: " + aspect); + if (useAutoRotateScreen()) { + rotateScreen(width, height, rotation); + } + binding.messageVideoView.setVisibility(View.VISIBLE); + player = new ExoPlayer.Builder(this).build(); + player.addListener(new Player.Listener() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + Player.Listener.super.onIsPlayingChanged(isPlaying); + if (isPlaying) { + hideFAB(); + } else { + if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { + hideFAB(); + } else { + showFAB(); + } + } + } + + @Override + public void onPlayerError(PlaybackException error) { + open(); + } + }); + player.setRepeatMode(Player.REPEAT_MODE_OFF); + binding.messageVideoView.setPlayer(player); + player.setMediaItem(MediaItem.fromUri(uri)); + player.prepare(); + player.setPlayWhenReady(true); + final MediaSessionCompat session = new MediaSessionCompat(this, getPackageName()); + final MediaSessionConnector connector = new MediaSessionConnector(session); + connector.setPlayer(player); + session.setActive(true); + requestAudioFocus(); + setVolumeControlStream(AudioManager.STREAM_MUSIC); +// binding.messageVideoView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); + } catch (Exception e) { + e.printStackTrace(); + open(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + private void PIPVideo() { + try { + binding.messageVideoView.hideController(); + binding.speedDial.setVisibility(View.GONE); + if (supportsPIP()) { + if (Compatibility.runsTwentySix()) { + final Rational rational = new Rational(width, height); + final Rational clippedRational = Rationals.clip(rational); + final PictureInPictureParams params = new PictureInPictureParams.Builder() + .setAspectRatio(clippedRational) + .build(); + this.enterPictureInPictureMode(params); + } else { + this.enterPictureInPictureMode(); + } + } + } catch (final IllegalStateException e) { + // this sometimes happens on Samsung phones (possibly when Knox is enabled) + Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); + } + } + + @Override + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); + if (isInPictureInPictureMode) { + startPlayer(); + hideFAB(); + } else { + showFAB(); + } + } + + private void releaseAudiFocus() { + AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + if (am != null) { + am.abandonAudioFocus(this); + } + } + + private void requestAudioFocus() { + AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); + if (am != null) { + am.requestAudioFocus(this, + AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } + } + + @Override + public void onBackPressed() { + if (isVideo && isPlaying() && supportsPIP()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + PIPVideo(); + } + } else { + super.onBackPressed(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + if (isVideo) { + PIPVideo(); + } + } + + private int getRotation(Uri image) { + try (final InputStream is = this.getContentResolver().openInputStream(image)) { + return is == null ? 0 : FileBackend.getRotation(is); + } catch (final Exception e) { + return 0; + } + } + + private void rotateScreen(final int width, final int height, final int rotation) { + if (width > height) { + if (rotation == 0 || rotation == 180) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } else { + if (rotation == 90 || rotation == 270) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + } + } + + private void pausePlayer() { + if (player != null && isVideo && isPlaying()) { + player.setPlayWhenReady(false); + player.getPlaybackState(); + if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { + hideFAB(); + } else { + showFAB(); + } + } + } + + private void startPlayer() { + if (player != null && isVideo && !isPlaying()) { + player.setPlayWhenReady(true); + player.getPlaybackState(); + hideFAB(); + } + } + + private void stopPlayer() { + if (player != null && isVideo) { + if (supportsPIP()) { + finishAndRemoveTask(); + } + if (isPlaying()) { + player.stop(); + } + player.release(); + if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { + hideFAB(); + } else { + showFAB(); + } + } + } + + private boolean isPlaying() { + return player != null + && player.getPlaybackState() != Player.STATE_ENDED + && player.getPlaybackState() != Player.STATE_IDLE + && player.getPlayWhenReady(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + } + + @Override + public void onResume() { + WindowManager.LayoutParams layout = getWindow().getAttributes(); + if (useMaxBrightness()) { + layout.screenBrightness = 1; + } + getWindow().setAttributes(layout); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (!isPlaying()) { + showFAB(); + } else { + hideFAB(); + } + if (!isPlaying()) { + startPlayer(); + } + super.onResume(); + } + + @Override + public void onPause() { + if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { + startPlayer(); + } else { + pausePlayer(); + } + WindowManager.LayoutParams layout = getWindow().getAttributes(); + if (useMaxBrightness()) { + layout.screenBrightness = -1; + } + getWindow().setAttributes(layout); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setRequestedOrientation(oldOrientation); + super.onPause(); + } + + @Override + public void onStop() { + stopPlayer(); + releaseAudiFocus(); + WindowManager.LayoutParams layout = getWindow().getAttributes(); + if (useMaxBrightness()) { + layout.screenBrightness = -1; + } + getWindow().setAttributes(layout); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + setRequestedOrientation(oldOrientation); + super.onStop(); + } + + @Override + protected void onBackendConnected() { + + } + + public boolean useMaxBrightness() { + // return getPreferences().getBoolean("use_max_brightness", getResources().getBoolean(R.bool.use_max_brightness)); + return false; + } + + public boolean useAutoRotateScreen() { + // return getPreferences().getBoolean("use_auto_rotate", getResources().getBoolean(R.bool.auto_rotate)); + return false; + } + + public SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } + + @Override + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.i(Config.LOGTAG, "Audio focus granted."); + } else if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { + Log.i(Config.LOGTAG, "Audio focus failed."); + } + } + + private boolean isDeletableFile(File file) { + return (file == null || !file.toString().startsWith("/") || file.canWrite()); + } + + private void showFAB() { + binding.speedDial.show(); + } + + private void hideFAB() { + binding.speedDial.hide(); + } + + private boolean supportsPIP() { + if (Compatibility.runsTwentyFour()) { + return this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } else { + return false; + } + } +} \ No newline at end of file diff --git a/src/monocleschat/res/values/strings.xml b/src/monocleschat/res/values/strings.xml index c86b05225..7490e418a 100644 --- a/src/monocleschat/res/values/strings.xml +++ b/src/monocleschat/res/values/strings.xml @@ -76,4 +76,5 @@ One participant is typing … %d participants + Open