Switch to Kotlin coroutines

This commit is contained in:
Matéo Duparc 2022-04-20 15:17:33 +02:00
parent 72cce1d7e1
commit e00abdf5bb
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
10 changed files with 528 additions and 471 deletions

View File

@ -25,6 +25,9 @@ import androidx.camera.extensions.ExtensionsManager
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
@ -386,19 +389,17 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
if (timerDuration > 0){ if (timerDuration > 0){
binding.textTimer.visibility = View.VISIBLE binding.textTimer.visibility = View.VISIBLE
isWaitingForTimer = true isWaitingForTimer = true
Thread{ lifecycleScope.launch {
for (i in timerDuration downTo 1){ for (i in timerDuration downTo 1){
runOnUiThread { binding.textTimer.text = i.toString() } binding.textTimer.text = i.toString()
Thread.sleep(1000) delay(1000)
} }
if (!isFinishing) { if (!isFinishing) {
runOnUiThread { action()
action() binding.textTimer.visibility = View.GONE
binding.textTimer.visibility = View.GONE
}
} }
isWaitingForTimer = false isWaitingForTimer = false
}.start() }
} else { } else {
action() action()
} }

View File

@ -7,7 +7,7 @@ import android.text.InputType
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.util.* import java.util.*
@ -105,62 +105,63 @@ class ChangePasswordActivity: BaseActivity() {
} }
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) { private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) { var returnedHash: ByteArray? = null
override fun doTask(activity: AppCompatActivity) { if (binding.checkboxSavePassword.isChecked) {
var returnedHash: ByteArray? = null returnedHash = ByteArray(GocryptfsVolume.KeyLen)
if (binding.checkboxSavePassword.isChecked) { }
returnedHash = ByteArray(GocryptfsVolume.KeyLen) var currentPassword: CharArray? = null
} if (givenHash == null) {
var currentPassword: CharArray? = null currentPassword = CharArray(binding.editCurrentPassword.text.length)
if (givenHash == null) { binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
currentPassword = CharArray(binding.editCurrentPassword.text.length) }
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0) object : LoadingTask<Boolean>(this, themeValue, R.string.loading_msg_change_password) {
} override suspend fun doTask(): Boolean {
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) { val success = GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)
if (success) {
if (volumeDatabase.isHashSaved(volume.name)) { if (volumeDatabase.isHashSaved(volume.name)) {
volumeDatabase.removeHash(volume) volumeDatabase.removeHash(volume)
} }
stopTask {
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
// retry
it.savePasswordHash(volume, returnedHash)
}
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
finish()
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash, 0)
finish()
}
}
}
it.savePasswordHash(volume, returnedHash)
}
} else {
finish()
}
}
} else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.change_password_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
if (currentPassword != null) if (currentPassword != null)
Arrays.fill(currentPassword, 0.toChar()) Arrays.fill(currentPassword, 0.toChar())
Arrays.fill(newPassword, 0.toChar()) Arrays.fill(newPassword, 0.toChar())
if (givenHash != null) if (givenHash != null)
Arrays.fill(givenHash, 0) Arrays.fill(givenHash, 0)
return success
}
}.startTask(lifecycleScope) { success ->
if (success) {
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
// retry
it.savePasswordHash(volume, returnedHash)
}
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
finish()
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash, 0)
finish()
}
}
}
it.savePasswordHash(volume, returnedHash)
}
} else {
finish()
}
} else {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.change_password_failed)
.setPositiveButton(R.string.ok, null)
.show()
} }
} }
} }

View File

@ -1,39 +1,34 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.databinding.DialogLoadingBinding
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) { abstract class LoadingTask<T>(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) {
private val dialogLoadingView = activity.layoutInflater.inflate(R.layout.dialog_loading, null) private val dialogLoading = CustomAlertDialogBuilder(activity, themeValue)
private val dialogLoading: AlertDialog = CustomAlertDialogBuilder(activity, themeValue) .setView(
.setView(dialogLoadingView) DialogLoadingBinding.inflate(activity.layoutInflater).apply {
textMessage.text = activity.getString(loadingMessageResId)
}.root
)
.setTitle(R.string.loading) .setTitle(R.string.loading)
.setCancelable(false) .setCancelable(false)
.create() .create()
private var isStopped = false
init { abstract suspend fun doTask(): T
dialogLoadingView.findViewById<TextView>(R.id.text_message).text = activity.getString(loadingMessageResId)
startTask() fun startTask(scope: CoroutineScope, onDone: (T) -> Unit) {
}
abstract fun doTask(activity: AppCompatActivity)
private fun startTask() {
dialogLoading.show() dialogLoading.show()
Thread { scope.launch {
doTask(activity) val result = withContext(Dispatchers.IO) {
if (!isStopped){ doTask()
dialogLoading.dismiss()
}
}.start()
}
fun stopTask(onUiThread: (() -> Unit)?){
isStopped = true
dialogLoading.dismiss()
onUiThread?.let {
activity.runOnUiThread {
onUiThread()
} }
dialogLoading.dismiss()
onDone(result)
} }
} }
} }

