From 55b15780d18c2c8be42381c7b0b41a7a0d660fe3 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 20 Apr 2021 15:26:51 +0200 Subject: [PATCH] Adds pre Q and post Q hand history exports --- app/src/main/AndroidManifest.xml | 3 +- .../replayer/ReplayExportService.kt | 221 +++++++++++++----- 2 files changed, 171 insertions(+), 53 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e269fd9e..9ef0dffc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,8 @@ - + + = Build.VERSION_CODES.Q) { + startFFMPEGVideoExport() + } else { + startFFMPEGVideoExportPreQ() + } } fun gifExport(handHistoryId: String) { this@ReplayExportService.handHistoryId = handHistoryId - startGIFExport() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startGIFExport() + } else { + startGIFExportPreQ() + } } private fun startGIFExport() { @@ -89,22 +97,17 @@ class ReplayExportService : Service() { // Add a specific media item. val resolver = applicationContext.contentResolver - // Find all video files on the primary external storage device. - val videoCollection = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - } else { - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } + // Q version tested before calling the function + val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val gifDetails = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, fileName) - put(MediaStore.Images.Media.MIME_TYPE, "image/gif") } - val uri = resolver.insert(videoCollection, gifDetails) + val uri = resolver.insert(imageCollection, gifDetails) if (uri != null) { + val os = resolver.openOutputStream(uri) val writer = AnimatedGIFWriter(false) @@ -128,12 +131,13 @@ class ReplayExportService : Service() { animationCount++ } } - } writer.finishWrite(os) realm.close() notifyUser(uri, FileType.IMAGE) + } else { + Timber.w("Resolver insert ended without uri...") } } c.await() @@ -159,7 +163,6 @@ class ReplayExportService : Service() { val height = square animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) -// animator.setDimension() val drawer = TableDrawer() drawer.configurePaints(context, animator) @@ -170,16 +173,10 @@ class ReplayExportService : Service() { val formattedDate = Date().dateTimeFileFormatted val fileName = "hand_${formattedDate}.mp4" -// val output = File( -// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), -// fileName -// ).path val outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid") val output = "${outputDirectory.path}/$fileName" -// Environment.getExternalStorageState(tmpDir) - Timber.d("Assembling images for video...") FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -pix_fmt yuv420p $output") { id, rc -> @@ -196,24 +193,22 @@ class ReplayExportService : Service() { } } + File(dpath).delete() tmpDir.delete() val file = File(output) val resolver = applicationContext.contentResolver - val videoCollection = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - } else { - MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } + + // Q version tested before calling the function + val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val fileDetails = ContentValues().apply { Timber.d("set file details = $fileName") put(MediaStore.Video.Media.DISPLAY_NAME, fileName) -// put(MediaStore.Video.Media.CONTENT_TYPE, "video/mp4") } + // copy video to nice path resolver.insert(videoCollection, fileDetails)?.let { uri -> Timber.d("copy file at uri = $uri") @@ -222,7 +217,7 @@ class ReplayExportService : Service() { os?.write(file.readBytes()) os?.close() - file.delete() + file.delete() // delete temp file notifyUser(uri, FileType.VIDEO) @@ -230,17 +225,6 @@ class ReplayExportService : Service() { Timber.w("Resolver insert ended without uri...") } - -// val uri = FileProvider.getUriForFile( -// applicationContext, -// applicationContext.packageName.toString() + ".fileprovider", -// File(output) -// ) - -// Timber.d("File exported at $output") - - -// notifyUser(output) } } @@ -295,26 +279,159 @@ class ReplayExportService : Service() { // // } + private fun startGIFExportPreQ() { + + GlobalScope.launch(coroutineContext) { + val c = GlobalScope.async { + + val realm = Realm.getDefaultInstance() + realm.refresh() + + val handHistory = realm.findById(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") + + val context = this@ReplayExportService + + val animator = ReplayerAnimator(handHistory, true) + + val square = 1024 + + val width = square + val height = square + + animator.configure(width.toFloat(), height.toFloat(), context) + + val formattedDate = Date().dateTimeFileFormatted + val path = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + "hand_${formattedDate}.gif" + ).toString() + + val writer = AnimatedGIFWriter(false) + val os = FileOutputStream(path) + writer.prepareForWrite(os, width, height) + + val drawer = TableDrawer() + drawer.configurePaints(context, animator) + + var animationCount = 0 + animator.frames(context) { bitmap, count -> + + when { + count > 10 -> { + writer.writeFrame(os, bitmap, count * 8) + animationCount = 0 + } + else -> { + if (animationCount % 2 == 0) { + writer.writeFrame(os, bitmap) + } + animationCount++ + } + } + + } + writer.finishWrite(os) + + realm.close() + notifyUser(path) + + } + c.await() + } + + } + + private fun startFFMPEGVideoExportPreQ() { + + GlobalScope.launch(coroutineContext) { + val async = GlobalScope.async { + + val realm = Realm.getDefaultInstance() + val handHistory = realm.findById(handHistoryId) ?: throw PAIllegalStateException("HandHistory not found, id: $handHistoryId") + + val context = this@ReplayExportService + + val animator = ReplayerAnimator(handHistory, true) + + val square = 1024 + + val width = square + val height = square + + animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) + val drawer = TableDrawer() + drawer.configurePaints(context, animator) + + // generates all images and file descriptor + Timber.d("Generating images for video...") + val tmpDir = animator.generateVideoContent(this@ReplayExportService) + val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE" + + val formattedDate = Date().dateTimeFileFormatted + val output = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + "hand_${formattedDate}.mp4" + ).path + + Environment.getExternalStorageState(tmpDir) + + Timber.d("Assembling images for video...") + FFmpeg.executeAsync("-f concat -safe 0 -i $dpath -vb 20M -vsync vfr -s ${width}x${height} -pix_fmt yuv420p $output") { id, rc -> + + if (rc == RETURN_CODE_SUCCESS) { + Timber.d("FFMPEG command execution completed successfully") + } else if (rc == RETURN_CODE_CANCEL) { + Timber.d("Command execution cancelled by user.") + } else { + Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc)) + } + // Delete descriptor and image files + tmpDir.delete() + File(dpath).delete() + + notifyUser(output) + } + + } + async.await() + } + + } + private fun notifyUser(uri: Uri, type: FileType) { val title = getString(R.string.video_available) val body = getString(R.string.video_retrieval_message) + ": " + uri.path - Timber.d("Show local notification, path of file: ${uri.path}") + this.showNotification(title, body, uri, type.value) -// val uri = FileProvider.getUriForFile( -// this, -// this.applicationContext.packageName.toString() + ".fileprovider", -// File(path) -// ) -// -// val type = when { -// path.contains("gif") -> "image/gif" -// else -> "video/*" -// } + } + + private fun notifyUser(path: String) { + + val title = getString(R.string.video_available) + val body = getString(R.string.video_retrieval_message) + ": " + path + + Timber.d("Show local notification, path of file: ${path}") + + val uri = FileProvider.getUriForFile( + this, + this.applicationContext.packageName.toString() + ".fileprovider", + File(path) + ) + + val type = when { + path.contains("gif") -> "image/gif" + else -> "video/*" + } + + this.showNotification(title, body, uri, type) + } + + private fun showNotification(title: String, body: String, uri: Uri, type: String) { val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(uri, type.value) + intent.setDataAndType(uri, type) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val chooser = Intent.createChooser(intent, getString(R.string.open_file_with))