Compare commits

..

7 commits

Author SHA1 Message Date
dece 1614cd1aa2 Lower minSdkVersion to 24 for Android 7 friends 2020-05-05 20:48:16 +02:00
dece b87a9a6209 Clean 2020-05-02 01:58:10 +02:00
dece 00c95cf270 Add camera video input 2020-05-02 01:56:44 +02:00
dece 37d5c191e9 Add camera photo input 2020-05-02 01:45:09 +02:00
dece 86e06e8d31 Add readme 2020-05-02 00:44:18 +02:00
dece 38fdeb1667 Try to reduce bundled app size 2020-05-02 00:39:07 +02:00
dece 7f51b02636 Update gitignore 2020-05-02 00:21:05 +02:00
8 changed files with 129 additions and 47 deletions
.gitignoreREADME.md
app
build.gradle
src
androidTest/java/dev/lowrespalmtree/zmingz
main
AndroidManifest.xml
java/dev/lowrespalmtree/zmingz
res

1
.gitignore vendored
View file

@ -8,7 +8,6 @@
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/app/release
/build /build
/captures /captures
.externalNativeBuild .externalNativeBuild

7
README.md Normal file
View file

@ -0,0 +1,7 @@
Zmingz
======
Stupid little mirror app for Android using [(mobile) FFmpeg][mobile-ffmpeg] done
in an afternoon for the sake of it.
[mobile-ffmpeg]: https://github.com/tanersener/mobile-ffmpeg

View file

@ -8,17 +8,16 @@ android {
defaultConfig { defaultConfig {
applicationId "dev.lowrespalmtree.zmingz" applicationId "dev.lowrespalmtree.zmingz"
minSdkVersion 26 minSdkVersion 24
targetSdkVersion 29 targetSdkVersion 29
versionCode 1 versionCode 2
versionName "1.0" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }

View file

@ -1,24 +0,0 @@
package dev.lowrespalmtree.zmingz
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("dev.lowrespalmtree.zmingz", appContext.packageName)
}
}

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-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application

View file

