Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Saving media to public directory #56

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 29
compileSdkVersion 33
defaultConfig {
applicationId "com.dozingcatsoftware.vectorcamera"
minSdkVersion 23
targetSdkVersion 29
targetSdkVersion 33
renderscriptTargetApi 23
versionCode 13
versionName "1.6.0"
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
android:requestLegacyExternalStorage="true">

<activity android:name="com.dozingcatsoftware.vectorcamera.MainActivity"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down Expand Up @@ -50,7 +51,8 @@
</activity>

<receiver android:name="com.dozingcatsoftware.vectorcamera.NewPictureReceiver" android:label="NewPictureReceiver"
android:enabled="false">
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.hardware.action.NEW_PICTURE" />
<data android:mimeType="image/*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity() {
saveIndicator.show()
(Thread {
try {
val photoId = photoLibrary.savePhoto(this, pb)
val photoId = photoLibrary.savePhoto(applicationContext, pb)
saveIndicator.dismiss()
handler.post {
ViewImageActivity.startActivityWithImageId(this, photoId)
Expand Down
149 changes: 131 additions & 18 deletions app/src/main/java/com/dozingcatsoftware/vectorcamera/PhotoLibrary.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package com.dozingcatsoftware.vectorcamera

import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.renderscript.RenderScript
import android.util.Log
import android.util.Size
import androidx.core.net.toFile
import com.dozingcatsoftware.vectorcamera.effect.EffectMetadata
import com.dozingcatsoftware.util.*
import org.json.JSONObject
Expand All @@ -17,7 +23,9 @@ import java.util.zip.GZIPOutputStream

/**
* Directory structure:
* [root]/
*
* // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
* [app-directory]/
* thumbnails/
* [image_id].jpg
* [video_id].jpg
Expand All @@ -36,6 +44,30 @@ import java.util.zip.GZIPOutputStream
* [video ID of recording in progress]_video.dat
* [video ID of recording in progress]_audio.pcm
*
* // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
* [app-directory]/
* thumbnails/
* [image_id].jpg
* [video_id].jpg
* metadata/
* [image_id].json
* [video_id].json
* raw/
* [image_id].gz
* [video_id]_video.dat
* [video_id]_audio.pcm
* tmp/
* [video ID of recording in progress]_video.dat
* [video ID of recording in progress]_audio.pcm
* [mediaStore]/
* DCIM/
* VectorCamera/
* Images/
* [image_id].png
* Videos/
* [video_id].webm (if exported)
*
*
* "raw_tmp" holds in-progress video recordings, so they can be cleaned up if the recording fails.
* Images are stored as flattened YUV data; first (width*height) bytes of Y, then
* (width*height/4) bytes of U, then (width*height/4) bytes of V. Video files store individual
Expand Down Expand Up @@ -135,14 +167,52 @@ class PhotoLibrary(val rootDirectory: File) {
fun writePngImage(context: Context, pb: ProcessedBitmap, itemId: String) {
val t1 = System.currentTimeMillis()
val resultBitmap = pb.renderBitmap(pb.sourceImage.width(), pb.sourceImage.height())
imageDirectory.mkdirs()
val imageFile = imageFileForItemId(itemId)
writeFileAtomicallyUsingTempDir(imageFile, getTempDirectory(), {
resultBitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
})

/** ref:
* https://stackoverflow.com/a/63870196/2445763
* https://gitlab.com/commonsguy/cw-android-q/-/blob/vFINAL/ConferenceVideos/src/main/java/com/commonsware/android/conferencevideos/VideoRepository.kt
* https://stackoverflow.com/a/56990305/2445763
**/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, itemId)
put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_DCIM}/VectorCamera/Images")
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
put(MediaStore.Images.Media.IS_PENDING, 1)
}

val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

try {
val outputStream = resolver.openOutputStream(uri!!)
Log.d("PhotoLibrary@writePngImage", uri.path.toString())
resultBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)

values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, values, null, null)
} catch(e: IOException) {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
context.contentResolver.delete(orphanUri, null, null)
}

throw e
}

} else {
imageDirectory.mkdirs()
val imageFile = imageFileForItemId(itemId)

writeFileAtomicallyUsingTempDir(imageFile, getTempDirectory()) {
resultBitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
scanSavedMediaFile(context, imageFile.path)
}

val t2 = System.currentTimeMillis()
scanSavedMediaFile(context, imageFile.path)
Log.i(TAG, "writePngImage: ${t2-t1}")
Log.i(TAG, "writePngImage: ${t2 - t1}")
}

