From d5e1d61d02f0d0eaeecfc0cd75bd194cba14d736 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Sat, 9 Jul 2022 02:55:41 +0100 Subject: [PATCH] fix passing bitmap thumbnail after IMAGE_CAPTURE intent - add a new BitmapOutput, returned from the MediaOutputHelper when no output URI is specified in an IMAGE_CAPTURE intent - in this format, take the picture without actually saving it and return the bitmap - this behaviour is consistent with the implementation described in the official Android docs https://developer.android.com/training/camera/photobasics#TaskPhotoView --- .../camera/activities/MainActivity.kt | 82 +++++++---------- .../camera/extensions/ImageProxy.kt | 11 +++ .../camera/helpers/BitmapUtils.kt | 73 +++++++++++++++ .../camera/helpers/MediaOutputHelper.kt | 17 +--- .../camera/implementations/CameraXPreview.kt | 92 ++++++++++--------- .../implementations/CameraXPreviewListener.kt | 4 +- .../camera/models/MediaOutput.kt | 2 + .../camera/models/VideoQuality.kt | 5 +- 8 files changed, 173 insertions(+), 113 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/extensions/ImageProxy.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/camera/helpers/BitmapUtils.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index 2d1452e4..198e048e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -2,18 +2,14 @@ package com.simplemobiletools.camera.activities import android.app.Activity import android.content.Intent +import android.graphics.Bitmap import android.hardware.SensorManager import android.net.Uri import android.os.Bundle import android.os.Handler import android.provider.MediaStore import android.util.Log -import android.util.Size -import android.view.KeyEvent -import android.view.OrientationEventListener -import android.view.View -import android.view.Window -import android.view.WindowManager +import android.view.* import android.widget.RelativeLayout import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -22,38 +18,17 @@ import com.bumptech.glide.request.RequestOptions import com.simplemobiletools.camera.BuildConfig import com.simplemobiletools.camera.R import com.simplemobiletools.camera.extensions.config -import com.simplemobiletools.camera.helpers.FLASH_OFF -import com.simplemobiletools.camera.helpers.FLASH_ON -import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_LEFT -import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_RIGHT -import com.simplemobiletools.camera.helpers.ORIENT_PORTRAIT -import com.simplemobiletools.camera.helpers.PhotoProcessor +import com.simplemobiletools.camera.helpers.* import com.simplemobiletools.camera.implementations.CameraXInitializer import com.simplemobiletools.camera.implementations.CameraXPreviewListener import com.simplemobiletools.camera.implementations.MyCameraImpl import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.camera.views.FocusCircleView import com.simplemobiletools.commons.extensions.* -import com.simplemobiletools.commons.helpers.BROADCAST_REFRESH_MEDIA -import com.simplemobiletools.commons.helpers.PERMISSION_CAMERA -import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO -import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE -import com.simplemobiletools.commons.helpers.REFRESH_PATH -import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.Release import java.util.concurrent.TimeUnit -import kotlinx.android.synthetic.main.activity_main.btn_holder -import kotlinx.android.synthetic.main.activity_main.capture_black_screen -import kotlinx.android.synthetic.main.activity_main.change_resolution -import kotlinx.android.synthetic.main.activity_main.last_photo_video_preview -import kotlinx.android.synthetic.main.activity_main.settings -import kotlinx.android.synthetic.main.activity_main.shutter -import kotlinx.android.synthetic.main.activity_main.toggle_camera -import kotlinx.android.synthetic.main.activity_main.toggle_flash -import kotlinx.android.synthetic.main.activity_main.toggle_photo_video -import kotlinx.android.synthetic.main.activity_main.video_rec_curr_timer -import kotlinx.android.synthetic.main.activity_main.preview_view -import kotlinx.android.synthetic.main.activity_main.view_holder +import kotlinx.android.synthetic.main.activity_main.* class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener { private val TAG = "MainActivity" @@ -562,28 +537,35 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } - override fun onMediaCaptured(uri: Uri) { + override fun onMediaSaved(uri: Uri) { loadLastTakenMedia(uri) - ensureBackgroundThread { - if (isImageCaptureIntent()) { - val bitmap = contentResolver.loadThumbnail(uri, Size(30, 30), null) - Intent().apply { - data = uri - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - putExtra("data", bitmap) - setResult(Activity.RESULT_OK, this) - } - Log.w(TAG, "onMediaCaptured: exiting uri=$uri") - finish() - } else if (isVideoCaptureIntent()) { - Intent().apply { - data = uri - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - setResult(Activity.RESULT_OK, this) - } - Log.w(TAG, "onMediaCaptured: video exiting uri=$uri") - finish() + if (isImageCaptureIntent()) { + Intent().apply { + data = uri + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + setResult(Activity.RESULT_OK, this) } + Log.w(TAG, "onMediaCaptured: exiting uri=$uri") + finish() + } else if (isVideoCaptureIntent()) { + Intent().apply { + data = uri + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + setResult(Activity.RESULT_OK, this) + } + Log.w(TAG, "onMediaCaptured: video exiting uri=$uri") + finish() + } + } + + override fun onImageCaptured(bitmap: Bitmap) { + if (isImageCaptureIntent()) { + Intent().apply { + putExtra("data", bitmap) + setResult(Activity.RESULT_OK, this) + } + Log.w(TAG, "onImageCaptured: exiting bitmap size=${bitmap.byteCount}") + finish() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/ImageProxy.kt b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/ImageProxy.kt new file mode 100644 index 00000000..c83becb4 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/ImageProxy.kt @@ -0,0 +1,11 @@ +package com.simplemobiletools.camera.extensions + +import androidx.camera.core.ImageProxy + +// Only for JPEG format +fun ImageProxy.toJpegByteArray(): ByteArray { + val buffer = planes.first().buffer + val jpegImageData = ByteArray(buffer.remaining()) + buffer[jpegImageData] + return jpegImageData +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/BitmapUtils.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/BitmapUtils.kt new file mode 100644 index 00000000..6636df95 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/BitmapUtils.kt @@ -0,0 +1,73 @@ +package com.simplemobiletools.camera.helpers + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.sqrt + +//inspired by https://android.googlesource.com/platform/packages/apps/Camera2/+/refs/heads/master/src/com/android/camera/util/CameraUtil.java#244 +object BitmapUtils { + private const val TAG = "BitmapUtils" + private const val INLINE_BITMAP_MAX_PIXEL_NUM = 50 * 1024 + + fun makeBitmap(jpegData: ByteArray, maxNumOfPixels: Int = INLINE_BITMAP_MAX_PIXEL_NUM): Bitmap? { + return try { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + + BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size, options) + + if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) { + return null + } + options.inSampleSize = computeSampleSize(options, -1, maxNumOfPixels) + options.inJustDecodeBounds = false + options.inDither = false + options.inPreferredConfig = Bitmap.Config.ARGB_8888 + BitmapFactory.decodeByteArray( + jpegData, 0, jpegData.size, + options + ) + } catch (ex: OutOfMemoryError) { + Log.e(TAG, "Got oom exception ", ex) + null + } + } + + private fun computeSampleSize(options: BitmapFactory.Options, minSideLength: Int, maxNumOfPixels: Int): Int { + val initialSize = computeInitialSampleSize( + options, minSideLength, + maxNumOfPixels + ) + var roundedSize: Int + if (initialSize <= 8) { + roundedSize = 1 + while (roundedSize < initialSize) { + roundedSize = roundedSize shl 1 + } + } else { + roundedSize = (initialSize + 7) / 8 * 8 + } + return roundedSize + } + + private fun computeInitialSampleSize(options: BitmapFactory.Options, minSideLength: Int, maxNumOfPixels: Int): Int { + val w = options.outWidth.toDouble() + val h = options.outHeight.toDouble() + val lowerBound = if (maxNumOfPixels < 0) 1 else ceil(sqrt(w * h / maxNumOfPixels)).toInt() + val upperBound = if (minSideLength < 0) 128 else floor(w / minSideLength).coerceAtMost(floor(h / minSideLength)).toInt() + if (upperBound < lowerBound) { + // return the larger one when there is no overlapping zone. + return lowerBound + } + return if (maxNumOfPixels < 0 && minSideLength < 0) { + 1 + } else if (minSideLength < 0) { + lowerBound + } else { + upperBound + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaOutputHelper.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaOutputHelper.kt index 08471abd..62b29dfd 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaOutputHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaOutputHelper.kt @@ -11,20 +11,7 @@ import com.simplemobiletools.camera.extensions.getOutputMediaFile import com.simplemobiletools.camera.extensions.getRandomMediaName import com.simplemobiletools.camera.models.MediaOutput import com.simplemobiletools.commons.activities.BaseSimpleActivity -import com.simplemobiletools.commons.extensions.createDocumentUriFromRootTree -import com.simplemobiletools.commons.extensions.createDocumentUriUsingFirstParentTreeUri -import com.simplemobiletools.commons.extensions.getAndroidSAFUri -import com.simplemobiletools.commons.extensions.getDocumentFile -import com.simplemobiletools.commons.extensions.getDoesFilePathExist -import com.simplemobiletools.commons.extensions.getFileOutputStreamSync -import com.simplemobiletools.commons.extensions.getFilenameFromPath -import com.simplemobiletools.commons.extensions.getMimeType -import com.simplemobiletools.commons.extensions.hasProperStoredAndroidTreeUri -import com.simplemobiletools.commons.extensions.hasProperStoredFirstParentUri -import com.simplemobiletools.commons.extensions.hasProperStoredTreeUri -import com.simplemobiletools.commons.extensions.isAccessibleWithSAFSdk30 -import com.simplemobiletools.commons.extensions.isRestrictedSAFOnlyRoot -import com.simplemobiletools.commons.extensions.needsStupidWritePermissions +import com.simplemobiletools.commons.extensions.* import java.io.File import java.io.OutputStream @@ -56,7 +43,7 @@ class MediaOutputHelper( getMediaStoreOutput(isPhoto = true) } } else { - getMediaStoreOutput(isPhoto = true) + MediaOutput.BitmapOutput } } else { getOutputStreamMediaOutput() ?: getMediaStoreOutput(isPhoto = true) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 89bfe1ba..5a950139 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -5,34 +5,14 @@ import android.content.Context import android.hardware.SensorManager import android.hardware.display.DisplayManager import android.util.Log -import android.view.Display -import android.view.GestureDetector +import android.view.* import android.view.GestureDetector.SimpleOnGestureListener -import android.view.MotionEvent -import android.view.OrientationEventListener -import android.view.ScaleGestureDetector -import android.view.Surface import androidx.appcompat.app.AppCompatActivity -import androidx.camera.core.AspectRatio -import androidx.camera.core.Camera -import androidx.camera.core.CameraSelector -import androidx.camera.core.CameraState -import androidx.camera.core.DisplayOrientedMeteringPointFactory -import androidx.camera.core.FocusMeteringAction -import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY -import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO -import androidx.camera.core.ImageCapture.FLASH_MODE_OFF -import androidx.camera.core.ImageCapture.FLASH_MODE_ON -import androidx.camera.core.ImageCapture.Metadata -import androidx.camera.core.ImageCapture.OnImageSavedCallback -import androidx.camera.core.ImageCapture.OutputFileOptions -import androidx.camera.core.ImageCapture.OutputFileResults -import androidx.camera.core.ImageCaptureException -import androidx.camera.core.Preview -import androidx.camera.core.UseCase +import androidx.camera.core.* +import androidx.camera.core.ImageCapture.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.* +import androidx.camera.video.VideoCapture import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.core.view.doOnLayout @@ -359,29 +339,53 @@ class CameraXPreview( } val mediaOutput = mediaOutputHelper.getImageMediaOutput() - val outputOptionsBuilder = when (mediaOutput) { - is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues) - is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream) - else -> throw IllegalArgumentException("Unexpected option for image") + + if (mediaOutput is MediaOutput.BitmapOutput) { + imageCapture.takePicture(mainExecutor, object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + listener.toggleBottomButtons(false) + val bitmap = BitmapUtils.makeBitmap(image.toJpegByteArray()) + if (bitmap != null) { + listener.onImageCaptured(bitmap) + } else { + cameraErrorHandler.handleImageCaptureError(ImageCapture.ERROR_CAPTURE_FAILED) + } + } + + override fun onError(exception: ImageCaptureException) { + handleImageCaptureError(exception) + } + }) + } else { + val outputOptionsBuilder = when (mediaOutput) { + is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues) + is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream) + is MediaOutput.BitmapOutput -> throw IllegalStateException("Cannot produce an OutputFileOptions for a bitmap output") + else -> throw IllegalArgumentException("Unexpected option for image ") + } + + val outputOptions = outputOptionsBuilder.setMetadata(metadata).build() + + imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback { + override fun onImageSaved(outputFileResults: OutputFileResults) { + listener.toggleBottomButtons(false) + listener.onMediaSaved(mediaOutput.uri ?: outputFileResults.savedUri!!) + } + + override fun onError(exception: ImageCaptureException) { + handleImageCaptureError(exception) + } + }) } - - val outputOptions = outputOptionsBuilder.setMetadata(metadata).build() - - imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback { - override fun onImageSaved(outputFileResults: OutputFileResults) { - listener.toggleBottomButtons(false) - listener.onMediaCaptured(mediaOutput.uri ?: outputFileResults.savedUri!!) - } - - override fun onError(exception: ImageCaptureException) { - Log.e(TAG, "Error", exception) - listener.toggleBottomButtons(false) - cameraErrorHandler.handleImageCaptureError(exception.imageCaptureError) - } - }) playShutterSoundIfEnabled() } + private fun handleImageCaptureError(exception: ImageCaptureException) { + Log.e(TAG, "Error", exception) + listener.toggleBottomButtons(false) + cameraErrorHandler.handleImageCaptureError(exception.imageCaptureError) + } + override fun initPhotoMode() { isPhotoCapture = true startCamera() @@ -441,7 +445,7 @@ class CameraXPreview( Log.e(TAG, "recording failed:", recordEvent.cause) cameraErrorHandler.handleVideoRecordingError(recordEvent.error) } else { - listener.onMediaCaptured(mediaOutput.uri ?: recordEvent.outputResults.outputUri) + listener.onMediaSaved(mediaOutput.uri ?: recordEvent.outputResults.outputUri) } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt index b5758137..73bf5db5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -1,5 +1,6 @@ package com.simplemobiletools.camera.implementations +import android.graphics.Bitmap import android.net.Uri interface CameraXPreviewListener { @@ -8,7 +9,8 @@ interface CameraXPreviewListener { fun setFlashAvailable(available: Boolean) fun onChangeCamera(frontCamera: Boolean) fun toggleBottomButtons(hide:Boolean) - fun onMediaCaptured(uri: Uri) + fun onMediaSaved(uri: Uri) + fun onImageCaptured(bitmap: Bitmap) fun onChangeFlashMode(flashMode: Int) fun onVideoRecordingStarted() fun onVideoRecordingStopped() diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/models/MediaOutput.kt b/app/src/main/kotlin/com/simplemobiletools/camera/models/MediaOutput.kt index d05a74ec..c4e1e8d0 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/models/MediaOutput.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/models/MediaOutput.kt @@ -22,4 +22,6 @@ sealed class MediaOutput( val fileDescriptor: ParcelFileDescriptor, override val uri: Uri, ) : MediaOutput(uri) + + object BitmapOutput : MediaOutput(null) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt b/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt index 9fce732f..ec2a536b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt @@ -9,10 +9,9 @@ enum class VideoQuality(val width: Int, val height: Int) { HD(1280, 720), SD(720, 480); - val pixels: Int = width * height - val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / VideoQuality.ONE_MEGA_PIXELS) + val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / VideoQuality.ONE_MEGA_PIXEL) val ratio = width / height.toFloat() @@ -54,6 +53,6 @@ enum class VideoQuality(val width: Int, val height: Int) { } companion object { - private const val ONE_MEGA_PIXELS = 1000000 + private const val ONE_MEGA_PIXEL = 1000000 } }