@ -1,6 +1,7 @@
package dev.lowrespalmtree.zmingz package dev.lowrespalmtree.zmingz
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -16,6 +17,7 @@ import android.widget.PopupMenu
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toFile
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
@ -35,6 +37,20 @@ class MainActivity: AppCompatActivity() {
private var savingPath = "" private var savingPath = ""
/** Temporary type for the media to save, to use after perms being granted. */ /** Temporary type for the media to save, to use after perms being granted. */
private var savingType = MediaType.IMAGE private var savingType = MediaType.IMAGE
/** Path for storing camera image. */
private var cameraImagePath = ""
/** Path to media cache dir, ensuring it exists. */
private val mediaCacheDir: File?
get() {
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 null
}
return mediaCacheDir
}
enum class MediaType { IMAGE, VIDEO } enum class MediaType { IMAGE, VIDEO }
@ -59,6 +75,8 @@ class MainActivity: AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQ_PICK_IMG, REQ_PICK_VID -> handlePickResult(requestCode, resultCode, data) REQ_PICK_IMG, REQ_PICK_VID -> handlePickResult(requestCode, resultCode, data)
REQ_TAKE_IMG -> handleTakeImageResult(resultCode)
REQ_TAKE_VID -> handleTakeVideoResult(resultCode, data)
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
@ -79,6 +97,13 @@ class MainActivity: AppCompatActivity() {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
private fun getUriForFile(file: File): Uri =
FileProvider.getUriForFile(
this,
"dev.lowrespalmtree.fileprovider",
file
)
fun openFile(v: View) { fun openFile(v: View) {
val req: Int val req: Int
val pickIntent = when (v.id) { val pickIntent = when (v.id) {
@ -116,6 +141,27 @@ class MainActivity: AppCompatActivity() {
menu.show() menu.show()
} }
fun openCamera(v: View) {
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
Log.e(TAG, "No camera feature")
return
}
when (v.id) {
buttonCamera.id -> {
val inputFile = File(mediaCacheDir, "${System.currentTimeMillis()}.jpg")
cameraImagePath = inputFile.canonicalPath
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.apply { putExtra(MediaStore.EXTRA_OUTPUT, getUriForFile(inputFile)) }
startActivityForResult(captureIntent, REQ_TAKE_IMG)
}
buttonCameraVideo.id -> {
val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
startActivityForResult(captureIntent, REQ_TAKE_VID)
}
}
}
private fun mediaTypeFromViewId(id: Int): MediaType? = private fun mediaTypeFromViewId(id: Int): MediaType? =
when (id) { when (id) {
imageView1.id, imageView2.id -> MediaType.IMAGE imageView1.id, imageView2.id -> MediaType.IMAGE
@ -125,6 +171,8 @@ class MainActivity: AppCompatActivity() {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
private fun handlePickResult(requestCode: Int, resultCode: Int, data: Intent?) { private fun handlePickResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode != Activity.RESULT_OK)
return
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 type = when (requestCode) { val type = when (requestCode) {
@ -135,6 +183,21 @@ class MainActivity: AppCompatActivity() {
processMedia(uri, type) processMedia(uri, type)
} }
private fun handleTakeImageResult(resultCode: Int) {
if (resultCode != Activity.RESULT_OK)
return
processMedia(cameraImagePath, MediaType.IMAGE)
}
private fun handleTakeVideoResult(resultCode: Int, data: Intent?) {
if (resultCode != Activity.RESULT_OK)
return
data?.data
?.let { processMedia(it, MediaType.VIDEO) }
?: return Unit. also { Log.e(TAG, "No data in intent or invalid URI") }
}
/** Process media at URI, copying to a local cache file for FFmpeg beforehand. */
private fun processMedia(uri: Uri, type: MediaType) { private fun processMedia(uri: Uri, type: MediaType) {
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") }
@ -149,23 +212,30 @@ class MainActivity: AppCompatActivity() {
} }
} }
val mediaCacheDir = File(cacheDir, "media") processMedia(inputFile.canonicalPath, type)
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
}
/** Start the async mirroring tasks. */
private fun processMedia(path: String, type: MediaType) {
imageLayout.visibility = View.GONE imageLayout.visibility = View.GONE
videoLayout.visibility = View.GONE videoLayout.visibility = View.GONE
Toast.makeText(this, R.string.please_wait, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.please_wait, Toast.LENGTH_SHORT).show()
val extension = getFileExtension(path).let {
if (it.isEmpty())
when (type) {
MediaType.IMAGE -> "jpg"
MediaType.VIDEO -> "mp4"
}
else
it
}
val outputFile1 = File.createTempFile("output1", ".$extension", mediaCacheDir) val outputFile1 = File.createTempFile("output1", ".$extension", mediaCacheDir)
MirrorTask(WeakReference(this), type, 1) MirrorTask(WeakReference(this), type, 1)
.execute(inputFile.canonicalPath, outputFile1.canonicalPath, VF1) .execute(path, outputFile1.canonicalPath, VF1)
val outputFile2 = File.createTempFile("output2", ".$extension", mediaCacheDir) val outputFile2 = File.createTempFile("output2", ".$extension", mediaCacheDir)
MirrorTask(WeakReference(this), type, 2) MirrorTask(WeakReference(this), type, 2)
.execute(inputFile.canonicalPath, outputFile2.canonicalPath, VF2) .execute(path, outputFile2.canonicalPath, VF2)
} }
class MirrorTask( class MirrorTask(
@ -250,7 +320,15 @@ class MainActivity: AppCompatActivity() {
return return
} }
val extension = getFileExtension(path).let { if (it.isEmpty()) "xxx" else it } val extension = getFileExtension(path).let {
if (it.isEmpty())
when (type) {
MediaType.IMAGE -> "jpg"
MediaType.VIDEO -> "mp4"
}
else
it
}
val outputFile = File(mediaDir.canonicalPath, "${System.currentTimeMillis()}.$extension") val outputFile = File(mediaDir.canonicalPath, "${System.currentTimeMillis()}.$extension")
if (!outputFile.createNewFile()) { if (!outputFile.createNewFile()) {
Log.e(TAG, "Failed to create new file: $outputFile") Log.e(TAG, "Failed to create new file: $outputFile")
@ -267,11 +345,7 @@ class MainActivity: AppCompatActivity() {
} }
private fun share(path: String, type: MediaType) { private fun share(path: String, type: MediaType) {
val uri = FileProvider.getUriForFile( val uri = getUriForFile(File(path))
this,
"dev.lowrespalmtree.fileprovider",
File(path)
)
Log.i(TAG, "share with uri $uri") Log.i(TAG, "share with uri $uri")
val shareIntent = Intent().also { val shareIntent = Intent().also {
it.action = Intent.ACTION_SEND it.action = Intent.ACTION_SEND
@ -289,6 +363,8 @@ class MainActivity: AppCompatActivity() {
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 REQ_WRITE_PERM = 3
private const val REQ_TAKE_IMG = 4
private const val REQ_TAKE_VID = 5
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

@ -83,6 +83,28 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
style="@style/Widget.AppCompat.ButtonBar"> style="@style/Widget.AppCompat.ButtonBar">
<ImageButton
android:id="@+id/buttonCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:onClick="openCamera"
android:src="@android:drawable/ic_menu_camera"
style="?android:attr/buttonBarButtonStyle"
android:contentDescription="@android:string/untitled" />
<ImageButton
android:id="@+id/buttonCameraVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:onClick="openCamera"
android:src="@android:drawable/ic_media_play"
style="?android:attr/buttonBarButtonStyle"
android:contentDescription="@android:string/untitled" />
<Button <Button
android:id="@+id/buttonImage" android:id="@+id/buttonImage"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -1,11 +1,12 @@
<resources> <resources>
<string name="app_name">Zmingz</string> <string name="app_name">Zmingz</string>
<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="please_wait">Please wait</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="write_error">Write error.</string> <string name="write_error">Write error.</string>
<string name="send">Send</string> <string name="send">Send</string>
<string name="open_camera">Camera</string>
</resources> </resources>