SmashKs/OneShoot

View on GitHub
presentation/src/main/java/smash/ks/com/oneshoot/features/photograph/TakeAPicFragment.kt

Summary

Maintainability
D
1 day
Test Coverage
/*
 * Copyright (C) 2019 The Smash Ks Open Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package smash.ks.com.oneshoot.features.photograph

import android.Manifest.permission.CAMERA
import android.Manifest.permission.RECORD_AUDIO
import android.animation.AnimatorInflater
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat.JPEG
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.KeyEvent.KEYCODE_BACK
import android.widget.Toast.LENGTH_SHORT
import android.widget.Toast.makeText
import androidx.annotation.IdRes
import androidx.core.app.ActivityCompat.requestPermissions
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.devrapid.dialogbuilder.support.QuickDialogFragment
import com.devrapid.kotlinknifer.displayPixels
import com.devrapid.kotlinknifer.toBitmap
import com.devrapid.kotlinknifer.visible
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraView
import com.otaliastudios.cameraview.Flash.AUTO
import com.otaliastudios.cameraview.Flash.OFF
import com.otaliastudios.cameraview.Flash.ON
import com.otaliastudios.cameraview.PictureResult
import kotlinx.android.synthetic.main.dialog_fragment_options.view.ib_analyze
import kotlinx.android.synthetic.main.dialog_fragment_options.view.ib_upload
import kotlinx.android.synthetic.main.dialog_fragment_options.view.iv_snippet
import kotlinx.android.synthetic.main.fragment_take_a_pic.cv_camera
import kotlinx.android.synthetic.main.fragment_take_a_pic.ib_flash
import kotlinx.android.synthetic.main.fragment_take_a_pic.iv_preview
import kotlinx.android.synthetic.main.fragment_take_a_pic.sav_selection
import kotlinx.android.synthetic.main.fragment_take_a_pic.v_flash
import kotlinx.android.synthetic.main.merge_bottom_shot_bar.fab_shot
import kotlinx.coroutines.delay
import org.jetbrains.anko.collections.forEachWithIndex
import org.jetbrains.anko.sdk25.coroutines.onClick
import smash.ks.com.ext.const.Constant.CAMERA_QUALITY
import smash.ks.com.ext.const.Constant.DEBOUNCE_DELAY
import smash.ks.com.ext.const.DEFAULT_INT
import smash.ks.com.oneshoot.R
import smash.ks.com.oneshoot.bases.AdvFragment
import smash.ks.com.oneshoot.ext.image.glide.loadByAny
import smash.ks.com.oneshoot.ext.resource.gStrings
import smash.ks.com.oneshoot.features.fake.FakeFragment.Parameter.REQUEST_CAMERA_PERMISSION
import java.io.ByteArrayOutputStream
import kotlin.math.roundToInt
import kotlinx.android.synthetic.main.dialog_fragment_options.view.ib_close as option_close

class TakeAPicFragment : AdvFragment<PhotographActivity, TakeAPicViewModel>() {
    //region Static parameters
    companion object Parameter {
        // The key name of the fragment initialization parameters.
        const val ARG_IMAGE_DATA = "param image data array"

        private const val DIALOG_FRAGMENT_WIDTH_RATIO = .85f
        private const val DIALOG_FRAGMENT_HEIGHT_RATIO = .65f
    }
    //endregion

    //region *** Private Variable ***
    private lateinit var byteArrayPhoto: ByteArray
    private var selectionDialog: QuickDialogFragment? = null
    private var shotDebounce = false
    private var prevDebounce = false
    private val flashCycle by lazy {
        listOf(OFF to R.drawable.ic_flash_off,
               ON to R.drawable.ic_flash_on,
               AUTO to R.drawable.ic_flash_auto)
    }
    private val cameraCallback by lazy {
        object : CameraListener() {
            override fun onPictureTaken(result: PictureResult) {
                scaleBitmap(result.data)
            }
        }
    }
    private val selectedRectF = object {
        var x = 0
        var y = 0
        var w = 0
        var h = 0
    }
    //endregion

    //region Fragment Lifecycle
    override fun onResume() {
        super.onResume()

        // Request the authority of the camera.
        when {
            checkSelfPermission(parent, CAMERA) == PERMISSION_GRANTED &&
            checkSelfPermission(parent, RECORD_AUDIO) == PERMISSION_GRANTED -> cv_camera.open()
            shouldShowRequestPermissionRationale(CAMERA) -> QuickDialogFragment.Builder(this) {
                message = gStrings(R.string.camera_permission_confirmation)
                btnPositiveText = "Ok" to { _ ->
                    requestPermissions(parent, arrayOf(CAMERA, RECORD_AUDIO), REQUEST_CAMERA_PERMISSION)
                }
                btnNegativeText = "Deny" to { _ ->
                    makeText(parent, gStrings(R.string.camera_permission_not_granted), LENGTH_SHORT).show()
                }
            }.build().show()
            else -> requestPermissions(parent, arrayOf(CAMERA, RECORD_AUDIO), REQUEST_CAMERA_PERMISSION)
        }
    }

    override fun onPause() {
        super.onPause()

        cv_camera.close()
    }

    override fun onDestroy() {
        super.onDestroy()

        selectionDialog?.takeIf(Fragment::isVisible)?.dismiss()
        selectionDialog = null
        cv_camera?.let(CameraView::destroy)
    }
    //endregion

    //region Base Fragment
    override fun rendered(savedInstanceState: Bundle?) {
        cv_camera.apply {
            setLifecycleOwner(this@TakeAPicFragment)
            clearCameraListeners()
            addCameraListener(cameraCallback)
        }
        fab_shot.setOnClickListener {
            if (!shotDebounce) {
                shotDebounce = true
                cv_camera.takePicture()
                makeCameraFlashEffecting()
            }
        }
        ib_flash.apply {
            currentFlashState()?.second?.let(::setImageResource)
            setOnClickListener {
                val state = nextFlashState()

                cv_camera.flash = state.first
                ib_flash.setImageResource(state.second)
            }
        }
        iv_preview.apply {
            setOnClickListener {
                if (!prevDebounce) {
                    prevDebounce = true
                    showSelectionDialog(drawable.toBitmap())
                }
            }
        }
        sav_selection.selectedAreaCallback = { x, y, w, h ->
            selectedRectF.x = x
            selectedRectF.y = y
            selectedRectF.w = w
            selectedRectF.h = h
        }
    }

    override fun provideInflateView() = R.layout.fragment_take_a_pic
    //endregion

    //region Showing From ViewModel
    private fun showSelectionDialog(bitmap: Bitmap) {
        selectionDialog = QuickDialogFragment.Builder(this) {
            var debouncing = false

            viewResCustom = R.layout.dialog_fragment_options
            cancelable = false
            onStartBlock = {
                val (width, height) = requireNotNull(it.activity?.displayPixels())
                val realWidth = width * DIALOG_FRAGMENT_WIDTH_RATIO
                val realHeight = height * DIALOG_FRAGMENT_HEIGHT_RATIO
                it.dialog?.window?.apply {
                    setWindowAnimations(R.style.KsDialog)
                    setLayout(realWidth.roundToInt(), realHeight.roundToInt())
                }
            }
            fetchComponents = { v, df ->
                fun navigateTo(@IdRes navigationAction: Int) {
                    dismissOptionDialog()
                    view?.findNavController()?.navigate(navigationAction, bundleOf(ARG_IMAGE_DATA to byteArrayPhoto))
                }

                v.apply {
                    iv_snippet.loadByAny(bitmap)
                    option_close.onClick {
                        if (false == debouncing) {
                            debouncing = true
                            delay(DEBOUNCE_DELAY)
                            dismissOptionDialog()
                        }
                    }
                    ib_analyze.onClick { navigateTo(R.id.action_takeAPicFragment_to_analyzeFragment) }
                    ib_upload.onClick { navigateTo(R.id.action_takeAPicFragment_to_uploadPicFragment) }
                }

                // For touch the back press key then close the dialog fragment.
                df.dialog?.setOnKeyListener { _, keyCode, _ ->
                    when (keyCode) {
                        KEYCODE_BACK -> {
                            dismissOptionDialog()
                            true
                        }
                        else -> false
                    }
                }

                // Open the dialog well then the debounce can re-trigger again.
                shotDebounce = false
                prevDebounce = false
            }
        }.build()

        selectionDialog?.takeUnless(Fragment::isVisible)?.show()
    }

    private fun dismissOptionDialog() {
        selectionDialog?.dismiss()
        selectionDialog = null
    }
    //endregion

    //region Camera Effective
    private fun makeCameraFlashEffecting() {
        if (!v_flash.isVisible) v_flash.visible()
        AnimatorInflater.loadAnimator(requireContext(), R.animator.camera_flash)
            .apply { setTarget(v_flash) }
            .start()
    }

    private fun currentFlashState() = flashCycle.find { cv_camera.flash == it.first }

    private fun currentFlashStateIndex(): Int {
        var currentIndex = DEFAULT_INT

        flashCycle.forEachWithIndex { index, flash ->
            if (cv_camera.flash == flash.first) currentIndex = index
        }

        return currentIndex
    }

    private fun nextFlashState() = flashCycle[(currentFlashStateIndex() + 1) % flashCycle.size]
    //endregion

    private fun scaleBitmap(data: ByteArray) {
        BitmapFactory.decodeByteArray(data, 0, data.size).also { bmp ->
            selectedRectF.apply {
                // Round the x, y, width, and height for avoiding the range is over than bitmap size.
                var roundWidth = (x + w).let { if (it > bmp.width) bmp.width - x else w }
                var roundHeight = (y + h).let { if (it > bmp.height) bmp.height - y else h }
                val roundX = x.takeIf { 0 < it } ?: let { roundWidth = w + x; 0 }
                val roundY = y.takeIf { 0 < it } ?: let { roundHeight = h + y; 0 }

                val ratioX = bmp.width.toFloat() / cv_camera.width
                val ratioY = bmp.height.toFloat() / cv_camera.height

                val bitmap = Bitmap.createBitmap(bmp,
                                                 (roundX * ratioX).toInt(),
                                                 (roundY * ratioY).toInt(),
                                                 (roundWidth * ratioX).toInt(),
                                                 (roundHeight * ratioY).toInt())

                // Cropped bitmap translates to byte array again.
                val stream = ByteArrayOutputStream()
                bitmap.compress(JPEG, CAMERA_QUALITY, stream)
                byteArrayPhoto = stream.toByteArray()
                // Let image view and dialog show the bitmap.
                iv_preview.loadByAny(bitmap)
                showSelectionDialog(bitmap)
            }
        }.recycle()
    }
}