302 lines
11 KiB
Kotlin
302 lines
11 KiB
Kotlin
package dev.lowrespalmtree.zmingz
|
|
|
|
import android.Manifest
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.graphics.BitmapFactory
|
|
import android.net.Uri
|
|
import android.os.AsyncTask
|
|
import android.os.Bundle
|
|
import android.os.Environment
|
|
import android.os.Parcelable
|
|
import android.provider.MediaStore
|
|
import android.util.Log
|
|
import android.view.View
|
|
import android.widget.PopupMenu
|
|
import android.widget.Toast
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.content.FileProvider
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
|
import com.arthenica.mobileffmpeg.FFmpeg
|
|
import kotlinx.android.synthetic.main.activity_main.*
|
|
import java.io.File
|
|
import java.io.FileInputStream
|
|
import java.io.FileOutputStream
|
|
import java.lang.ref.WeakReference
|
|
|
|
class MainActivity: AppCompatActivity() {
|
|
|
|
/** Path for first image/video. */
|
|
private var path1 = ""
|
|
/** Path for second image/video. */
|
|
private var path2 = ""
|
|
/** Temporary path for the media to save, to use after perms being granted. */
|
|
private var savingPath = ""
|
|
/** Temporary type for the media to save, to use after perms being granted. */
|
|
private var savingType = MediaType.IMAGE
|
|
|
|
enum class MediaType { IMAGE, VIDEO }
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setContentView(R.layout.activity_main)
|
|
|
|
if (intent.action == Intent.ACTION_SEND) {
|
|
val type = if (intent.type?.startsWith("image") == true) {
|
|
MediaType.IMAGE
|
|
} else if (intent.type?.startsWith("video") == true) {
|
|
MediaType.VIDEO
|
|
} else {
|
|
null
|
|
}
|
|
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
|
|
if (type != null && uri != null)
|
|
processMedia(uri, type)
|
|
}
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
when (requestCode) {
|
|
REQ_PICK_IMG, REQ_PICK_VID -> handlePickResult(requestCode, resultCode, data)
|
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
|
}
|
|
}
|
|
|
|
override fun onRequestPermissionsResult(
|
|
requestCode: Int,
|
|
permissions: Array<out String>,
|
|
grantResults: IntArray
|
|
) {
|
|
if (requestCode == REQ_WRITE_PERM) {
|
|
if (permissions[0] == Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
saveWithPerm(savingPath, savingType)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
}
|
|
|
|
fun openFile(v: View) {
|
|
val req: Int
|
|
val pickIntent = when (v.id) {
|
|
buttonImage.id -> {
|
|
req = REQ_PICK_IMG
|
|
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
|
}
|
|
buttonVideo.id -> {
|
|
req = REQ_PICK_VID
|
|
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
|
|
}
|
|
else -> return
|
|
}
|
|
startActivityForResult(pickIntent, req)
|
|
}
|
|
|
|
fun openViewMenu(v: View) {
|
|
val path = when (v.id) {
|
|
imageView1.id, videoView1.id -> path1
|
|
imageView2.id, videoView2.id -> path2
|
|
else -> return
|
|
}
|
|
val type = mediaTypeFromViewId(v.id)
|
|
?: return
|
|
|
|
val menu = PopupMenu(this, v)
|
|
menu.menu.add(R.string.save).setOnMenuItemClickListener {
|
|
save(path, type)
|
|
true
|
|
}
|
|
menu.menu.add(R.string.share).setOnMenuItemClickListener {
|
|
share(path, type)
|
|
true
|
|
}
|
|
menu.show()
|
|
}
|
|
|
|
private fun mediaTypeFromViewId(id: Int): MediaType? =
|
|
when (id) {
|
|
imageView1.id, imageView2.id -> MediaType.IMAGE
|
|
videoView1.id, videoView2.id -> MediaType.VIDEO
|
|
else -> null
|
|
}
|
|
|
|
@Suppress("UNUSED_PARAMETER")
|
|
private fun handlePickResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
val uri = data?.data
|
|
?: return Unit.also { Log.e(TAG, "No intent or data") }
|
|
val type = when (requestCode) {
|
|
REQ_PICK_IMG -> MediaType.IMAGE
|
|
REQ_PICK_VID -> MediaType.VIDEO
|
|
else -> return
|
|
}
|
|
processMedia(uri, type)
|
|
}
|
|
|
|
private fun processMedia(uri: Uri, type: MediaType) {
|
|
val uriPath = uri.path
|
|
?: return Unit.also { Log.e(TAG, "No path in URI") }
|
|
val extension = getFileExtension(uriPath)
|
|
// Copy picked file to cache dir for FFmpeg.
|
|
val inputFile = File.createTempFile("input", ".$extension", cacheDir)
|
|
contentResolver.openInputStream(uri).use { inputStream ->
|
|
if (inputStream == null)
|
|
return Unit .also { Log.e(TAG, "Could not open input file") }
|
|
FileOutputStream(inputFile).use { fos ->
|
|
fos.buffered().write(inputStream.buffered().readBytes())
|
|
}
|
|
}
|
|
|
|
val mediaCacheDir = File(cacheDir, "media")
|
|
if (!mediaCacheDir.exists() && !mediaCacheDir.mkdir()) {
|
|
Log.e(TAG, "Could not create cache media dir")
|
|
Toast.makeText(this, R.string.write_error, Toast.LENGTH_SHORT).show()
|
|
return
|
|
}
|
|
|
|
imageLayout.visibility = View.GONE
|
|
videoLayout.visibility = View.GONE
|
|
Toast.makeText(this, R.string.please_wait, Toast.LENGTH_SHORT).show()
|
|
|
|
val outputFile1 = File.createTempFile("output1", ".$extension", mediaCacheDir)
|
|
MirrorTask(WeakReference(this), type, 1)
|
|
.execute(inputFile.canonicalPath, outputFile1.canonicalPath, VF1)
|
|
val outputFile2 = File.createTempFile("output2", ".$extension", mediaCacheDir)
|
|
MirrorTask(WeakReference(this), type, 2)
|
|
.execute(inputFile.canonicalPath, outputFile2.canonicalPath, VF2)
|
|
}
|
|
|
|
class MirrorTask(
|
|
private val activity: WeakReference<MainActivity>,
|
|
private val type: MediaType,
|
|
private val index: Int
|
|
): AsyncTask<String, Void, Boolean>() {
|
|
private lateinit var outputPath: String
|
|
|
|
override fun doInBackground(vararg params: String?): Boolean {
|
|
val inputPath = params[0]!!
|
|
outputPath = params[1]!!
|
|
val vf = params[2]!!
|
|
val command = "-i $inputPath -vf \"$vf\" -y $outputPath"
|
|
return when (val rc = FFmpeg.execute(command)) {
|
|
RETURN_CODE_SUCCESS -> true. also { Log.d(TAG, "FFmpeg success") }
|
|
RETURN_CODE_CANCEL -> false .also { Log.d(TAG, "FFmpeg canceled") }
|
|
else -> false .also { Log.d(TAG, "FFmpeg failed with rc $rc") }
|
|
}
|
|
}
|
|
|
|
override fun onPostExecute(result: Boolean?) {
|
|
if (result == true) {
|
|
activity.get()?.updateViews(type, index, outputPath)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
internal fun updateViews(type: MediaType, viewIndex: Int, outputPath: String) {
|
|
when (type) {
|
|
MediaType.IMAGE -> {
|
|
if (imageLayout.visibility != View.VISIBLE)
|
|
imageLayout.visibility = View.VISIBLE
|
|
val mirrored = BitmapFactory.decodeFile(outputPath)
|
|
val view = when (viewIndex) {
|
|
1 -> { path1 = outputPath; imageView1 }
|
|
2 -> { path2 = outputPath; imageView2 }
|
|
else -> return
|
|
}
|
|
view.setImageBitmap(mirrored)
|
|
}
|
|
MediaType.VIDEO -> {
|
|
if (videoLayout.visibility != View.VISIBLE)
|
|
videoLayout.visibility = View.VISIBLE
|
|
val view = when (viewIndex) {
|
|
1 -> { path1 = outputPath; videoView1 }
|
|
2 -> { path2 = outputPath; videoView2 }
|
|
else -> return
|
|
}
|
|
view.setVideoPath(outputPath)
|
|
view.setOnPreparedListener { mp -> mp.isLooping = true }
|
|
view.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun save(path: String, type: MediaType) {
|
|
val writePermission = Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
val permissionStatus = checkSelfPermission(writePermission)
|
|
if (permissionStatus == PackageManager.PERMISSION_GRANTED) {
|
|
saveWithPerm(path, type)
|
|
} else {
|
|
savingPath = path
|
|
savingType = type
|
|
requestPermissions(arrayOf(writePermission), REQ_WRITE_PERM)
|
|
}
|
|
}
|
|
|
|
private fun saveWithPerm(path: String, type: MediaType) {
|
|
// Ensure media directories for the app exist.
|
|
val envDirId = when (type) {
|
|
MediaType.IMAGE -> Environment.DIRECTORY_PICTURES
|
|
MediaType.VIDEO -> Environment.DIRECTORY_MOVIES
|
|
}
|
|
val externalMediaDir = getExternalFilesDir(envDirId)
|
|
?: return Unit .also { Log.e(TAG, "Can't find an external dir") }
|
|
val mediaDir = File(externalMediaDir, getString(R.string.app_name))
|
|
if (!mediaDir.exists() && !mediaDir.mkdir()) {
|
|
Log.e(TAG, "Failed to create app media dir: $mediaDir")
|
|
Toast.makeText(this, R.string.write_error, Toast.LENGTH_SHORT).show()
|
|
return
|
|
}
|
|
|
|
val extension = getFileExtension(path).let { if (it.isEmpty()) "xxx" else it }
|
|
val outputFile = File(mediaDir.canonicalPath, "${System.currentTimeMillis()}.$extension")
|
|
if (!outputFile.createNewFile()) {
|
|
Log.e(TAG, "Failed to create new file: $outputFile")
|
|
Toast.makeText(this, R.string.write_error, Toast.LENGTH_SHORT).show()
|
|
return
|
|
}
|
|
|
|
FileInputStream(path).use { fis ->
|
|
FileOutputStream(outputFile).use { fos ->
|
|
fos.buffered().write(fis.buffered().readBytes())
|
|
}
|
|
}
|
|
Log.i(TAG, "File saved at $outputFile")
|
|
}
|
|
|
|
private fun share(path: String, type: MediaType) {
|
|
val uri = FileProvider.getUriForFile(
|
|
this,
|
|
"dev.lowrespalmtree.fileprovider",
|
|
File(path)
|
|
)
|
|
Log.i(TAG, "share with uri $uri")
|
|
val shareIntent = Intent().also {
|
|
it.action = Intent.ACTION_SEND
|
|
it.putExtra(Intent.EXTRA_STREAM, uri)
|
|
it.type = when (type) {
|
|
MediaType.IMAGE -> "image/*"
|
|
MediaType.VIDEO -> "video/*"
|
|
}
|
|
}
|
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.send)))
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "ZMINGZ"
|
|
private const val REQ_PICK_IMG = 1
|
|
private const val REQ_PICK_VID = 2
|
|
private const val REQ_WRITE_PERM = 3
|
|
private const val VF1 = "crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left][right] hstack"
|
|
private const val VF2 = "crop=iw/2:ih:iw/2:0,split[left][tmp];[tmp]hflip[right];[right][left] hstack"
|
|
|
|
private fun getFileExtension(path: String): String {
|
|
if (!path.contains('.'))
|
|
return ""
|
|
return path.substring(path.lastIndexOf('.') + 1)
|
|
}
|
|
}
|
|
|
|
} |