From e03f0481265632540e96be70a762eafffa9aa925 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 21 May 2021 15:39:17 +0200 Subject: [PATCH] fix displaying of large images Signed-off-by: Marcel Hibbe --- app/build.gradle | 1 + .../activities/FullScreenImageActivity.kt | 40 +++++-- .../nextcloud/talk/utils/BitmapShrinker.kt | 112 ++++++++++++++++++ 3 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt diff --git a/app/build.gradle b/app/build.gradle index cb29cf758..b90793b21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,6 +186,7 @@ dependencies { implementation "androidx.work:work-runtime:${workVersion}" implementation "androidx.work:work-rxjava2:${workVersion}" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.exifinterface:exifinterface:1.3.2' androidTestImplementation "androidx.work:work-testing:${workVersion}" implementation 'com.google.android:flexbox:2.0.1' implementation ('com.gitlab.bitfireAT:dav4jvm:2.1.2', { diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt index 36649411a..8edeeeee0 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt @@ -25,29 +25,26 @@ package com.nextcloud.talk.activities import android.content.Intent -import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding +import com.nextcloud.talk.utils.BitmapShrinker import pl.droidsonroids.gif.GifDrawable import java.io.File class FullScreenImageActivity : AppCompatActivity() { lateinit var binding: ActivityFullScreenImageBinding - private lateinit var path: String - private var showFullscreen = false - private val maxScale = 6.0f - private val mediumScale = 2.45f - override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_preview, menu) return true @@ -98,8 +95,8 @@ class FullScreenImageActivity : AppCompatActivity() { // Enable enlarging the image more than default 3x maximumScale. // Medium scale adapted to make double-tap behaviour more consistent. - binding.photoView.maximumScale = maxScale - binding.photoView.mediumScale = mediumScale + binding.photoView.maximumScale = MAX_SCALE + binding.photoView.mediumScale = MEDIUM_SCALE val fileName = intent.getStringExtra("FILE_NAME") val isGif = intent.getBooleanExtra("IS_GIF", false) @@ -116,7 +113,25 @@ class FullScreenImageActivity : AppCompatActivity() { } else { binding.gifView.visibility = View.INVISIBLE binding.photoView.visibility = View.VISIBLE - binding.photoView.setImageURI(Uri.parse(path)) + displayImage(path) + } + } + + private fun displayImage(path: String) { + val displayMetrics = applicationContext.resources.displayMetrics + val doubleScreenWidth = displayMetrics.widthPixels * 2 + val doubleScreenHeight = displayMetrics.heightPixels * 2 + + val bitmap = BitmapShrinker.shrinkBitmap(path, doubleScreenWidth, doubleScreenHeight) + + val bitmapSize: Int = bitmap.byteCount + + // info that 100MB is the limit comes from https://stackoverflow.com/a/53334563 + if (bitmapSize > HUNDRED_MB) { + Log.e(TAG, "bitmap will be too large to display. It won't be displayed to avoid RuntimeException") + Toast.makeText(this, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } else { + binding.photoView.setImageBitmap(bitmap) } } @@ -149,4 +164,11 @@ class FullScreenImageActivity : AppCompatActivity() { or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN ) } + + companion object { + private val TAG = "FullScreenImageActivity" + private const val HUNDRED_MB = 100 * 1024 * 1024 + private const val MAX_SCALE = 6.0f + private const val MEDIUM_SCALE = 2.45f + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt new file mode 100644 index 000000000..bbe835eb5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt @@ -0,0 +1,112 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.util.Log +import androidx.exifinterface.media.ExifInterface +import java.io.IOException + +object BitmapShrinker { + + private val TAG = "BitmapShrinker" + private const val DEGREES_90 = 90f + private const val DEGREES_180 = 180f + private const val DEGREES_270 = 270f + + fun shrinkBitmap( + path: String, + reqWidth: Int, + reqHeight: Int + ): Bitmap { + val bitmap = decodeBitmap(path, reqWidth, reqHeight) + return rotateBitmap(path, bitmap) + } + + // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap + private fun decodeBitmap( + path: String, + requestedWidth: Int, + requestedHeight: Int + ): Bitmap { + return BitmapFactory.Options().run { + inJustDecodeBounds = true + BitmapFactory.decodeFile(path, this) + inSampleSize = getInSampleSize(this, requestedWidth, requestedHeight) + inJustDecodeBounds = false + BitmapFactory.decodeFile(path, this) + } + } + + // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap + private fun getInSampleSize( + options: BitmapFactory.Options, + requestedWidth: Int, + requestedHeight: Int + ): Int { + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + if (height > requestedHeight || width > requestedWidth) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + // "||" was used instead of "&&". Otherwise it would still crash for wide panorama photos. + while (halfHeight / inSampleSize >= requestedHeight || halfWidth / inSampleSize >= requestedWidth) { + inSampleSize *= 2 + } + } + return inSampleSize + } + + // solution inspired by https://stackoverflow.com/a/15341203 + private fun rotateBitmap(path: String, bitmap: Bitmap): Bitmap { + try { + val exif = ExifInterface(path) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> { + matrix.postRotate(DEGREES_90) + } + ExifInterface.ORIENTATION_ROTATE_180 -> { + matrix.postRotate(DEGREES_180) + } + ExifInterface.ORIENTATION_ROTATE_270 -> { + matrix.postRotate(DEGREES_270) + } + } + val rotatedBitmap = Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.getWidth(), + bitmap.getHeight(), + matrix, + true + ) + return rotatedBitmap + } catch (e: IOException) { + Log.e(TAG, "error while rotating image", e) + } + return bitmap + } +}