View File

@ -16,9 +16,10 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.adapters.VolumeAdapter import sushi.hardcore.droidfs.adapters.VolumeAdapter
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
@ -34,7 +35,6 @@ import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.NoSuchElementException
class MainActivity : BaseActivity(), VolumeAdapter.Listener { class MainActivity : BaseActivity(), VolumeAdapter.Listener {
@ -389,20 +389,25 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) { private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) {
fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) { dstRootDirectory, failedItem -> lifecycleScope.launch {
runOnUiThread { val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
if (failedItem == null) { when {
dstRootDirectory?.let { result.taskResult.cancelled -> {
result.dstRootDirectory?.delete()
}
result.taskResult.failedItem == null -> {
result.dstRootDirectory?.let {
getResultVolume(it)?.let { volume -> getResultVolume(it)?.let { volume ->
volumeDatabase.saveVolume(volume) volumeDatabase.saveVolume(volume)
onVolumeAdded() onVolumeAdded()
Toast.makeText(this@MainActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
} }
} }
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show() }
} else { else -> {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.copy_failed, failedItem.name)) .setMessage(getString(R.string.copy_failed, result.taskResult.failedItem.name))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
@ -458,20 +463,21 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
volumeAdapter.refresh() volumeAdapter.refresh()
} }
override fun onPasswordHashDecrypted(hash: ByteArray) { override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) { object : LoadingTask<Int>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override fun doTask(activity: AppCompatActivity) { override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null) val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
Arrays.fill(hash, 0) Arrays.fill(hash, 0)
if (sessionId != -1) return sessionId
stopTask { startExplorer(sessionId, volume.shortName) } }
else }.startTask(lifecycleScope) { sessionId ->
stopTask { if (sessionId != -1) {
CustomAlertDialogBuilder(activity, themeValue) startExplorer(sessionId, volume.shortName)
.setTitle(R.string.open_volume_failed) } else {
.setMessage(R.string.open_failed_hash_msg) CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setPositiveButton(R.string.ok, null) .setTitle(R.string.open_volume_failed)
.show() .setMessage(R.string.open_failed_hash_msg)
} .setPositiveButton(R.string.ok, null)
.show()
} }
} }
} }
@ -544,53 +550,53 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) { private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
object : LoadingTask(this, themeValue, R.string.loading_msg_open) { var returnedHash: ByteArray? = null
override fun doTask(activity: AppCompatActivity) { if (savePasswordHash && usfFingerprint) {
var returnedHash: ByteArray? = null returnedHash = ByteArray(GocryptfsVolume.KeyLen)
if (savePasswordHash && usfFingerprint) { }
returnedHash = ByteArray(GocryptfsVolume.KeyLen) object : LoadingTask<Int>(this, themeValue, R.string.loading_msg_open) {
} override suspend fun doTask(): Int {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash) val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
Arrays.fill(password, 0.toChar()) Arrays.fill(password, 0.toChar())
if (sessionId != -1) { return sessionId
val fingerprintProtector = fingerprintProtector }
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23 }.startTask(lifecycleScope) { sessionId ->
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) if (sessionId != -1) {
stopTask { val fingerprintProtector = fingerprintProtector
fingerprintProtector.listener = object : FingerprintProtector.Listener { @SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
override fun onHashStorageReset() { if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
volumeAdapter.refresh() fingerprintProtector.listener = object : FingerprintProtector.Listener {
} override fun onHashStorageReset() {
override fun onPasswordHashDecrypted(hash: ByteArray) {} volumeAdapter.refresh()
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
volumeAdapter.onVolumeChanged(position)
startExplorer(sessionId, volume.shortName)
}
private var isClosed = false
override fun onFailed(pending: Boolean) {
if (!isClosed) {
GocryptfsVolume(this@MainActivity, sessionId).close()
isClosed = true
}
Arrays.fill(returnedHash, 0)
}
}
fingerprintProtector.savePasswordHash(volume, returnedHash)
} }
else override fun onPasswordHashDecrypted(hash: ByteArray) {}
stopTask { startExplorer(sessionId, volume.shortName) } override fun onPasswordHashSaved() {
} else Arrays.fill(returnedHash, 0)
stopTask { volumeAdapter.onVolumeChanged(position)
CustomAlertDialogBuilder(activity, themeValue) startExplorer(sessionId, volume.shortName)
.setTitle(R.string.open_volume_failed) }
.setMessage(R.string.open_volume_failed_msg) private var isClosed = false
.setPositiveButton(R.string.ok, null) override fun onFailed(pending: Boolean) {
.setOnDismissListener { if (!isClosed) {
askForPassword(volume, position) GocryptfsVolume(this@MainActivity, sessionId).close()
isClosed = true
} }
.show() Arrays.fill(returnedHash, 0)
}
} }
fingerprintProtector.savePasswordHash(volume, returnedHash)
} else {
startExplorer(sessionId, volume.shortName)
}
} else {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok, null)
.setOnDismissListener {
askForPassword(volume, position)
}
.show()
} }
} }
} }

