Browse Source

fix displaying of large images

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 4 years ago
parent
commit
e03f048126

+ 1 - 0
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', {

+ 31 - 9
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
+    }
 }

+ 112 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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
+    }
+}