Save files to app external dirs
This commit is contained in:
parent
97c3f59741
commit
1a925bd070
|
@ -2,6 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="dev.lowrespalmtree.zmingz">
|
package="dev.lowrespalmtree.zmingz">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
@ -1,28 +1,68 @@
|
||||||
package dev.lowrespalmtree.zmingz
|
package dev.lowrespalmtree.zmingz
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
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 kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class MainActivity: AppCompatActivity() {
|
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
|
||||||
|
|
||||||
|
private enum class MediaType { IMAGE, VIDEO }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fun openFile(v: View) {
|
||||||
val req: Int
|
val req: Int
|
||||||
val pickIntent = when (v.id) {
|
val pickIntent = when (v.id) {
|
||||||
|
@ -39,17 +79,36 @@ class MainActivity: AppCompatActivity() {
|
||||||
startActivityForResult(pickIntent, req)
|
startActivityForResult(pickIntent, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
fun openViewMenu(v: View) {
|
||||||
when (requestCode) {
|
val menu = PopupMenu(this, v)
|
||||||
REQ_PICK_IMG, REQ_PICK_VID -> handlePickResult(requestCode, resultCode, data)
|
val saveItem = menu.menu.add(R.string.save)
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
val shareItem = menu.menu.add(R.string.share)
|
||||||
|
menu.setOnMenuItemClickListener { item ->
|
||||||
|
val path = when (v.id) {
|
||||||
|
imageView1.id, videoView1.id -> path1
|
||||||
|
imageView2.id, videoView2.id -> path2
|
||||||
|
else -> return@setOnMenuItemClickListener false
|
||||||
|
}
|
||||||
|
when (item.itemId) {
|
||||||
|
saveItem.itemId -> {
|
||||||
|
val type = when (v.id) {
|
||||||
|
imageView1.id, imageView2.id -> MediaType.IMAGE
|
||||||
|
videoView1.id, videoView2.id -> MediaType.VIDEO
|
||||||
|
else -> return@setOnMenuItemClickListener false
|
||||||
|
}
|
||||||
|
save(path, type)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
menu.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
private fun handlePickResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private fun handlePickResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
val uri = data?.data
|
val uri = data?.data
|
||||||
?: return Unit .also { Log.e(TAG, "No intent or data.") }
|
?: return Unit .also { Log.e(TAG, "No intent or data") }
|
||||||
val uriPath = uri.path
|
val uriPath = uri.path
|
||||||
?: return Unit .also { Log.e(TAG, "No path in URI") }
|
?: return Unit .also { Log.e(TAG, "No path in URI") }
|
||||||
val extension = getFileExtension(uriPath)
|
val extension = getFileExtension(uriPath)
|
||||||
|
@ -64,6 +123,10 @@ class MainActivity: AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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", cacheDir)
|
val outputFile1 = File.createTempFile("output1", ".$extension", cacheDir)
|
||||||
MirrorTask(WeakReference(this), requestCode, 1)
|
MirrorTask(WeakReference(this), requestCode, 1)
|
||||||
.execute(inputFile.canonicalPath, outputFile1.canonicalPath, VF1)
|
.execute(inputFile.canonicalPath, outputFile1.canonicalPath, VF1)
|
||||||
|
@ -85,9 +148,9 @@ class MainActivity: AppCompatActivity() {
|
||||||
val vf = params[2]!!
|
val vf = params[2]!!
|
||||||
val command = "-i $inputPath -vf \"$vf\" -y $outputPath"
|
val command = "-i $inputPath -vf \"$vf\" -y $outputPath"
|
||||||
return when (val rc = FFmpeg.execute(command)) {
|
return when (val rc = FFmpeg.execute(command)) {
|
||||||
RETURN_CODE_SUCCESS -> true. also { Log.d(TAG, "ffmpeg success") }
|
RETURN_CODE_SUCCESS -> true. also { Log.d(TAG, "FFmpeg success") }
|
||||||
RETURN_CODE_CANCEL -> false .also { Log.d(TAG, "ffmpeg canceled") }
|
RETURN_CODE_CANCEL -> false .also { Log.d(TAG, "FFmpeg canceled") }
|
||||||
else -> false .also { Log.d(TAG, "ffmpeg failed with rc $rc") }
|
else -> false .also { Log.d(TAG, "FFmpeg failed with rc $rc") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,25 +164,21 @@ class MainActivity: AppCompatActivity() {
|
||||||
|
|
||||||
internal fun updateViews(requestCode: Int, viewIndex: Int, outputPath: String) {
|
internal fun updateViews(requestCode: Int, viewIndex: Int, outputPath: String) {
|
||||||
if (requestCode == REQ_PICK_IMG) {
|
if (requestCode == REQ_PICK_IMG) {
|
||||||
if (imageLayout.visibility != View.VISIBLE) {
|
if (imageLayout.visibility != View.VISIBLE)
|
||||||
videoLayout.visibility = View.GONE
|
|
||||||
imageLayout.visibility = View.VISIBLE
|
imageLayout.visibility = View.VISIBLE
|
||||||
}
|
|
||||||
val mirrored = BitmapFactory.decodeFile(outputPath)
|
val mirrored = BitmapFactory.decodeFile(outputPath)
|
||||||
val view = when (viewIndex) {
|
val view = when (viewIndex) {
|
||||||
1 -> imageView1
|
1 -> { path1 = outputPath; imageView1 }
|
||||||
2 -> imageView2
|
2 -> { path2 = outputPath; imageView2 }
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
view.setImageBitmap(mirrored)
|
view.setImageBitmap(mirrored)
|
||||||
} else if (requestCode == REQ_PICK_VID) {
|
} else if (requestCode == REQ_PICK_VID) {
|
||||||
if (videoLayout.visibility != View.VISIBLE) {
|
if (videoLayout.visibility != View.VISIBLE)
|
||||||
imageLayout.visibility = View.GONE
|
|
||||||
videoLayout.visibility = View.VISIBLE
|
videoLayout.visibility = View.VISIBLE
|
||||||
}
|
|
||||||
val view = when (viewIndex) {
|
val view = when (viewIndex) {
|
||||||
1 -> videoView1
|
1 -> { path1 = outputPath; videoView1 }
|
||||||
2 -> videoView2
|
2 -> { path2 = outputPath; videoView2 }
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
view.setVideoPath(outputPath)
|
view.setVideoPath(outputPath)
|
||||||
|
@ -128,10 +187,54 @@ class MainActivity: AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ZMINGZ"
|
private const val TAG = "ZMINGZ"
|
||||||
private const val REQ_PICK_IMG = 1
|
private const val REQ_PICK_IMG = 1
|
||||||
private const val REQ_PICK_VID = 2
|
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 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 const val VF2 = "crop=iw/2:ih:iw/2:0,split[left][tmp];[tmp]hflip[right];[right][left] hstack"
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0.5"
|
android:layout_weight="0.5"
|
||||||
|
android:onClick="openViewMenu"
|
||||||
android:contentDescription="@android:string/untitled" />
|
android:contentDescription="@android:string/untitled" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0.5"
|
android:layout_weight="0.5"
|
||||||
|
android:onClick="openViewMenu"
|
||||||
android:contentDescription="@android:string/untitled" />
|
android:contentDescription="@android:string/untitled" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -50,6 +52,7 @@
|
||||||
android:id="@+id/videoView1"
|
android:id="@+id/videoView1"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="openViewMenu"
|
||||||
app:layout_constraintBottom_toTopOf="@id/videoView2"
|
app:layout_constraintBottom_toTopOf="@id/videoView2"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
@ -60,6 +63,7 @@
|
||||||
android:id="@+id/videoView2"
|
android:id="@+id/videoView2"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="openViewMenu"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
|
|
@ -3,4 +3,8 @@
|
||||||
|
|
||||||
<string name="open_image">Image</string>
|
<string name="open_image">Image</string>
|
||||||
<string name="open_video">Video</string>
|
<string name="open_video">Video</string>
|
||||||
|
<string name="please_wait">Please wait...</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="share">Share</string>
|
||||||
|
<string name="write_error">Failed to create file.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Reference in a new issue