package se.ajpanton.notificationlog import android.app.Dialog import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.Window import android.view.WindowManager import android.widget.FrameLayout import android.widget.ProgressBar import android.widget.TextView import androidx.core.os.bundleOf import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import se.ajpanton.notificationlog.data.EncryptedImageStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Full-screen image viewer kept within MainActivity so opening it does not re-lock the app. */ class ImageViewerDialogFragment : DialogFragment() { private lateinit var root: FrameLayout private lateinit var image: ZoomImageView private lateinit var progress: ProgressBar private var controlsVisible = false override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { root = FrameLayout(requireContext()).apply { setBackgroundColor(Color.BLACK) } image = ZoomImageView(requireContext()).apply { visibility = View.GONE onSingleTap = ::toggleSystemControls } progress = ProgressBar(requireContext()).apply { isIndeterminate = true } root.addView(image, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) root.addView(progress, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)) return Dialog(requireContext()).apply { requestWindowFeature(Window.FEATURE_NO_TITLE) setContentView(root) } } override fun onStart() { super.onStart() dialog?.window?.apply { setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) setBackgroundDrawable(ColorDrawable(Color.BLACK)) WindowCompat.setDecorFitsSystemWindows(this, false) statusBarColor = Color.TRANSPARENT navigationBarColor = Color.TRANSPARENT decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION attributes = attributes.apply { layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } addFlags(WindowManager.LayoutParams.FLAG_SECURE) } hideSystemControls() loadImage(requireArguments().getString(ARGUMENT_IMAGE_ID)!!) } private fun loadImage(imageId: String) { val targetWidth = resources.displayMetrics.widthPixels val targetHeight = resources.displayMetrics.heightPixels val appContext = requireContext().applicationContext lifecycleScope.launch { val bitmap = withContext(Dispatchers.IO) { runCatching { EncryptedImageStore(appContext).read(imageId)?.let { bytes -> decodeForViewer(bytes, targetWidth, targetHeight) } }.getOrNull() } if (!isAdded) return@launch progress.visibility = View.GONE if (bitmap == null) { root.addView(TextView(requireContext()).apply { text = "[image]" setTextColor(Color.WHITE) textSize = 18f }, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)) } else { image.setImageBitmap(bitmap) image.visibility = View.VISIBLE } } } private fun decodeForViewer(bytes: ByteArray, targetWidth: Int, targetHeight: Int): Bitmap? { val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds) var sampleSize = 1 while (bounds.outWidth / sampleSize > targetWidth * MAX_SCREEN_SCALE || bounds.outHeight / sampleSize > targetHeight * MAX_SCREEN_SCALE ) sampleSize *= 2 return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, BitmapFactory.Options().apply { inSampleSize = sampleSize }) } private fun toggleSystemControls() { if (controlsVisible) hideSystemControls() else showSystemControls() } private fun hideSystemControls() { val window = dialog?.window ?: return WindowInsetsControllerCompat(window, root).apply { systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE hide(WindowInsetsCompat.Type.systemBars()) } controlsVisible = false } private fun showSystemControls() { val window = dialog?.window ?: return WindowInsetsControllerCompat(window, root).show(WindowInsetsCompat.Type.systemBars()) controlsVisible = true } companion object { private const val ARGUMENT_IMAGE_ID = "image_id" private const val MAX_SCREEN_SCALE = 4 fun newInstance(imageId: String) = ImageViewerDialogFragment().apply { arguments = bundleOf(ARGUMENT_IMAGE_ID to imageId) } } }