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.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()
}
}

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.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)

View File

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

View File

@ -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()

View File

@ -22,4 +22,6 @@ sealed class MediaOutput(
val fileDescriptor: ParcelFileDescriptor,
override val uri: 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),
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
}
}