Adds pre Q and post Q hand history exports

blinds
Laurent 5 years ago
parent f874f35ad9
commit 55b15780d1
  1. 3
      app/src/main/AndroidManifest.xml
  2. 221
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt

@ -6,7 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<application <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"

@ -10,6 +10,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.FileProvider
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
import com.arthenica.mobileffmpeg.FFmpeg import com.arthenica.mobileffmpeg.FFmpeg
@ -25,6 +26,7 @@ import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.video.AnimatedGIFWriter import net.pokeranalytics.android.util.video.AnimatedGIFWriter
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -47,19 +49,25 @@ class ReplayExportService : Service() {
} }
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): ReplayExportService = this@ReplayExportService fun getService(): ReplayExportService = this@ReplayExportService
} }
fun videoExport(handHistoryId: String) { fun videoExport(handHistoryId: String) {
this@ReplayExportService.handHistoryId = handHistoryId this@ReplayExportService.handHistoryId = handHistoryId
startFFMPEGVideoExport() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startFFMPEGVideoExport()
} else {
startFFMPEGVideoExportPreQ()
}
} }
fun gifExport(handHistoryId: String) { fun gifExport(handHistoryId: String) {
this@ReplayExportService.handHistoryId = handHistoryId this@ReplayExportService.handHistoryId = handHistoryId
startGIFExport() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startGIFExport()
} else {
startGIFExportPreQ()
}
} }
private fun startGIFExport() { private fun startGIFExport() {
@ -89,22 +97,17 @@ class ReplayExportService : Service() {
// Add a specific media item. // Add a specific media item.
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
// Find all video files on the primary external storage device. // Q version tested before calling the function
val videoCollection = val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val gifDetails = ContentValues().apply { val gifDetails = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName) 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) { if (uri != null) {
val os = resolver.openOutputStream(uri) val os = resolver.openOutputStream(uri)
val writer = AnimatedGIFWriter(false) val writer = AnimatedGIFWriter(false)
@ -128,12 +131,13 @@ class ReplayExportService : Service() {
animationCount++ animationCount++
} }
} }
} }
writer.finishWrite(os) writer.finishWrite(os)
realm.close() realm.close()
notifyUser(uri, FileType.IMAGE) notifyUser(uri, FileType.IMAGE)
} else {
Timber.w("Resolver insert ended without uri...")
} }
} }
c.await() c.await()
@ -159,7 +163,6 @@ class ReplayExportService : Service() {
val height = square val height = square
animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService) animator.configure(width.toFloat(), height.toFloat(), this@ReplayExportService)
// animator.setDimension()
val drawer = TableDrawer() val drawer = TableDrawer()
drawer.configurePaints(context, animator) drawer.configurePaints(context, animator)
@ -170,16 +173,10 @@ class ReplayExportService : Service() {
val formattedDate = Date().dateTimeFileFormatted val formattedDate = Date().dateTimeFileFormatted
val fileName = "hand_${formattedDate}.mp4" 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 outputDirectory = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) ?: throw PAIllegalStateException("File is invalid")
val output = "${outputDirectory.path}/$fileName" val output = "${outputDirectory.path}/$fileName"
// Environment.getExternalStorageState(tmpDir)
Timber.d("Assembling images for video...") 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 -> 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() tmpDir.delete()
val file = File(output) val file = File(output)
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
val videoCollection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Q version tested before calling the function
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
val fileDetails = ContentValues().apply { val fileDetails = ContentValues().apply {
Timber.d("set file details = $fileName") Timber.d("set file details = $fileName")
put(MediaStore.Video.Media.DISPLAY_NAME, 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 -> resolver.insert(videoCollection, fileDetails)?.let { uri ->
Timber.d("copy file at uri = $uri") Timber.d("copy file at uri = $uri")
@ -222,7 +217,7 @@ class ReplayExportService : Service() {
os?.write(file.readBytes()) os?.write(file.readBytes())
os?.close() os?.close()
file.delete() file.delete() // delete temp file
notifyUser(uri, FileType.VIDEO) notifyUser(uri, FileType.VIDEO)
@ -230,17 +225,6 @@ class ReplayExportService : Service() {
Timber.w("Resolver insert ended without uri...") 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<HandHistory>(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<HandHistory>(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) { private fun notifyUser(uri: Uri, type: FileType) {
val title = getString(R.string.video_available) val title = getString(R.string.video_available)
val body = getString(R.string.video_retrieval_message) + ": " + uri.path 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", private fun notifyUser(path: String) {
// File(path)
// ) val title = getString(R.string.video_available)
// val body = getString(R.string.video_retrieval_message) + ": " + path
// val type = when {
// path.contains("gif") -> "image/gif" Timber.d("Show local notification, path of file: ${path}")
// else -> "video/*"
// } 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) val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, type.value) intent.setDataAndType(uri, type)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val chooser = Intent.createChooser(intent, getString(R.string.open_file_with)) val chooser = Intent.createChooser(intent, getString(R.string.open_file_with))

Loading…
Cancel
Save