view app/src/main/java/com/bartsent/simpleresizer/EditImage.kt @ 15:20da616dcda0

Add preferences.
author David Barts <n5jrn@me.com>
date Thu, 18 Feb 2021 22:12:19 -0800
parents 678adef4774f
children 3ed74dc0e34a
line wrap: on
line source

package com.bartsent.simpleresizer

import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.ProgressBar
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import com.bartsent.simpleresizer.databinding.ActivityEditImageBinding
import com.bartsent.simpleresizer.lib.getScaledInstance
import java.io.File
import java.io.IOException
import kotlin.concurrent.thread
import androidx.preference.PreferenceManager

class EditImage : AppCompatActivity() {
    private object State {
        var uri: Uri? = null
        var bitmap: Bitmap? = null
    }

    private val STDDIMS = arrayOf<Int>(1600, 1280, 1024, 800, 640, 512, 400, 320).apply {
        sort()
    }

    private lateinit var binding: ActivityEditImageBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityEditImageBinding.inflate(layoutInflater)
        setContentView(binding.root)
        PreferenceManager.setDefaultValues(applicationContext, R.xml.root_preferences, false)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_edit, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.settings_item) {
            startActivity(
                Intent(Intent.ACTION_APPLICATION_PREFERENCES, null, this,
                    SettingsActivity::class.java))
            return true
        }
        return false
    }

    // Cribbed from: https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content
    private fun getFileName(uri: Uri): String? {
        var result: String? = null
        if (uri.scheme == "content") {
            contentResolver.query(uri, null, null, null, null).use { cursor ->
                if (cursor != null && cursor.moveToFirst())
                    result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            }
        }
        if (result == null) {
            val uriPath = uri.path
            result = uriPath?.substring(uriPath.lastIndexOf('/') + 1)
        }
        return result
    }

    private fun showError(message: String): Unit {
        AlertDialog.Builder(this).also {
            it.setMessage(message)
            it.setNeutralButton(R.string.ok_text) { dialog, _ ->
                dialog.dismiss()
            }
            it.setOnDismissListener {
                finish()
            }
            it.create()
        }.show()
    }

    override fun onResume() {
        super.onResume()

        // Read the URI, die if we can't.
        val imageUri = intent?.data
        if (imageUri == null) {
            showError(getString(R.string.error_no_uri))
            return
        }

        // Being stateful stops data loss when the phone gets rotated.
        if (imageUri != State.uri) {
            State.uri = imageUri
            State.bitmap = contentResolver.openInputStream(imageUri).use {
                BitmapFactory.decodeStream(it)
            }
        }
        if (State.bitmap == null) {
            showError(getString(R.string.error_bad_image))
            return
        }
        setImage(State.bitmap!!)
    }

    fun setImage(image: Bitmap): Unit {
        binding.imageSize.text = getString(R.string.image_size_text, image.width, image.height)
        binding.image.setImageBitmap(image)
        State.bitmap = image
    }

    fun scaleClicked(view: View): Unit {
        val maxSize = State.bitmap!!.run { maxOf(width, height) }
        PopupMenu(this, view).apply {
            menu.run {
                STDDIMS.filter { it < maxSize }.forEach { add(it.toString()) }
                add(getString(R.string.custom_text))
                add(getString(R.string.cancel_text))
            }
            setOnMenuItemClickListener(::scaleMenuItemClicked)
            show()
        }
    }

    fun scaleMenuItemClicked(item: MenuItem) : Boolean {
        val itString = item.title.toString()
        var itVal = itString.toIntOrNull()
        if (itVal == null) {
            return when (itString) {
                getString(R.string.cancel_text) -> true
                getString(R.string.custom_text) -> { showCustomScaleDialog(); true }
                else -> false
            }
        }
        doScale(itVal)
        return true
    }

    private fun copyColorSpace(old: Bitmap, new: Bitmap): Unit {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            val oldColorSpace = old.colorSpace
            if (oldColorSpace != null)
                new.setColorSpace(oldColorSpace)
        }
    }

    fun doScale(newMax: Int): Unit {
        val oldBitmap = State.bitmap!!
        val factor = newMax.toDouble() / maxOf(oldBitmap.width, oldBitmap.height).toDouble()
        if (factor >= 1.0) {
            throw IllegalArgumentException("can only scale down")
        }
        val scaleType = PreferenceManager.getDefaultSharedPreferences(applicationContext).getString(
            "scale_type", "speed" )
        Log.d("EditImage", "scaling, scale_type = $scaleType")
        binding.progressBar.visibility = ProgressBar.VISIBLE
        thread {
            val newWidth = (oldBitmap.width.toDouble() * factor + 0.5).toInt()
            val newHeight = (oldBitmap.height.toDouble() * factor + 0.5).toInt()
            val newBitmap = if (scaleType == "quality")
                oldBitmap.getScaledInstance(newWidth, newHeight)
            else
                Bitmap.createScaledBitmap(oldBitmap, newWidth, newHeight, true)
            runOnUiThread {
                binding.progressBar.visibility = ProgressBar.INVISIBLE
                setImage(newBitmap)
            }
        }
    }

    // is there any way to remember the last scale value?
    // if so: should we?

    fun showCustomScaleDialog(): Unit {
        val image = State.bitmap!!
        val curMaxDim = maxOf(image.width, image.height)
        val dialogView = layoutInflater.inflate(R.layout.dialog_custom_scale, null)
        AlertDialog.Builder(this).also {
            it.setPositiveButton(R.string.ok_text) { dialog, _ ->
                val maxDim = dialogView.findViewById<EditText>(R.id.custom_scale)?.text.toString().toIntOrNull()
                dialog.dismiss()
                if (maxDim == null || maxDim < 8 || maxDim >= curMaxDim) {
                    AlertDialog.Builder(this).also {
                        it.setMessage(R.string.bad_scale)
                        it.setNeutralButton(R.string.ok_text) { dialog, _ ->
                            dialog.dismiss()
                        }
                        it.create()
                    }.show()
                } else {
                    doScale(maxDim)
                }
            }
            it.setNegativeButton(R.string.cancel_text) { dialog, _ ->
                dialog.dismiss()
            }
            it.setView(dialogView)
            it.create()
        }.show()
    }

    fun rotateClicked(view: View): Unit {
        PopupMenu(this, view).apply {
            setOnMenuItemClickListener {
                when(it.itemId) {
                    R.id.r_90_cw -> {
                        doRotate(90)
                        true
                    }
                    R.id.r_180 -> {
                        doRotate(180)
                        true
                    }
                    R.id.r_90_ccw -> {
                        doRotate(270)
                        true
                    }
                    R.id.r_cancel -> true
                    else -> false
                }
            }
            inflate(R.menu.rotate)
            show()
        }
    }

    fun doRotate(deg: Int): Unit {
        val oldBitmap = State.bitmap!!
        if (deg % 90 != 0) {
            throw IllegalArgumentException("$deg not a multiple of 90")
        }
        val (w, h) = if (deg % 180 == 0) Pair(oldBitmap.width, oldBitmap.height) else Pair(oldBitmap.height, oldBitmap.width)
        val rotater = Matrix().apply {
            setRotate(deg.toFloat(), oldBitmap.width.toFloat()/2.0f, oldBitmap.height.toFloat()/2.0f)
            postTranslate((w - oldBitmap.width).toFloat()/2.0f, (h - oldBitmap.height).toFloat()/2.0f)
        }
        binding.progressBar.visibility = ProgressBar.VISIBLE
        thread {
            val newBitmap = Bitmap.createBitmap(w, h, oldBitmap.config)
            copyColorSpace(oldBitmap, newBitmap)
            Canvas(newBitmap).run {
                drawBitmap(oldBitmap, rotater, null)
            }
            runOnUiThread {
                binding.progressBar.visibility = ProgressBar.INVISIBLE
                setImage(newBitmap)
            }
        }
    }

     fun cancelClicked(view: View): Unit {
        State.uri = null
        State.bitmap = null
        finish()
    }

    fun doneClicked(view: View): Unit {
        val contentValues = ContentValues().apply {
            var fileName = getFileName(State.uri!!)
            if (fileName == null) {
                val d = java.util.Date()
                fileName = "IMG_%tY%tm%td_%tH%tM%tS.jpg".format(d, d, d, d, d, d)
            } else {
                val dot = fileName.lastIndexOf('.')
                if (dot == -1) {
                    fileName = fileName + ".jpg"
                } else {
                    val fileExt = fileName.substring(dot).toLowerCase()
                    if (fileExt !in setOf(".jpg", ".jpeg"))
                        fileName = fileName.substring(0..dot) + ".jpg"
                }
            }
            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                put(MediaStore.MediaColumns.RELATIVE_PATH,
                    File(Environment.DIRECTORY_PICTURES, getString(R.string.app_name)).getPath())
            }
        }
        try {
            val myUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            if (myUri == null) {
                throw IOException(getString(R.string.error_create_mediastore))
            }
            val stream = contentResolver.openOutputStream(myUri)
            if (stream == null) {
                throw IOException(getString(R.string.error_get_output))
            }
            val quality = maxOf(0, minOf(100,
                PreferenceManager.getDefaultSharedPreferences(applicationContext).getInt(
                    "jpeg_quality", 85)))
            Log.d("EditImage", "saving, jpeg_quality = $quality")
            stream.use {
                if (!State.bitmap!!.compress(Bitmap.CompressFormat.JPEG, quality, it)) {
                    throw IOException(getString(R.string.error_save_bitmap))
                }
            }
        } catch (ioe: IOException) {
            showError(ioe.message ?: getString(R.string.error_io))
        }
        finish()
    }
}