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
This commit is contained in:
darthpaul 2022-07-09 02:55:41 +01:00
parent 74e2656831
commit d5e1d61d02
8 changed files with 173 additions and 113 deletions

View File

@ -2,18 +2,14 @@ package com.simplemobiletools.camera.activities
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.hardware.SensorManager import android.hardware.SensorManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.util.Size import android.view.*
import android.view.KeyEvent
import android.view.OrientationEventListener
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.RelativeLayout import android.widget.RelativeLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.BuildConfig
import com.simplemobiletools.camera.R import com.simplemobiletools.camera.R
import com.simplemobiletools.camera.extensions.config import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.helpers.FLASH_OFF import com.simplemobiletools.camera.helpers.*
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.implementations.CameraXInitializer import com.simplemobiletools.camera.implementations.CameraXInitializer
import com.simplemobiletools.camera.implementations.CameraXPreviewListener import com.simplemobiletools.camera.implementations.CameraXPreviewListener
import com.simplemobiletools.camera.implementations.MyCameraImpl import com.simplemobiletools.camera.implementations.MyCameraImpl
import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.camera.views.FocusCircleView import com.simplemobiletools.camera.views.FocusCircleView
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.BROADCAST_REFRESH_MEDIA import com.simplemobiletools.commons.helpers.*
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.models.Release import com.simplemobiletools.commons.models.Release
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.android.synthetic.main.activity_main.btn_holder import kotlinx.android.synthetic.main.activity_main.*
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
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener { class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
private val TAG = "MainActivity" 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) loadLastTakenMedia(uri)
ensureBackgroundThread { if (isImageCaptureIntent()) {
if (isImageCaptureIntent()) { Intent().apply {
val bitmap = contentResolver.loadThumbnail(uri, Size(30, 30), null) data = uri
Intent().apply { flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
data = uri setResult(Activity.RESULT_OK, this)
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()
} }
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()
} }
} }

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -11,20 +11,7 @@ import com.simplemobiletools.camera.extensions.getOutputMediaFile
import com.simplemobiletools.camera.extensions.getRandomMediaName import com.simplemobiletools.camera.extensions.getRandomMediaName
import com.simplemobiletools.camera.models.MediaOutput import com.simplemobiletools.camera.models.MediaOutput
import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.createDocumentUriFromRootTree import com.simplemobiletools.commons.extensions.*
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 java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
@ -56,7 +43,7 @@ class MediaOutputHelper(
getMediaStoreOutput(isPhoto = true) getMediaStoreOutput(isPhoto = true)
} }
} else { } else {
getMediaStoreOutput(isPhoto = true) MediaOutput.BitmapOutput
} }
} else { } else {
getOutputStreamMediaOutput() ?: getMediaStoreOutput(isPhoto = true) getOutputStreamMediaOutput() ?: getMediaStoreOutput(isPhoto = true)

View File

@ -5,34 +5,14 @@ import android.content.Context
import android.hardware.SensorManager import android.hardware.SensorManager
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.util.Log import android.util.Log
import android.view.Display import android.view.*
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener 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.appcompat.app.AppCompatActivity
import androidx.camera.core.AspectRatio import androidx.camera.core.*
import androidx.camera.core.Camera import androidx.camera.core.ImageCapture.*
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.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.* import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
@ -359,29 +339,53 @@ class CameraXPreview(
} }
val mediaOutput = mediaOutputHelper.getImageMediaOutput() val mediaOutput = mediaOutputHelper.getImageMediaOutput()
val outputOptionsBuilder = when (mediaOutput) {
is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues) if (mediaOutput is MediaOutput.BitmapOutput) {
is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream) imageCapture.takePicture(mainExecutor, object : ImageCapture.OnImageCapturedCallback() {
else -> throw IllegalArgumentException("Unexpected option for image") 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() playShutterSoundIfEnabled()
} }
private fun handleImageCaptureError(exception: ImageCaptureException) {
Log.e(TAG, "Error", exception)
listener.toggleBottomButtons(false)
cameraErrorHandler.handleImageCaptureError(exception.imageCaptureError)
}
override fun initPhotoMode() { override fun initPhotoMode() {
isPhotoCapture = true isPhotoCapture = true
startCamera() startCamera()
@ -441,7 +445,7 @@ class CameraXPreview(
Log.e(TAG, "recording failed:", recordEvent.cause) Log.e(TAG, "recording failed:", recordEvent.cause)
cameraErrorHandler.handleVideoRecordingError(recordEvent.error) cameraErrorHandler.handleVideoRecordingError(recordEvent.error)
} else { } else {
listener.onMediaCaptured(mediaOutput.uri ?: recordEvent.outputResults.outputUri) listener.onMediaSaved(mediaOutput.uri ?: recordEvent.outputResults.outputUri)
} }
} }
} }

View File

@ -1,5 +1,6 @@
package com.simplemobiletools.camera.implementations package com.simplemobiletools.camera.implementations
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
interface CameraXPreviewListener { interface CameraXPreviewListener {
@ -8,7 +9,8 @@ interface CameraXPreviewListener {
fun setFlashAvailable(available: Boolean) fun setFlashAvailable(available: Boolean)
fun onChangeCamera(frontCamera: Boolean) fun onChangeCamera(frontCamera: Boolean)
fun toggleBottomButtons(hide:Boolean) fun toggleBottomButtons(hide:Boolean)
fun onMediaCaptured(uri: Uri) fun onMediaSaved(uri: Uri)
fun onImageCaptured(bitmap: Bitmap)
fun onChangeFlashMode(flashMode: Int) fun onChangeFlashMode(flashMode: Int)
fun onVideoRecordingStarted() fun onVideoRecordingStarted()
fun onVideoRecordingStopped() fun onVideoRecordingStopped()

View File

@ -22,4 +22,6 @@ sealed class MediaOutput(
val fileDescriptor: ParcelFileDescriptor, val fileDescriptor: ParcelFileDescriptor,
override val uri: Uri, override val uri: Uri,
) : MediaOutput(uri) ) : MediaOutput(uri)
object BitmapOutput : MediaOutput(null)
} }

View File

@ -9,10 +9,9 @@ enum class VideoQuality(val width: Int, val height: Int) {
HD(1280, 720), HD(1280, 720),
SD(720, 480); SD(720, 480);
val pixels: Int = width * height 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() val ratio = width / height.toFloat()
@ -54,6 +53,6 @@ enum class VideoQuality(val width: Int, val height: Int) {
} }
companion object { companion object {
private const val ONE_MEGA_PIXELS = 1000000 private const val ONE_MEGA_PIXEL = 1000000
} }
} }