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:
parent
74e2656831
commit
d5e1d61d02
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -22,4 +22,6 @@ sealed class MediaOutput(
|
|||
val fileDescriptor: ParcelFileDescriptor,
|
||||
override val uri: Uri,
|
||||
) : MediaOutput(uri)
|
||||
|
||||
object BitmapOutput : MediaOutput(null)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue