Skip to content

Commit

Permalink
Move library from sdcard root to app-specific directory on startup (#50)
Browse files Browse the repository at this point in the history
Needed for Android 11+ scoped storage. Also updates Kotlin, Gradle, and other dependencies.
  • Loading branch information
dozingcat authored Oct 18, 2021
1 parent ddd7dcb commit 8f9c167
Show file tree
Hide file tree
Showing 17 changed files with 170 additions and 39 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ android {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.preference:preference-ktx:1.1.1"
testImplementation 'junit:junit:4.12'
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/dozingcatsoftware/util/images.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fun scaledBitmapFromURIWithMinimumSize(
options.inSampleSize = Math.min(wratio, hratio).toInt()

return BitmapFactory.decodeStream(
context.contentResolver.openInputStream(imageURI), null, options)
context.contentResolver.openInputStream(imageURI), null, options)!!
}

/**
Expand All @@ -87,7 +87,7 @@ fun scaledBitmapFromURIWithMaximumSize(
options.inSampleSize = Math.max(wratio, hratio)

return BitmapFactory.decodeStream(
context.contentResolver.openInputStream(imageURI), null, options)
context.contentResolver.openInputStream(imageURI), null, options)!!
}

private fun powerOf2GreaterOrEqual(arg: Double): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
imageAllocationCallback: ((CameraImage) -> Unit)? = null) {
Log.i(TAG, "start(), status=${status}")
this.captureSize = pickBestSize(
cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
.getOutputSizes(ImageFormat.YUV_420_888),
targetSize)
this.targetStatus = targetStatus
Expand Down Expand Up @@ -213,8 +213,8 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
request.addTarget(allocation!!.surface)
if (this.zoomRatio > 0) {
val cc = cameraCharacteristics
val baseRect = cc.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
val maxZoom = cc.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
val baseRect = cc.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
val maxZoom = cc.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)!!
val zoom = Math.pow(maxZoom.toDouble(), this.zoomRatio)
val width = (baseRect.width() / zoom).toInt()
val height = (baseRect.height() / zoom).toInt()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import java.util.Date


class ImageListActivity : Activity() {
private val photoLibrary = PhotoLibrary.defaultLibrary()
private lateinit var photoLibrary: PhotoLibrary

private lateinit var gridView: GridView
private var gridImageIds: List<String>? = null
Expand All @@ -34,6 +34,7 @@ class ImageListActivity : Activity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.imagegrid)
photoLibrary = PhotoLibrary.defaultLibrary(this)
val self = this
gridView = findViewById(R.id.gridview)
gridView.onItemClickListener = OnItemClickListener { parent, view, position, id ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dozingcatsoftware.vectorcamera

import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Intent
import android.content.res.Configuration
Expand Down Expand Up @@ -33,7 +34,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var imageProcessor: CameraImageProcessor
private var preferredImageSize = ImageSize.HALF_SCREEN

private val photoLibrary = PhotoLibrary.defaultLibrary()
private lateinit var photoLibrary: PhotoLibrary

private lateinit var rs: RenderScript
private val effectRegistry = EffectRegistry()
Expand All @@ -55,8 +56,14 @@ class MainActivity : AppCompatActivity() {

private var layoutIsPortrait = false

private var askedForPermissions = false
private var libraryMigrationDone = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

photoLibrary = PhotoLibrary.defaultLibrary(this)

requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
PreferenceManager.setDefaultValues(this.baseContext, R.xml.preferences, false)
Expand Down Expand Up @@ -145,7 +152,7 @@ class MainActivity : AppCompatActivity() {
Log.i(TAG, "Selected photo: ${intent!!.data}")
Thread({
try {
val imageId = ProcessImageOperation().processImage(this, intent.data)
val imageId = ProcessImageOperation().processImage(this, intent.data!!)
handler.post({
ViewImageActivity.startActivityWithImageId(this, imageId)
})
Expand All @@ -159,6 +166,74 @@ class MainActivity : AppCompatActivity() {
}
}

private fun migratePhotoLibraryIfNeeded() {
if (libraryMigrationDone) {
return
}
val needsMigration = PhotoLibrary.shouldMigrateToPrivateStorage(this)
if (!needsMigration) {
libraryMigrationDone = true
return
}
else {
handler.post {
val migrationSpinner = ProgressDialog(this)
migrationSpinner.isIndeterminate = true
migrationSpinner.setMessage("Moving library...")
migrationSpinner.setCancelable(false)
migrationSpinner.show()

Thread {
var numFiles = 0
var totalBytes = 0L
var migrationError: Exception? = null
try {
PhotoLibrary.migrateToPrivateStorage(this) {fileSize ->
handler.post {
numFiles += 1
totalBytes += fileSize
val mb = String.format("%.1f", totalBytes / 1e6)
val msg = "Moving library:\nProcessed $numFiles files, ${mb}MB";
migrationSpinner.setMessage(msg)
}
}
libraryMigrationDone = true
Log.i(TAG, "Migration succeeded")
if (PhotoLibrary.shouldMigrateToPrivateStorage(this)) {
Log.i(TAG, "Hmm, but previous library is still there?")
}
}
catch (ex: Exception) {
Log.e(TAG, "Migration failed", ex)
migrationError = ex
}
handler.post {
migrationSpinner.hide()
val finishedMsg = if (libraryMigrationDone)
"""
Your Vector Camera library has been moved to private storage.
This is necessary to support Android 11. You shouldn't notice any
difference, but be aware that your library will be deleted if you
uninstall the app.
""".trimIndent().replace(System.lineSeparator(), " ")
else
"""
There was an error moving your Vector Camera library to private
storage (necessary to support Android 11). If this persists,
contact bnenning@gmail.com.
""".trimIndent().replace(System.lineSeparator(), " ") +
"\n\nThe error was:\n$migrationError"
AlertDialog.Builder(this)
.setMessage(finishedMsg)
.setPositiveButton("OK", null)
.show()

}
}.start()
}
}
}

private fun updateControls() {
var shutterResId = R.drawable.btn_camera_shutter_holo
if (shutterMode == ShutterMode.VIDEO) {
Expand Down Expand Up @@ -255,10 +330,16 @@ class MainActivity : AppCompatActivity() {
}

private fun checkPermissionAndStartCamera() {
if (PermissionsChecker.hasCameraPermission(this)) {
val hasCamera = PermissionsChecker.hasCameraPermission(this)
val hasStorage = PermissionsChecker.hasStoragePermission(this)
if (hasCamera) {
restartCameraImageGenerator()
}
else {
if (hasStorage) {
migratePhotoLibraryIfNeeded()
}
if (!askedForPermissions && !(hasCamera && hasStorage)) {
askedForPermissions = true
PermissionsChecker.requestCameraAndStoragePermissions(this)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ class NewPictureReceiver : BroadcastReceiver() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return
}
Log.i(javaClass.name, "Got picture: " + intent.data!!)
val data = intent.data!!
Log.i(javaClass.name, "Got picture: $data")
try {
ProcessImageOperation().processImage(context, intent.data)
ProcessImageOperation().processImage(context, data)
} catch (ex: Exception) {
Log.e(javaClass.name, "Error saving picture", ex)
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,54 @@ class PhotoLibrary(val rootDirectory: File) {
PHOTO_ID_FORMAT.timeZone = TimeZone.getTimeZone("UTC")
}

fun defaultLibrary(): PhotoLibrary {
return PhotoLibrary(
File(Environment.getExternalStorageDirectory(), "VectorCamera"))
fun defaultLibrary(context: Context): PhotoLibrary {
Log.i(TAG, "external storage dir: ${Environment.getExternalStorageDirectory()}")
Log.i(TAG, "filesDir: ${context.filesDir}")
Log.i(TAG, "externalFilesDir: ${context.getExternalFilesDir(null)}")
// val baseDir = Environment.getExternalStorageDirectory()
val baseDir = context.getExternalFilesDir(null)
return PhotoLibrary(File(baseDir, "VectorCamera"))
}

// Checks for the existence of a VectorCamera directory in the root of
// external storage, and if found moves it to `context.getExternalFilesDir`.
// Needed to migrate to Android 11, which disallows writing to external
// storage except for app-specific directories.
// See https://developer.android.com/training/data-storage/use-cases#migrate-legacy-storage
fun shouldMigrateToPrivateStorage(context: Context): Boolean {
val rootDir = File(Environment.getExternalStorageDirectory(), "VectorCamera")
return rootDir.isDirectory
}

fun migrateToPrivateStorage(context: Context, progressHandler: (fileSize: Long) -> Unit) {
val rootDir = File(Environment.getExternalStorageDirectory(), "VectorCamera")
val privateDir = File(context.getExternalFilesDir(null), "VectorCamera")
if (rootDir.isDirectory) {
Log.i(
TAG,
"Moving directory at ${rootDir.absolutePath} to ${privateDir.absolutePath}"
)
copyAndRemoveDir(rootDir, privateDir, progressHandler)
}
}

private fun copyAndRemoveDir(src: File, dst: File, progressHandler: (fileSize: Long) -> Unit) {
src.listFiles().forEach {
if (it.isDirectory) {
copyAndRemoveDir(it, File(dst, it.name), progressHandler)
} else {
copyAndRemoveFile(it, File(dst, it.name), progressHandler)
}
}
val isDeleted = src.delete()
Log.i(TAG, "Copied directory: ${src.absolutePath}, deleted=$isDeleted")
}

private fun copyAndRemoveFile(src: File, dst: File, progressHandler: (fileSize: Long) -> Unit) {
src.copyTo(dst, overwrite = true)
progressHandler(src.length())
val isDeleted = src.delete()
Log.i(TAG, "Copied file: ${src.absolutePath}, deleted=$isDeleted")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import com.dozingcatsoftware.util.scaledBitmapFromURIWithMaximumSize
import com.dozingcatsoftware.vectorcamera.effect.EffectRegistry

class ProcessImageOperation(val timeFn: (() -> Long) = System::currentTimeMillis) {
private val photoLibrary = PhotoLibrary.defaultLibrary()

fun processImage(context: Context, imageUri: Uri): String {
val photoLibrary = PhotoLibrary.defaultLibrary(context)
Log.i(TAG, "Processing image: ${imageUri}")
val t1 = timeFn()
val rs = RenderScript.create(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class VCPreferences(val context: Context) {

private fun sharedPrefs() = PreferenceManager.getDefaultSharedPreferences(context)

fun effectName(): String = sharedPrefs().getString(EFFECT_NAME_KEY, "")
fun effectName(): String = sharedPrefs().getString(EFFECT_NAME_KEY, "")!!

fun useHighQualityPreview() = sharedPrefs().getBoolean(HIGH_QUALITY_PREVIEW_KEY, false)

Expand All @@ -28,7 +28,7 @@ class VCPreferences(val context: Context) {

val lookupFunction = fun(key: String, defaultValue: Any): Any {
if (defaultValue is String) {
return sharedPrefs().getString(key, defaultValue)
return sharedPrefs().getString(key, defaultValue)!!
}
if (defaultValue is Int) {
return sharedPrefs().getInt(key, defaultValue)
Expand All @@ -44,7 +44,7 @@ class VCPreferences(val context: Context) {
}

fun effectParameters(): Map<String, Any> {
val effectJson = sharedPrefs().getString(EFFECT_PARAMETERS_KEY, "")
val effectJson = sharedPrefs().getString(EFFECT_PARAMETERS_KEY, "")!!
if (effectJson.isEmpty()) {
return mapOf()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import java.io.File


class ViewImageActivity : Activity() {
private val photoLibrary = PhotoLibrary.defaultLibrary()
private lateinit var photoLibrary: PhotoLibrary
private lateinit var rs : RenderScript
private lateinit var imageId: String
private var inEffectSelectionMode = false
Expand All @@ -39,13 +39,14 @@ class ViewImageActivity : Activity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_image)
rs = RenderScript.create(this)
photoLibrary = PhotoLibrary.defaultLibrary(this)

switchEffectButton.setOnClickListener(this::toggleEffectSelectionMode)
shareButton.setOnClickListener(this::shareImage)
deleteButton.setOnClickListener(this::deleteImage)
overlayView.touchEventHandler = this::handleOverlayViewTouch

imageId = intent.getStringExtra("imageId")
imageId = intent.getStringExtra("imageId")!!
loadImage()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal enum class ExportType private constructor(
}

class ViewVideoActivity: Activity() {
private val photoLibrary = PhotoLibrary.defaultLibrary()
private lateinit var photoLibrary: PhotoLibrary
private lateinit var rs : RenderScript
private lateinit var videoId: String
private var inEffectSelectionMode = false
Expand All @@ -73,6 +73,7 @@ class ViewVideoActivity: Activity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_video)
rs = RenderScript.create(this)
photoLibrary = PhotoLibrary.defaultLibrary(this)

shareButton.setOnClickListener(this::doShare)
switchEffectButton.setOnClickListener(this::toggleEffectSelectionMode)
Expand All @@ -81,7 +82,7 @@ class ViewVideoActivity: Activity() {
overlayView.touchEventHandler = this::handleOverlayViewTouch

// Yes, this does I/O.
videoId = intent.getStringExtra("videoId")
videoId = intent.getStringExtra("videoId")!!
videoReader = VideoReader(rs, photoLibrary, videoId, getLandscapeDisplaySize(this))

frameSeekBar.max = videoReader.numberOfFrames() - 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ class MatrixEffect(val rs: RenderScript, val effectParams: Map<String, Any>): Ef
val ca = ByteArray(4 * numCells)
for (i in 0 until numCells) {
val fraction = toUInt(blockAverages[i]) / 255.0
ca[4*i] = (fraction * maxTextRed).toByte()
ca[4*i + 1] = (fraction * maxTextGreen).toByte()
ca[4*i + 2] = (fraction * maxTextBlue).toByte()
ca[4*i] = (fraction * maxTextRed).toInt().toByte()
ca[4*i + 1] = (fraction * maxTextGreen).toInt().toByte()
ca[4*i + 2] = (fraction * maxTextBlue).toInt().toByte()
ca[4*i + 3] = 0xff.toByte()
}
val raindropLifetimeMillis = maxRaindropLength * raindropMillisPerTick + raindropDecayMillis
Expand Down Expand Up @@ -139,11 +139,11 @@ class MatrixEffect(val rs: RenderScript, val effectParams: Map<String, Any>): Ef
}
val brightness = (255 * fraction).roundToInt().toByte()
val baseOffset = 4 * (y * metrics.numCharacterColumns + drop.x)
ca[baseOffset] = if (dy == ticks) brightness else (fraction * maxTextRed).toByte()
ca[baseOffset] = if (dy == ticks) brightness else (fraction * maxTextRed).toInt().toByte()
ca[baseOffset + 1] =
if (dy == ticks) brightness else (fraction * maxTextGreen).toByte()
if (dy == ticks) brightness else (fraction * maxTextGreen).toInt().toByte()
ca[baseOffset + 2] =
if (dy == ticks) brightness else (fraction * maxTextBlue).toByte()
if (dy == ticks) brightness else (fraction * maxTextBlue).toInt().toByte()
ca[baseOffset + 3] = 0xff.toByte()
}
}
Expand Down
Loading

0 comments on commit 8f9c167

Please sign in to comment.