Save files to app external dirs

This commit is contained in:
dece 2020-05-01 21:06:02 +02:00
parent 97c3f59741
commit 1a925bd070
4 changed files with 132 additions and 19 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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>