@@ -32,9 +32,10 @@ import com.dan.simplerawcamera.databinding.ActivityMainBinding
3232import kotlinx.coroutines.Dispatchers
3333import kotlinx.coroutines.GlobalScope
3434import kotlinx.coroutines.launch
35- import java.io.OutputStream
35+ import java.io.ByteArrayOutputStream
3636import java.text.SimpleDateFormat
3737import java.util.*
38+ import kotlin.concurrent.schedule
3839import kotlin.concurrent.timer
3940import kotlin.math.abs
4041import kotlin.math.log2
@@ -74,10 +75,22 @@ class CameraActivity : AppCompatActivity() {
7475 const val FOCUS_STATE_SEARCHING = 2
7576 const val FOCUS_STATE_LOCKED = 3
7677
78+ const val MEMORY_RETRY_TIMEOUT = 250L // ms
79+
7780 const val SELECT_CAMERA_ASYNC_DELAY = 250L // ms
7881
7982 private val FILE_NAME_DATE_FORMAT = SimpleDateFormat (" yyyyMMdd_HHmmss_SSS" , Locale .US )
8083
84+ fun getMemInfo (): Pair <Long , Long > {
85+ val info = Runtime .getRuntime()
86+ val usedSize = (info.totalMemory() - info.freeMemory()) / (1024L * 1024L )
87+ val maxSize = info.maxMemory() / (1024L * 1024L )
88+ val freeSize = maxSize - usedSize
89+ return Pair (freeSize, maxSize)
90+ }
91+
92+ fun getFreeMemInfo (): Long = getMemInfo().first
93+
8194 /* * Get photo name */
8295 fun getPhotoBaseFileName (timestamp : Long ): String = FILE_NAME_DATE_FORMAT .format(Date (timestamp))
8396
@@ -163,6 +176,9 @@ class CameraActivity : AppCompatActivity() {
163176
164177 private var mLocation: Location ? = null
165178
179+ private val mSaveAsyncMQ = mutableListOf<Triple <String , String , ByteArray >>()
180+ private var mSaveAsyncBusy = false
181+
166182 private var mOrientationEventListener: OrientationEventListener ? = null
167183 private var mScreenOrientation: Int = 0
168184 private var mPhotoExifOrientation: Int = 0
@@ -858,7 +874,7 @@ class CameraActivity : AppCompatActivity() {
858874 }
859875
860876 mBinding.switch4X.isChecked = false
861- mBinding.switch4X.setOnCheckedChangeListener { _, isChecked ->
877+ mBinding.switch4X.setOnCheckedChangeListener { _, _ ->
862878 giveHapticFeedback(mBinding.switchSequences)
863879 setupCapturePreviewRequest()
864880 }
@@ -1109,27 +1125,61 @@ class CameraActivity : AppCompatActivity() {
11091125 }
11101126 }
11111127
1112- private fun createAndSave ( fileName : String , mimeType : String , saveCallback : (outputStream: OutputStream ) -> Unit ) {
1113- mSaveFolder?.let { saveFolder ->
1114- saveFolder.createFile(mimeType, fileName)?.let { newFile ->
1115- contentResolver.openOutputStream(newFile.uri)?.let { outputStream ->
1116- saveCallback.invoke( outputStream )
1117- outputStream.close()
1128+ private fun saveAsyncNextItem () {
1129+ mBinding.frameView.updateDebugMemInfo()
1130+
1131+ if (! mSaveAsyncBusy && mSaveAsyncMQ.isNotEmpty()) {
1132+ mSaveAsyncBusy = true
1133+ mBinding.frameView.showSavePhotosIcon(true )
1134+ val item = mSaveAsyncMQ.get(0 )
1135+ mSaveAsyncMQ.removeAt(0 )
1136+
1137+ GlobalScope .launch(Dispatchers .IO ) {
1138+ val fileName = item.first
1139+ val mimeType = item.second
1140+ val byteArray = item.third
1141+ var failed = true
1142+
1143+ try {
1144+ mSaveFolder?.let { saveFolder ->
1145+ saveFolder.createFile(mimeType, fileName)?.let { newFile ->
1146+ contentResolver.openOutputStream(newFile.uri)?.let { outputStream ->
1147+ outputStream.write(byteArray)
1148+ outputStream.close()
1149+ failed = false
1150+ }
1151+ }
1152+ }
1153+ } catch (e: Exception ) {
1154+ e.printStackTrace()
1155+ }
1156+
1157+ mSaveAsyncBusy = false
1158+ runOnUiThread {
1159+ if (failed) mBinding.frameView.showSaveError()
1160+ saveAsyncNextItem()
11181161 }
11191162 }
1163+ } else if (! mSaveAsyncBusy) {
1164+ mBinding.frameView.showSavePhotosIcon(false )
11201165 }
11211166 }
11221167
1168+ private fun saveAsync (fileName : String , mimeType : String , byteArray : ByteArray ) {
1169+ mSaveAsyncMQ.add(Triple (fileName, mimeType, byteArray))
1170+ saveAsyncNextItem()
1171+ }
1172+
11231173 private fun saveDng (image : Image , captureResult : TotalCaptureResult ) {
11241174 Log .i(" TAKE_PHOTO" , " DNG: Save starts" )
11251175 try {
1176+ val outputStream = ByteArrayOutputStream ()
11261177 val dngCreator = DngCreator (mCameraInfo.cameraCharacteristics, captureResult)
11271178 mLocation?.let { dngCreator.setLocation(it) }
11281179 dngCreator.setOrientation(mPhotoExifOrientation)
1129- createAndSave( " $mPhotoFileNameBase .dng" , " image/x-adobe-dng" ) { outputStream ->
1130- dngCreator.writeImage(outputStream, image)
1131- }
1132- } catch (e: Exception ) {
1180+ dngCreator.writeImage(outputStream, image)
1181+ saveAsync(" $mPhotoFileNameBase .dng" , " image/x-adobe-dng" , outputStream.toByteArray())
1182+ } catch (e: Exception ) {
11331183 e.printStackTrace()
11341184 }
11351185 Log .i(" TAKE_PHOTO" , " DNG: Save ends" )
@@ -1138,12 +1188,12 @@ class CameraActivity : AppCompatActivity() {
11381188 private fun saveJpeg (image : Image ) {
11391189 Log .i(" TAKE_PHOTO" , " JPEG: Save starts" )
11401190 try {
1191+ val outputStream = ByteArrayOutputStream ()
11411192 val buffer = image.planes[0 ].buffer
11421193 val bytes = ByteArray (buffer.remaining())
11431194 buffer.get(bytes)
1144- createAndSave( " $mPhotoFileNameBase .jpg" , " image/jpeg" ) { outputStream ->
1145- outputStream.write(bytes)
1146- }
1195+ outputStream.write(bytes)
1196+ saveAsync(" $mPhotoFileNameBase .jpg" , " image/jpeg" , outputStream.toByteArray())
11471197 } catch (e: Exception ) {
11481198 e.printStackTrace()
11491199 }
@@ -1169,6 +1219,8 @@ class CameraActivity : AppCompatActivity() {
11691219 /* * Start taking a photo */
11701220 private fun takePhoto (newFile : Boolean = false, start : Boolean = false) {
11711221 runOnUiThread {
1222+ mBinding.frameView.updateDebugMemInfo()
1223+
11721224 if (start) {
11731225 if (! mSequenceStarted) {
11741226 mPhotoCounter = 0
@@ -1215,24 +1267,41 @@ class CameraActivity : AppCompatActivity() {
12151267 val cameraCaptureSession = mCameraCaptureSession
12161268
12171269 if (takeNewPhoto && null != captureRequestPhoto && null != cameraCaptureSession) {
1218- Log .i(" TAKE_PHOTO" , " New photo" )
1270+ var minMem =
1271+ when (settings.takePhotoModes) {
1272+ Settings .PHOTO_TYPE_DNG -> mCameraInfo.estimatedDngSize * 2
1273+ Settings .PHOTO_TYPE_JPEG -> mCameraInfo.estimatedJpegSize
1274+ else -> mCameraInfo.estimatedDngSize * 2 + mCameraInfo.estimatedJpegSize
1275+ }
1276+ minMem = 1 + minMem / (1024 * 1024 ) // convert to MB
1277+
1278+ if (mSaveAsyncMQ.isNotEmpty() && minMem > getFreeMemInfo()) {
1279+ Log .i(" TAKE_PHOTO" , " Not enough memory" )
1280+ mPhotoTakeMask = PHOTO_TAKE_OUT_OF_MEMORY
1281+ Timer (" Out of memory" , false ).schedule(MEMORY_RETRY_TIMEOUT ) {
1282+ mPhotoTakeMask = 0
1283+ takePhoto(true )
1284+ }
1285+ } else {
1286+ Log .i(" TAKE_PHOTO" , " New photo" )
12191287
1220- mPhotoInProgress = true
1288+ mPhotoInProgress = true
12211289
1222- mPhotoTakeMask = when (settings.takePhotoModes) {
1223- Settings .PHOTO_TYPE_DNG -> PHOTO_TAKE_DNG or PHOTO_TAKE_COMPLETED
1224- Settings .PHOTO_TYPE_JPEG -> PHOTO_TAKE_JPEG or PHOTO_TAKE_COMPLETED
1225- else -> PHOTO_TAKE_JPEG or PHOTO_TAKE_DNG or PHOTO_TAKE_COMPLETED
1226- }
1290+ mPhotoTakeMask = when (settings.takePhotoModes) {
1291+ Settings .PHOTO_TYPE_DNG -> PHOTO_TAKE_DNG or PHOTO_TAKE_COMPLETED
1292+ Settings .PHOTO_TYPE_JPEG -> PHOTO_TAKE_JPEG or PHOTO_TAKE_COMPLETED
1293+ else -> PHOTO_TAKE_JPEG or PHOTO_TAKE_DNG or PHOTO_TAKE_COMPLETED
1294+ }
12271295
1228- mPhotoTimestamp = System .currentTimeMillis()
1229- mPhotoFileNameBase = getPhotoBaseFileName(mPhotoTimestamp)
1296+ mPhotoTimestamp = System .currentTimeMillis()
1297+ mPhotoFileNameBase = getPhotoBaseFileName(mPhotoTimestamp)
12301298
1231- cameraCaptureSession.capture(
1232- captureRequestPhoto,
1233- mCameraCaptureSessionPhotoCaptureCallback,
1234- getWorkerHandler()
1235- )
1299+ cameraCaptureSession.capture(
1300+ captureRequestPhoto,
1301+ mCameraCaptureSessionPhotoCaptureCallback,
1302+ getWorkerHandler()
1303+ )
1304+ }
12361305 } else {
12371306 mPhotoInProgress = false
12381307 setupCapturePreviewRequest()
0 commit comments