/**
Expand Down Expand Up @@ -271,7 +341,7 @@ class PhotoLibrary(val rootDirectory: File) {
}

fun imageFileForItemId(itemId: String): File {
return File(imageDirectory, itemId + ".png")
return File(imageDirectory, itemId + ".png")
}

fun videoFileForItemId(itemId: String): File {
Expand Down Expand Up @@ -302,15 +372,58 @@ class PhotoLibrary(val rootDirectory: File) {
videoFramesArchiveForItemId(itemId).length())
}

fun deleteItem(itemId: String): Boolean {
// Some or all of these will not exist, which is fine.
imageFileForItemId(itemId).delete()
videoFileForItemId(itemId).delete()
rawImageFileForItemId(itemId).delete()
rawVideoFileForItemId(itemId).delete()
rawAudioFileForItemId(itemId).delete()
videoFramesArchiveForItemId(itemId).delete()
return metadataFileForItemId(itemId).delete()
fun deleteItem(context: Context, itemId: String): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?"
val selectionArgs = arrayOf("${Environment.DIRECTORY_DCIM}/VectorCamera/Images/")
val cursor = resolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null)
var uri: Uri? = null

cursor?.let {
Log.d("PhotoLibrary@it.count", "${it.count}")
if (it.count > 0) {
while (it.moveToNext()) {
val itemFileName = it.getString(it.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))

if (itemFileName.equals("${itemId}.png")) {
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))

uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
break;
}
}
}

cursor.close()
}

Log.d("PhotoLibrary@imageFileForItemId", uri?.path.toString())

uri?.let{
val result = resolver.delete(it, null, null)
if (result > 0) {
Log.d("PhotoLibrary@deleteItem", "${it.path.toString()} deleted!");
}
}

// Some or all of these will not exist, which is fine.
videoFileForItemId(itemId).delete()
rawImageFileForItemId(itemId).delete()
rawVideoFileForItemId(itemId).delete()
rawAudioFileForItemId(itemId).delete()
videoFramesArchiveForItemId(itemId).delete()
return metadataFileForItemId(itemId).delete()
} else {
// Some or all of these will not exist, which is fine.
imageFileForItemId(itemId).delete()
videoFileForItemId(itemId).delete()
rawImageFileForItemId(itemId).delete()
rawVideoFileForItemId(itemId).delete()
rawAudioFileForItemId(itemId).delete()
videoFramesArchiveForItemId(itemId).delete()
return metadataFileForItemId(itemId).delete()
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ class ViewImageActivity : Activity() {

private fun deleteImage(view: View) {
val deleteFn = { _: DialogInterface, _: Int ->
photoLibrary.deleteItem(imageId)
photoLibrary.deleteItem(this, imageId)
finish()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package com.dozingcatsoftware.vectorcamera
import android.app.Activity
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ContentValues
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.provider.MediaStore
import android.renderscript.RenderScript
import androidx.core.content.FileProvider
import android.view.MotionEvent
Expand All @@ -23,6 +27,8 @@ import com.dozingcatsoftware.util.grantUriPermissionForIntent
import com.dozingcatsoftware.util.scanSavedMediaFile
import kotlinx.android.synthetic.main.view_video.*
import java.io.File
import java.io.FileInputStream
import java.io.IOException


// Ways videos can be exported, and the messages shown in the export progress dialog for each.
Expand Down Expand Up @@ -316,7 +322,43 @@ class ViewVideoActivity: Activity() {
progressDialog.dismiss()
if (result.status == ProcessVideoTask.ResultStatus.SUCCEEDED) {
if (exportType == ExportType.WEBM) {
scanSavedMediaFile(this, result.outputFile!!.path)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, videoId)
put(MediaStore.Video.Media.TITLE, videoId)
put(MediaStore.Video.Media.RELATIVE_PATH, "${Environment.DIRECTORY_DCIM}/VectorCamera/Videos")
put(MediaStore.Video.Media.MIME_TYPE, "video/webm")
put(MediaStore.Video.Media.IS_PENDING, 1)
}

val resolver = this.contentResolver
val uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)

try {
val outputStream = resolver.openOutputStream(uri!!)
val inputStream = FileInputStream(result.outputFile)

outputStream?.apply {
inputStream.copyTo(this)
flush()
close()
inputStream.close()
}

values.clear()
values.put(MediaStore.Video.Media.IS_PENDING, 0)
resolver.update(uri, values, null, null)
} catch(e: IOException) {
uri?.let { orphanUri ->
// Don't leave an orphan entry in the MediaStore
this.contentResolver.delete(orphanUri, null, null)
}

throw e
}
} else {
scanSavedMediaFile(this, result.outputFile!!.path)
}
}
val metadata = photoLibrary.metadataForItemId(videoId)
val newMetadata = metadata.withExportedEffectMetadata(
Expand Down Expand Up @@ -388,7 +430,7 @@ class ViewVideoActivity: Activity() {

private fun deleteVideo(view: View) {
val deleteFn = { _: DialogInterface, _: Int ->
photoLibrary.deleteItem(videoId)
photoLibrary.deleteItem(view.context, videoId)
finish()
}

Expand Down