View File

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.*
import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
@ -112,33 +113,40 @@ class ExplorerElementAdapter(
} }
class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) { class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
var displayThumbnail = true private var target: DrawableImageViewTarget? = null
var target: DrawableImageViewTarget? = null private var job: Job? = null
private val scope = CoroutineScope(Dispatchers.IO)
private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) { private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) {
adapter.gocryptfsVolume?.let { volume -> adapter.gocryptfsVolume?.let { volume ->
displayThumbnail = true job = scope.launch {
Thread {
volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let { volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let {
if (displayThumbnail) { if (isActive) {
adapter.activity.runOnUiThread { withContext(Dispatchers.Main) {
if (displayThumbnail && !adapter.activity.isFinishing) { if (isActive && !adapter.activity.isFinishing) {
target = Glide.with(adapter.activity).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(icon) { target = Glide.with(adapter.activity).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(icon) {
override fun onResourceReady( override fun onResourceReady(
resource: Drawable, resource: Drawable,
transition: Transition<in Drawable>? transition: Transition<in Drawable>?
) { ) {
target = null
val bitmap = resource.toBitmap() val bitmap = resource.toBitmap()
adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true)) adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true))
super.onResourceReady(resource, transition) super.onResourceReady(resource, transition)
target = null
} }
}) })
} }
} }
} }
} }
}.start() }
}
}
fun cancelThumbnailLoading(adapter: ExplorerElementAdapter) {
job?.cancel()
target?.let {
Glide.with(adapter.activity).clear(it)
} }
} }
@ -199,11 +207,7 @@ class ExplorerElementAdapter(
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder is FileViewHolder) { if (holder is FileViewHolder) {
//cancel pending thumbnail display holder.cancelThumbnailLoading(this)
holder.displayThumbnail = false
holder.target?.let {
Glide.with(activity).clear(it)
}
} }
} }

View File

