|
|
|
|
@ -2,19 +2,19 @@ package net.pokeranalytics.android.ui.modules.handhistory.replayer |
|
|
|
|
|
|
|
|
|
import android.app.PendingIntent |
|
|
|
|
import android.app.Service |
|
|
|
|
import android.content.ContentValues |
|
|
|
|
import android.content.Intent |
|
|
|
|
import android.net.Uri |
|
|
|
|
import android.os.Binder |
|
|
|
|
import android.os.Build |
|
|
|
|
import android.os.Environment |
|
|
|
|
import android.os.IBinder |
|
|
|
|
import androidx.core.content.FileProvider |
|
|
|
|
import android.provider.MediaStore |
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL |
|
|
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS |
|
|
|
|
import com.arthenica.mobileffmpeg.FFmpeg |
|
|
|
|
import io.realm.Realm |
|
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
|
import kotlinx.coroutines.GlobalScope |
|
|
|
|
import kotlinx.coroutines.async |
|
|
|
|
import kotlinx.coroutines.launch |
|
|
|
|
import kotlinx.coroutines.* |
|
|
|
|
import net.pokeranalytics.android.R |
|
|
|
|
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
|
|
|
|
import net.pokeranalytics.android.model.realm.handhistory.HandHistory |
|
|
|
|
@ -25,10 +25,13 @@ import net.pokeranalytics.android.util.extensions.findById |
|
|
|
|
import net.pokeranalytics.android.util.video.AnimatedGIFWriter |
|
|
|
|
import timber.log.Timber |
|
|
|
|
import java.io.File |
|
|
|
|
import java.io.FileOutputStream |
|
|
|
|
import java.util.* |
|
|
|
|
import kotlin.coroutines.CoroutineContext |
|
|
|
|
|
|
|
|
|
enum class FileType(var value: String) { |
|
|
|
|
IMAGE("image/gif"), |
|
|
|
|
VIDEO("video/*") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class ReplayExportService : Service() { |
|
|
|
|
|
|
|
|
|
@ -39,7 +42,7 @@ class ReplayExportService : Service() { |
|
|
|
|
private val coroutineContext: CoroutineContext |
|
|
|
|
get() = Dispatchers.Main |
|
|
|
|
|
|
|
|
|
override fun onBind(intent: Intent?): IBinder? { |
|
|
|
|
override fun onBind(intent: Intent?): IBinder { |
|
|
|
|
return binder |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -81,40 +84,57 @@ class ReplayExportService : Service() { |
|
|
|
|
animator.configure(width.toFloat(), height.toFloat(), context) |
|
|
|
|
|
|
|
|
|
val formattedDate = Date().dateTimeFileFormatted |
|
|
|
|
val path = File( |
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
|
|
|
|
"gif_${formattedDate}.gif" |
|
|
|
|
).toString() |
|
|
|
|
val fileName = "hand_${formattedDate}.gif" |
|
|
|
|
|
|
|
|
|
val writer = AnimatedGIFWriter(false) |
|
|
|
|
val os = FileOutputStream(path) |
|
|
|
|
writer.prepareForWrite(os, width, height) |
|
|
|
|
// Add a specific media item. |
|
|
|
|
val resolver = applicationContext.contentResolver |
|
|
|
|
|
|
|
|
|
val drawer = TableDrawer() |
|
|
|
|
drawer.configurePaints(context, animator) |
|
|
|
|
// 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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var animationCount = 0 |
|
|
|
|
animator.frames(context) { bitmap, count -> |
|
|
|
|
val gifDetails = ContentValues().apply { |
|
|
|
|
put(MediaStore.Images.Media.DISPLAY_NAME, fileName) |
|
|
|
|
put(MediaStore.Images.Media.MIME_TYPE, "image/gif") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
when { |
|
|
|
|
count > 10 -> { |
|
|
|
|
writer.writeFrame(os, bitmap, count * 8) |
|
|
|
|
animationCount = 0 |
|
|
|
|
} |
|
|
|
|
else -> { |
|
|
|
|
if (animationCount % 2 == 0) { |
|
|
|
|
writer.writeFrame(os, bitmap) |
|
|
|
|
val uri = resolver.insert(videoCollection, gifDetails) |
|
|
|
|
|
|
|
|
|
if (uri != null) { |
|
|
|
|
val os = resolver.openOutputStream(uri) |
|
|
|
|
|
|
|
|
|
val writer = AnimatedGIFWriter(false) |
|
|
|
|
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++ |
|
|
|
|
} |
|
|
|
|
animationCount++ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
writer.finishWrite(os) |
|
|
|
|
|
|
|
|
|
realm.close() |
|
|
|
|
notifyUser(uri, FileType.IMAGE) |
|
|
|
|
} |
|
|
|
|
writer.finishWrite(os) |
|
|
|
|
|
|
|
|
|
realm.close() |
|
|
|
|
notifyUser(path) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
c.await() |
|
|
|
|
} |
|
|
|
|
@ -148,30 +168,79 @@ class ReplayExportService : Service() { |
|
|
|
|
val tmpDir = animator.generateVideoContent(this@ReplayExportService) |
|
|
|
|
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE" |
|
|
|
|
|
|
|
|
|
// val directory = context.getExternalFilesDir(null) ?: throw PAIllegalStateException("File is invalid") |
|
|
|
|
// val output = "${directory.path}/video_${Date().dateTimeFileFormatted}.mp4" |
|
|
|
|
|
|
|
|
|
val formattedDate = Date().dateTimeFileFormatted |
|
|
|
|
val output = File( |
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), |
|
|
|
|
"video_${formattedDate}.mp4" |
|
|
|
|
).path |
|
|
|
|
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) |
|
|
|
|
// 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)) |
|
|
|
|
when (rc) { |
|
|
|
|
RETURN_CODE_SUCCESS -> { |
|
|
|
|
Timber.d("FFMPEG command execution completed successfully") |
|
|
|
|
} |
|
|
|
|
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)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
tmpDir.delete() |
|
|
|
|
|
|
|
|
|
notifyUser(output) |
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
resolver.insert(videoCollection, fileDetails)?.let { uri -> |
|
|
|
|
|
|
|
|
|
Timber.d("copy file at uri = $uri") |
|
|
|
|
|
|
|
|
|
val os = resolver.openOutputStream(uri) |
|
|
|
|
os?.write(file.readBytes()) |
|
|
|
|
os?.close() |
|
|
|
|
|
|
|
|
|
file.delete() |
|
|
|
|
|
|
|
|
|
notifyUser(uri, FileType.VIDEO) |
|
|
|
|
|
|
|
|
|
} ?: run { |
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
@ -226,26 +295,26 @@ class ReplayExportService : Service() { |
|
|
|
|
// |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
private fun notifyUser(path: String) { |
|
|
|
|
|
|
|
|
|
Timber.d("Show local notification") |
|
|
|
|
private fun notifyUser(uri: Uri, type: FileType) { |
|
|
|
|
|
|
|
|
|
val title = getString(R.string.video_available) |
|
|
|
|
val body = getString(R.string.video_retrieval_message) + ": " + path |
|
|
|
|
val body = getString(R.string.video_retrieval_message) + ": " + uri.path |
|
|
|
|
|
|
|
|
|
val uri = FileProvider.getUriForFile( |
|
|
|
|
this, |
|
|
|
|
this.applicationContext.packageName.toString() + ".fileprovider", |
|
|
|
|
File(path) |
|
|
|
|
) |
|
|
|
|
Timber.d("Show local notification, path of file: ${uri.path}") |
|
|
|
|
|
|
|
|
|
val type = when { |
|
|
|
|
path.contains("gif") -> "image/gif" |
|
|
|
|
else -> "video/*" |
|
|
|
|
} |
|
|
|
|
// val uri = FileProvider.getUriForFile( |
|
|
|
|
// this, |
|
|
|
|
// this.applicationContext.packageName.toString() + ".fileprovider", |
|
|
|
|
// File(path) |
|
|
|
|
// ) |
|
|
|
|
// |
|
|
|
|
// val type = when { |
|
|
|
|
// path.contains("gif") -> "image/gif" |
|
|
|
|
// else -> "video/*" |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
val intent = Intent(Intent.ACTION_VIEW) |
|
|
|
|
intent.setDataAndType(uri, type) |
|
|
|
|
intent.setDataAndType(uri, type.value) |
|
|
|
|
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)) |
|
|
|
|
|