mirror of
				https://github.com/SimpleMobileTools/Simple-Camera.git
				synced 2025-06-27 09:02:59 +02:00 
			
		
		
		
	Merge pull request #327 from KryptKode/feat/camera-x
fix passing bitmap thumbnail after IMAGE_CAPTURE intent
This commit is contained in:
		| @@ -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 | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user