@ -12,6 +12,7 @@ import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import sushi.hardcore.droidfs.* import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -123,9 +124,14 @@ class CreateVolumeFragment: Fragment() {
if (!password.contentEquals(passwordConfirm)) { if (!password.contentEquals(passwordConfirm)) {
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
Arrays.fill(password, 0.toChar()) Arrays.fill(password, 0.toChar())
Arrays.fill(passwordConfirm, 0.toChar())
} else { } else {
object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) { Arrays.fill(passwordConfirm, 0.toChar())
override fun doTask(activity: AppCompatActivity) { var returnedHash: ByteArray? = null
if (binding.checkboxSavePassword.isChecked)
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
object: LoadingTask<Volume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
override suspend fun doTask(): Volume? {
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) { val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
0 -> 0 0 -> 0
1 -> 1 1 -> 1
@ -134,10 +140,16 @@ class CreateVolumeFragment: Fragment() {
val volumeFile = File(volumePath) val volumeFile = File(volumePath)
if (!volumeFile.exists()) if (!volumeFile.exists())
volumeFile.mkdirs() volumeFile.mkdirs()
var returnedHash: ByteArray? = null val volume = if (GocryptfsVolume.createVolume(
if (binding.checkboxSavePassword.isChecked) volumePath,
returnedHash = ByteArray(GocryptfsVolume.KeyLen) password,
if (GocryptfsVolume.createVolume(volumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.CREATOR, returnedHash)) { false,
xchacha,
GocryptfsVolume.ScryptDefaultLogN,
ConstValues.CREATOR,
returnedHash
)
) {
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
val volume = Volume(volumeName, isHiddenVolume) val volume = Volume(volumeName, isHiddenVolume)
volumeDatabase.apply { volumeDatabase.apply {
@ -145,46 +157,48 @@ class CreateVolumeFragment: Fragment() {
removeVolume(volumeName) removeVolume(volumeName)
saveVolume(volume) saveVolume(volume)
} }
stopTask { volume
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
hashStorageReset = true
// retry
it.savePasswordHash(volume, returnedHash)
}
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
onVolumeCreated()
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash, 0)
onVolumeCreated()
}
}
}
it.savePasswordHash(volume, returnedHash)
}
} else onVolumeCreated()
}
} else { } else {
stopTask { null
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
Arrays.fill(password, 0.toChar()) Arrays.fill(password, 0.toChar())
return volume
}
}.startTask(lifecycleScope) { volume ->
if (volume == null) {
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
hashStorageReset = true
// retry
it.savePasswordHash(volume, returnedHash)
}
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
onVolumeCreated()
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash, 0)
onVolumeCreated()
}
}
}
it.savePasswordHash(volume, returnedHash)
}
} else onVolumeCreated()
} }
} }
} }
Arrays.fill(passwordConfirm, 0.toChar())
} }
private fun onVolumeCreated() { private fun onVolumeCreated() {

View File

@ -5,6 +5,10 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.LoadingTask import sushi.hardcore.droidfs.LoadingTask
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
@ -13,7 +17,7 @@ import java.io.File
object ExternalProvider { object ExternalProvider {
private const val content_type_all = "*/*" private const val content_type_all = "*/*"
private var storedFiles: MutableList<Uri> = ArrayList() private var storedFiles = HashSet<Uri>()
private fun getContentType(filename: String, previous_content_type: String?): String { private fun getContentType(filename: String, previous_content_type: String?): String {
if (content_type_all != previous_content_type) { if (content_type_all != previous_content_type) {
var contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(filename).extension) var contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(filename).extension)
@ -42,26 +46,23 @@ object ExternalProvider {
} }
fun share(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) { fun share(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) {
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) { var contentType: String? = null
override fun doTask(activity: AppCompatActivity) { val uris = ArrayList<Uri>(file_paths.size)
var contentType: String? = null object : LoadingTask<String?>(activity, themeValue, R.string.loading_msg_export) {
val uris = ArrayList<Uri>() override suspend fun doTask(): String? {
for (path in file_paths) { for (path in file_paths) {
val result = exportFile(activity, gocryptfsVolume, path, contentType) val result = exportFile(activity, gocryptfsVolume, path, contentType)
contentType = if (result.first != null) { contentType = if (result.first != null) {
uris.add(result.first!!) uris.add(result.first!!)
result.second result.second
} else { } else {
stopTask { return path
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(activity.getString(R.string.export_failed, path))
.setPositiveButton(R.string.ok, null)
.show()
}
return
} }
} }
return null
}
}.startTask(activity.lifecycleScope) { failedItem ->
if (failedItem == null) {
val shareIntent = Intent() val shareIntent = Intent()
shareIntent.type = contentType shareIntent.type = contentType
if (uris.size == 1) { if (uris.size == 1) {
@ -71,45 +72,51 @@ object ExternalProvider {
shareIntent.action = Intent.ACTION_SEND_MULTIPLE shareIntent.action = Intent.ACTION_SEND_MULTIPLE
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
} }
stopTask { activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_chooser)))
activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_chooser))) } else {
} CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(activity.getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
} }
} }
} }
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) { fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) { object : LoadingTask<Intent?>(activity, themeValue, R.string.loading_msg_export) {
override fun doTask(activity: AppCompatActivity) { override suspend fun doTask(): Intent? {
val result = exportFile(activity, gocryptfsVolume, file_path, null) val result = exportFile(activity, gocryptfsVolume, file_path, null)
if (result.first != null) { return if (result.first != null) {
val openIntent = Intent(Intent.ACTION_VIEW) Intent(Intent.ACTION_VIEW).apply {
openIntent.setDataAndType(result.first, result.second) setDataAndType(result.first, result.second)
stopTask { activity.startActivity(openIntent) }
} else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(activity.getString(R.string.export_failed, file_path))
.setPositiveButton(R.string.ok, null)
.show()
} }
} else {
null
} }
} }
}.startTask(activity.lifecycleScope) { openIntent ->
if (openIntent == null) {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(activity.getString(R.string.export_failed, file_path))
.setPositiveButton(R.string.ok, null)
.show()
} else {
activity.startActivity(openIntent)
}
} }
} }
fun removeFiles(context: Context) { fun removeFilesAsync(context: Context) = GlobalScope.launch(Dispatchers.IO) {
Thread{ val success = HashSet<Uri>(storedFiles.size)
val success = ArrayList<Uri>() for (uri in storedFiles) {
for (uri in storedFiles){ if (context.contentResolver.delete(uri, null, null) == 1) {
if (context.contentResolver.delete(uri, null, null) == 1){ success.add(uri)
success.add(uri)
}
} }
for (uri in success){ }
storedFiles.remove(uri) for (uri in success) {
} storedFiles.remove(uri)
}.start() }
} }
} }

View File

