Removes deprecated file management API

blinds
Laurent 5 years ago
parent 21d700db1e
commit f874f35ad9
  1. 18
      app/src/main/AndroidManifest.xml
  2. 191
      app/src/main/java/net/pokeranalytics/android/ui/modules/handhistory/replayer/ReplayExportService.kt
  3. 2
      app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt

@ -6,26 +6,10 @@
<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:requestLegacyExternalStorage="true"
has been added due to a crash:
Fatal Exception: java.io.FileNotFoundException
/storage/emulated/0/Movies/gif_20_09_18_09_59_41.gif: open failed: EACCES (Permission denied)
happening here:
net.pokeranalytics.android.ui.modules.handhistory.replayer.ReplayExportService$startGIFExport$1$c$1.invokeSuspend (ReplayExportService.java:88)
The temporary fix comes from here:
https://medium.com/@sriramaripirala/android-10-open-failed-eacces-permission-denied-da8b630a89df
-->
<application <application
android:name=".PokerAnalyticsApplication" android:name=".PokerAnalyticsApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

@ -2,19 +2,19 @@ package net.pokeranalytics.android.ui.modules.handhistory.replayer
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.IBinder 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_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
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.handhistory.HandHistory 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 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
enum class FileType(var value: String) {
IMAGE("image/gif"),
VIDEO("video/*")
}
class ReplayExportService : Service() { class ReplayExportService : Service() {
@ -39,7 +42,7 @@ class ReplayExportService : Service() {
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
get() = Dispatchers.Main get() = Dispatchers.Main
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder {
return binder return binder
} }
@ -81,40 +84,57 @@ class ReplayExportService : Service() {
animator.configure(width.toFloat(), height.toFloat(), context) animator.configure(width.toFloat(), height.toFloat(), context)
val formattedDate = Date().dateTimeFileFormatted val formattedDate = Date().dateTimeFileFormatted
val path = File( val fileName = "hand_${formattedDate}.gif"
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
"gif_${formattedDate}.gif"
).toString()
val writer = AnimatedGIFWriter(false) // Add a specific media item.
val os = FileOutputStream(path) val resolver = applicationContext.contentResolver
writer.prepareForWrite(os, width, height)
val drawer = TableDrawer() // Find all video files on the primary external storage device.
drawer.configurePaints(context, animator) 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 val gifDetails = ContentValues().apply {
animator.frames(context) { bitmap, count -> put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, "image/gif")
}
when { val uri = resolver.insert(videoCollection, gifDetails)
count > 10 -> {
writer.writeFrame(os, bitmap, count * 8) if (uri != null) {
animationCount = 0 val os = resolver.openOutputStream(uri)
}
else -> { val writer = AnimatedGIFWriter(false)
if (animationCount % 2 == 0) { writer.prepareForWrite(os, width, height)
writer.writeFrame(os, bitmap)
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() c.await()
} }
@ -148,30 +168,79 @@ class ReplayExportService : Service() {
val tmpDir = animator.generateVideoContent(this@ReplayExportService) val tmpDir = animator.generateVideoContent(this@ReplayExportService)
val dpath = "${tmpDir.path}/$FFMPEG_DESCRIPTOR_FILE" 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 formattedDate = Date().dateTimeFileFormatted
val output = File( val fileName = "hand_${formattedDate}.mp4"
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), // val output = File(
"video_${formattedDate}.mp4" // Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
).path // 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...") 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 ->
if (rc == RETURN_CODE_SUCCESS) { when (rc) {
Timber.d("FFMPEG command execution completed successfully") RETURN_CODE_SUCCESS -> {
} else if (rc == RETURN_CODE_CANCEL) { Timber.d("FFMPEG command execution completed successfully")
Timber.d("Command execution cancelled by user.") }
} else { RETURN_CODE_CANCEL -> {
Timber.d(String.format("Command execution failed with rc=%d and the output below.", rc)) 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() 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) { private fun notifyUser(uri: Uri, type: FileType) {
Timber.d("Show local notification")
val title = getString(R.string.video_available) 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( Timber.d("Show local notification, path of file: ${uri.path}")
this,
this.applicationContext.packageName.toString() + ".fileprovider",
File(path)
)
val type = when { // val uri = FileProvider.getUriForFile(
path.contains("gif") -> "image/gif" // this,
else -> "video/*" // this.applicationContext.packageName.toString() + ".fileprovider",
} // File(path)
// )
//
// val type = when {
// path.contains("gif") -> "image/gif"
// else -> "video/*"
// }
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, type) intent.setDataAndType(uri, type.value)
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))

@ -120,7 +120,7 @@ fun Date.getDayMonthYear(): String {
// Returns a file friendly date time string // Returns a file friendly date time string
val Date.dateTimeFileFormatted: String val Date.dateTimeFileFormatted: String
get() { get() {
return SimpleDateFormat("yy_MM_dd_hh_mm_ss", Locale.getDefault()).format(this) return SimpleDateFormat("yy_MM_dd-hh_mm_ss", Locale.getDefault()).format(this)
} }
// Return the netDuration between two dates // Return the netDuration between two dates

Loading…
Cancel
Save