@ -14,13 +14,16 @@ import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.ConstValues.isAudio import sushi.hardcore.droidfs.ConstValues.isAudio
@ -312,21 +315,21 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile }) displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile })
displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory }) displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory })
if (mapFolders) { if (mapFolders) {
Thread { lifecycleScope.launch {
var totalSize: Long = 0 var totalSize: Long = 0
synchronized(this) { withContext(Dispatchers.IO) {
for (element in explorerElements) { synchronized(this@BaseExplorerActivity) {
if (element.isDirectory) { for (element in explorerElements) {
recursiveSetSize(element) if (element.isDirectory) {
recursiveSetSize(element)
}
totalSize += element.size
} }
totalSize += element.size
} }
} }
runOnUiThread { displayExplorerElements(totalSize)
displayExplorerElements(totalSize) onDisplayed?.invoke()
onDisplayed?.invoke() }
}
}.start()
} else { } else {
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size }) displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size })
onDisplayed?.invoke() onDisplayed?.invoke()
@ -455,9 +458,12 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (items.size > 0) { if (items.size > 0) {
checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
checkedItems?.let { checkedItems?.let {
fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris){ failedItem -> lifecycleScope.launch {
runOnUiThread { val taskResult = fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris)
callback(failedItem) if (taskResult.cancelled) {
setCurrentPath(currentDirectoryPath)
} else {
callback(taskResult.failedItem)
} }
} }
} }
@ -465,20 +471,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
} }
fun importDirectory(sourceUri: Uri, callback: (String?, List<Uri>, DocumentFile) -> Unit) {
val tree = DocumentFile.fromTreeUri(this, sourceUri)!! //non-null after Lollipop
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let {
fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) { failedItem, uris ->
runOnUiThread {
callback(failedItem, uris, tree)
}
}
}
}
}
protected fun rename(old_name: String, new_name: String){ protected fun rename(old_name: String, new_name: String){
if (new_name.isEmpty()) { if (new_name.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
@ -627,7 +619,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
finish() finish()
} else { } else {
isStartingActivity = false isStartingActivity = false
ExternalProvider.removeFiles(this) ExternalProvider.removeFilesAsync(this)
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
} }

View File

@ -8,6 +8,8 @@ import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.CameraActivity import sushi.hardcore.droidfs.CameraActivity
import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.MainActivity import sushi.hardcore.droidfs.MainActivity
@ -67,19 +69,18 @@ class ExplorerActivity : BaseExplorerActivity() {
if (items == null) { if (items == null) {
remoteGocryptfsVolume.close() remoteGocryptfsVolume.close()
} else { } else {
fileOperationService.copyElements(items, remoteGocryptfsVolume){ failedItem -> lifecycleScope.launch {
runOnUiThread { val failedItem = fileOperationService.copyElements(items, remoteGocryptfsVolume)
if (failedItem == null){ if (failedItem == null) {
Toast.makeText(this, R.string.success_import, Toast.LENGTH_SHORT).show() Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show()
} else { } else {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem)) .setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
}
setCurrentPath(currentDirectoryPath)
} }
setCurrentPath(currentDirectoryPath)
remoteGocryptfsVolume.close() remoteGocryptfsVolume.close()
} }
} }
@ -99,14 +100,15 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
if (uri != null) { if (uri != null) {
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem -> lifecycleScope.launch {
runOnUiThread { val result = fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] })
if (failedItem == null){ if (!result.cancelled) {
Toast.makeText(this, R.string.success_export, Toast.LENGTH_SHORT).show() if (result.failedItem == null) {
Toast.makeText(this@ExplorerActivity, R.string.success_export, Toast.LENGTH_SHORT).show()
} else { } else {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, failedItem)) .setMessage(getString(R.string.export_failed, result.failedItem))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
@ -117,7 +119,20 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri -> private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
rootUri?.let { rootUri?.let {
importDirectory(it, ::onImportComplete) val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let {
lifecycleScope.launch {
val result = fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree)
if (result.taskResult.cancelled) {
setCurrentPath(currentDirectoryPath)
} else {
onImportComplete(result.taskResult.failedItem, result.uris, tree)
}
}
}
}
} }
} }
@ -130,17 +145,16 @@ class ExplorerActivity : BaseExplorerActivity() {
${getString(R.string.ask_for_wipe)} ${getString(R.string.ask_for_wipe)}
""".trimIndent()) """.trimIndent())
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
fileOperationService.wipeUris(urisToWipe, rootFile) { errorMsg -> lifecycleScope.launch {
runOnUiThread { val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile)
if (errorMsg == null){ if (errorMsg == null) {
Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).show() Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
} else { } else {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.wipe_failed, errorMsg)) .setMessage(getString(R.string.wipe_failed, errorMsg))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
}
} }
} }
} }
@ -310,16 +324,17 @@ class ExplorerActivity : BaseExplorerActivity() {
if (currentItemAction == ItemsActions.COPY){ if (currentItemAction == ItemsActions.COPY){
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
items?.let { items?.let {
fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem -> lifecycleScope.launch {
runOnUiThread { val failedItem = fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>)
if (failedItem == null){ if (!isFinishing) {
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show() if (failedItem == null) {
Toast.makeText(this@ExplorerActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
} else { } else {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.copy_failed, failedItem)) .setMessage(getString(R.string.copy_failed, failedItem))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
@ -332,19 +347,18 @@ class ExplorerActivity : BaseExplorerActivity() {
mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath) mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath)
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
items?.let { items?.let {
fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem -> lifecycleScope.launch {
runOnUiThread { val failedItem = fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>)
if (failedItem == null){ if (failedItem == null) {
Toast.makeText(this, R.string.move_success, Toast.LENGTH_SHORT).show() Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show()
} else { } else {
CustomAlertDialogBuilder(this, themeValue) CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem)) .setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
}
setCurrentPath(currentDirectoryPath)
} }
setCurrentPath(currentDirectoryPath)
} }
} }
cancelItemAction() cancelItemAction()

View File

@ -13,6 +13,7 @@ import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.*
import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
@ -30,7 +31,7 @@ class FileOperationService : Service() {
private val binder = LocalBinder() private val binder = LocalBinder()
private lateinit var gocryptfsVolume: GocryptfsVolume private lateinit var gocryptfsVolume: GocryptfsVolume
private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationManager: NotificationManagerCompat
private var notifications = HashMap<Int, Boolean>() private val tasks = HashMap<Int, Job>()
private var lastNotificationId = 0 private var lastNotificationId = 0
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@ -88,7 +89,6 @@ class FileOperationService : Service() {
.setContentText(getString(R.string.discovering_files)) .setContentText(getString(R.string.discovering_files))
.setProgress(0, 0, true) .setProgress(0, 0, true)
} }
notifications[lastNotificationId] = false
notificationManager.notify(lastNotificationId, notificationBuilder.build()) notificationManager.notify(lastNotificationId, notificationBuilder.build())
return FileOperationNotification(notificationBuilder, lastNotificationId) return FileOperationNotification(notificationBuilder, lastNotificationId)
} }
@ -105,7 +105,20 @@ class FileOperationService : Service() {
} }
fun cancelOperation(notificationId: Int){ fun cancelOperation(notificationId: Int){
notifications[notificationId] = true tasks[notificationId]?.cancel()
}
open class TaskResult<T>(val cancelled: Boolean, val failedItem: T?)
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<T> {
tasks[notification.notificationId] = task
return try {
TaskResult(false, task.await())
} catch (e: CancellationException) {
TaskResult(true, null)
} finally {
cancelNotification(notification)
}
} }
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean { private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
@ -137,110 +150,105 @@ class FileOperationService : Service() {
return success return success
} }
fun copyElements(items: ArrayList<OperationFile>, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){ suspend fun copyElements(
Thread { items: ArrayList<OperationFile>,
val notification = showNotification(R.string.file_op_copy_msg, items.size) remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
): String? = coroutineScope {
val notification = showNotification(R.string.file_op_copy_msg, items.size)
val task = async {
var failedItem: String? = null var failedItem: String? = null
for (i in 0 until items.size){ for (i in 0 until items.size) {
if (notifications[notification.notificationId]!!){ withContext(Dispatchers.IO) {
cancelNotification(notification) if (items[i].explorerElement.isDirectory) {
return@Thread if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
} if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
if (items[i].explorerElement.isDirectory){ failedItem = items[i].explorerElement.fullPath
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) { }
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) { }
} else {
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
failedItem = items[i].explorerElement.fullPath failedItem = items[i].explorerElement.fullPath
} }
} }
} else {
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){
failedItem = items[i].explorerElement.fullPath
}
} }
if (failedItem == null){ if (failedItem == null) {
updateNotificationProgress(notification, i, items.size) updateNotificationProgress(notification, i+1, items.size)
} else { } else {
break break
} }
} }
cancelNotification(notification) failedItem
callback(failedItem) }
}.start() // treat cancellation as success
waitForTask(notification, task).failedItem
} }
fun moveElements(items: ArrayList<OperationFile>, callback: (String?) -> Unit){ suspend fun moveElements(items: ArrayList<OperationFile>): String? = coroutineScope {
Thread { val notification = showNotification(R.string.file_op_move_msg, items.size)
val notification = showNotification(R.string.file_op_move_msg, items.size) val task = async {
val mergedFolders = ArrayList<String>()
var failedItem: String? = null var failedItem: String? = null
for (i in 0 until items.size){ withContext(Dispatchers.IO) {
if (notifications[notification.notificationId]!!){ val mergedFolders = ArrayList<String>()
cancelNotification(notification) for (i in 0 until items.size) {
return@Thread if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)) { //folder will be merged
} mergedFolders.add(items[i].explorerElement.fullPath)
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)){ //folder will be merged
mergedFolders.add(items[i].explorerElement.fullPath)
} else {
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)){
failedItem = items[i].explorerElement.fullPath
break
} else { } else {
updateNotificationProgress(notification, i, items.size) if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)) {
failedItem = items[i].explorerElement.fullPath
break
} else {
updateNotificationProgress(notification, i+1, items.size)
}
}
}
if (failedItem == null) {
for (i in 0 until mergedFolders.size) {
if (!gocryptfsVolume.rmdir(mergedFolders[i])) {
failedItem = mergedFolders[i]
break
} else {
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
}
} }
} }
} }
if (failedItem == null){ failedItem
for (i in 0 until mergedFolders.size) { }
if (notifications[notification.notificationId]!!){ // treat cancellation as success
cancelNotification(notification) waitForTask(notification, task).failedItem
return@Thread
}
if (!gocryptfsVolume.rmdir(mergedFolders[i])){
failedItem = mergedFolders[i]
break
} else {
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
}
}
}
cancelNotification(notification)
callback(failedItem)
}.start()
} }
private fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, reuseNotification: FileOperationNotification? = null, callback: (String?) -> Unit){ private suspend fun importFilesFromUris(
val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size) dstPaths: List<String>,
uris: List<Uri>,
notification: FileOperationNotification,
): String? {
var failedIndex = -1 var failedIndex = -1
for (i in dstPaths.indices) { for (i in dstPaths.indices) {
if (notifications[notification.notificationId]!!){ withContext(Dispatchers.IO) {
cancelNotification(notification) try {
return if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
} failedIndex = i
try { }
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) { } catch (e: FileNotFoundException) {
failedIndex = i failedIndex = i
} }
} catch (e: FileNotFoundException){
failedIndex = i
} }
if (failedIndex == -1) { if (failedIndex == -1) {
updateNotificationProgress(notification, i, dstPaths.size) updateNotificationProgress(notification, i+1, dstPaths.size)
} else { } else {
cancelNotification(notification) return uris[failedIndex].toString()
callback(uris[failedIndex].toString())
break
} }
} }
if (failedIndex == -1){ return null
cancelNotification(notification)
callback(null)
}
} }
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) { suspend fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>): TaskResult<String?> = coroutineScope {
Thread { val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
importFilesFromUris(dstPaths, uris, null, callback) val task = async {
}.start() importFilesFromUris(dstPaths, uris, notification)
}
waitForTask(notification, task)
} }
/** /**
@ -256,18 +264,17 @@ class FileOperationService : Service() {
dstFiles: ArrayList<String>, dstFiles: ArrayList<String>,
srcUris: ArrayList<Uri>, srcUris: ArrayList<Uri>,
dstDirs: ArrayList<String>, dstDirs: ArrayList<String>,
notification: FileOperationNotification scope: CoroutineScope,
): Boolean { ): Boolean {
dstDirs.add(rootDstPath) dstDirs.add(rootDstPath)
for (child in rootSrcDir.listFiles()) { for (child in rootSrcDir.listFiles()) {
if (notifications[notification.notificationId]!!) { if (!scope.isActive) {
cancelNotification(notification)
return false return false
} }
child.name?.let { name -> child.name?.let { name ->
val subPath = PathUtils.pathJoin(rootDstPath, name) val subPath = PathUtils.pathJoin(rootDstPath, name)
if (child.isDirectory) { if (child.isDirectory) {
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) { if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) {
return false return false
} }
} }
@ -280,48 +287,50 @@ class FileOperationService : Service() {
return true return true
} }
fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List<Uri>) -> Unit) { class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>)
Thread {
val notification = showNotification(R.string.file_op_import_msg, null)
suspend fun importDirectory(
rootDstPath: String,
rootSrcDir: DocumentFile,
): ImportDirectoryResult = coroutineScope {
val notification = showNotification(R.string.file_op_import_msg, null)
val srcUris = arrayListOf<Uri>()
val task = async {
var failedItem: String? = null
val dstFiles = arrayListOf<String>() val dstFiles = arrayListOf<String>()
val srcUris = arrayListOf<Uri>()
val dstDirs = arrayListOf<String>() val dstDirs = arrayListOf<String>()
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
return@Thread
}
// create destination folders so the new files can use them withContext(Dispatchers.IO) {
for (dir in dstDirs) { if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
if (notifications[notification.notificationId]!!) { return@withContext
cancelNotification(notification)
return@Thread
} }
if (!gocryptfsVolume.mkdir(dir)) {
cancelNotification(notification) // create destination folders so the new files can use them
callback(dir, srcUris) for (dir in dstDirs) {
break if (!gocryptfsVolume.mkdir(dir)) {
failedItem = dir
break
}
} }
} }
if (failedItem == null) {
importFilesFromUris(dstFiles, srcUris, notification) { failedItem -> failedItem = importFilesFromUris(dstFiles, srcUris, notification)
callback(failedItem, srcUris)
} }
}.start() failedItem
}
ImportDirectoryResult(waitForTask(notification, task), srcUris)
} }
fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null, callback: (String?) -> Unit){ suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): String? = coroutineScope {
Thread { val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
val notification = showNotification(R.string.file_op_wiping_msg, uris.size) val task = async {
var errorMsg: String? = null var errorMsg: String? = null
for (i in uris.indices) { for (i in uris.indices) {
if (notifications[notification.notificationId]!!){ withContext(Dispatchers.IO) {
cancelNotification(notification) errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
return@Thread
} }
errorMsg = Wiper.wipe(this, uris[i])
if (errorMsg == null) { if (errorMsg == null) {
updateNotificationProgress(notification, i, uris.size) updateNotificationProgress(notification, i+1, uris.size)
} else { } else {
break break
} }
@ -329,9 +338,10 @@ class FileOperationService : Service() {
if (errorMsg == null) { if (errorMsg == null) {
rootFile?.delete() rootFile?.delete()
} }
cancelNotification(notification) errorMsg
callback(errorMsg) }
}.start() // treat cancellation as success
waitForTask(notification, task).failedItem
} }
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean { private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
@ -345,13 +355,20 @@ class FileOperationService : Service() {
} }
} }
private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? { private fun recursiveExportDirectory(
plain_directory_path: String,
treeDocumentFile: DocumentFile,
scope: CoroutineScope
): String? {
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree -> treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
val explorerElements = gocryptfsVolume.listDir(plain_directory_path) val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
for (e in explorerElements) { for (e in explorerElements) {
if (!scope.isActive) {
return null
}
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) { if (e.isDirectory) {
val failedItem = recursiveExportDirectory(fullPath, childTree) val failedItem = recursiveExportDirectory(fullPath, childTree, scope)
failedItem?.let { return it } failedItem?.let { return it }
} else { } else {
if (!exportFileInto(fullPath, childTree)){ if (!exportFileInto(fullPath, childTree)){
@ -364,40 +381,40 @@ class FileOperationService : Service() {
return treeDocumentFile.name return treeDocumentFile.name
} }
fun exportFiles(uri: Uri, items: List<ExplorerElement>, callback: (String?) -> Unit){ suspend fun exportFiles(uri: Uri, items: List<ExplorerElement>): TaskResult<String?> = coroutineScope {
Thread { val notification = showNotification(R.string.file_op_export_msg, items.size)
val task = async {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile -> val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
val notification = showNotification(R.string.file_op_export_msg, items.size) var failedItem: String? = null
var failedItem: String? = null for (i in items.indices) {
for (i in items.indices) { withContext(Dispatchers.IO) {
if (notifications[notification.notificationId]!!){
cancelNotification(notification)
return@Thread
}
failedItem = if (items[i].isDirectory) { failedItem = if (items[i].isDirectory) {
recursiveExportDirectory(items[i].fullPath, treeDocumentFile) recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this)
} else { } else {
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
} }
if (failedItem == null) {
updateNotificationProgress(notification, i, items.size)
} else {
break
}
} }
cancelNotification(notification) if (failedItem == null) {
callback(failedItem) updateNotificationProgress(notification, i+1, items.size)
} else {
break
}
} }
}.start() failedItem
}
waitForTask(notification, task)
} }
private fun recursiveCountChildElements(rootDirectory: DocumentFile): Int { private fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
if (!scope.isActive) {
return 0
}
val children = rootDirectory.listFiles() val children = rootDirectory.listFiles()
var count = children.size var count = children.size
for (child in children) { for (child in children) {
if (child.isDirectory) { if (child.isDirectory) {
count += recursiveCountChildElements(child) count += recursiveCountChildElements(child, scope)
} }
} }
return count return count
@ -411,12 +428,13 @@ class FileOperationService : Service() {
dstRootDirectory: ObjRef<DocumentFile?>?, dstRootDirectory: ObjRef<DocumentFile?>?,
notification: FileOperationNotification, notification: FileOperationNotification,
total: Int, total: Int,
scope: CoroutineScope,
progress: ObjRef<Int> = ObjRef(0) progress: ObjRef<Int> = ObjRef(0)
): DocumentFile? { ): DocumentFile? {
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
dstRootDirectory?.let { it.value = dstDir }
for (child in src.listFiles()) { for (child in src.listFiles()) {
if (notifications[notification.notificationId]!!) { if (!scope.isActive) {
cancelNotification(notification)
return null return null
} }
if (child.isFile) { if (child.isFile) {
@ -429,24 +447,29 @@ class FileOperationService : Service() {
inputStream.close() inputStream.close()
if (written != child.length()) return child if (written != child.length()) return child
} else { } else {
recursiveCopyVolume(child, dstDir, null, notification, total, progress)?.let { return it } recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
} }
progress.value++ progress.value++
updateNotificationProgress(notification, progress.value, total) updateNotificationProgress(notification, progress.value, total)
} }
dstRootDirectory?.let { it.value = dstDir }
return null return null
} }
fun copyVolume(src: DocumentFile, dst: DocumentFile, callback: (DocumentFile?, DocumentFile?) -> Unit) { class CopyVolumeResult(val taskResult: TaskResult<DocumentFile?>, val dstRootDirectory: DocumentFile?)
Thread {
val notification = showNotification(R.string.copy_volume_notification, null) suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope {
val total = recursiveCountChildElements(src) val notification = showNotification(R.string.copy_volume_notification, null)
updateNotificationProgress(notification, 0, total) val dstRootDirectory = ObjRef<DocumentFile?>(null)
val dstRootDirectory = ObjRef<DocumentFile?>(null) val task = async(Dispatchers.IO) {
val failedItem = recursiveCopyVolume(src, dst, dstRootDirectory, notification, total) val total = recursiveCountChildElements(src, this)
cancelNotification(notification) if (isActive) {
callback(dstRootDirectory.value, failedItem) updateNotificationProgress(notification, 0, total)
}.start() recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
} else {
null
}
}
// treat cancellation as success
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
} }
} }