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,8 +105,6 @@ 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) {
override fun doTask(activity: AppCompatActivity) {
var returnedHash: ByteArray? = null var returnedHash: ByteArray? = null
if (binding.checkboxSavePassword.isChecked) { if (binding.checkboxSavePassword.isChecked) {
returnedHash = ByteArray(GocryptfsVolume.KeyLen) returnedHash = ByteArray(GocryptfsVolume.KeyLen)
@ -116,11 +114,23 @@ class ChangePasswordActivity: BaseActivity() {
currentPassword = CharArray(binding.editCurrentPassword.text.length) currentPassword = CharArray(binding.editCurrentPassword.text.length)
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0) binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
} }
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) { object : LoadingTask<Boolean>(this, themeValue, R.string.loading_msg_change_password) {
override suspend fun doTask(): Boolean {
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 { }
if (currentPassword != null)
Arrays.fill(currentPassword, 0.toChar())
Arrays.fill(newPassword, 0.toChar())
if (givenHash != null)
Arrays.fill(givenHash, 0)
return success
}
}.startTask(lifecycleScope) { success ->
if (success) {
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden @SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) { if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let { fingerprintProtector!!.let {
@ -146,22 +156,13 @@ class ChangePasswordActivity: BaseActivity() {
} else { } else {
finish() finish()
} }
}
} else { } else {
stopTask { CustomAlertDialogBuilder(this, themeValue)
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.change_password_failed) .setMessage(R.string.change_password_failed)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
} }
if (currentPassword != null)
Arrays.fill(currentPassword, 0.toChar())
Arrays.fill(newPassword, 0.toChar())
if (givenHash != null)
Arrays.fill(givenHash, 0)
}
}
} }
} }

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() dialogLoading.dismiss()
} onDone(result)
}.start()
}
fun stopTask(onUiThread: (() -> Unit)?){
isStopped = true
dialogLoading.dismiss()
onUiThread?.let {
activity.runOnUiThread {
onUiThread()
}
} }
} }
} }

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,15 +463,17 @@ 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)
} else {
CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setTitle(R.string.open_volume_failed) .setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg) .setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -474,7 +481,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
} }
}
override fun onPasswordHashSaved() {} override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) { override fun onFailed(pending: Boolean) {
if (!pending) { if (!pending) {
@ -544,19 +550,21 @@ 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) {
override fun doTask(activity: AppCompatActivity) {
var returnedHash: ByteArray? = null var returnedHash: ByteArray? = null
if (savePasswordHash && usfFingerprint) { if (savePasswordHash && usfFingerprint) {
returnedHash = ByteArray(GocryptfsVolume.KeyLen) 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())
return sessionId
}
}.startTask(lifecycleScope) { sessionId ->
if (sessionId != -1) { if (sessionId != -1) {
val fingerprintProtector = fingerprintProtector val fingerprintProtector = fingerprintProtector
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23 @SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
stopTask {
fingerprintProtector.listener = object : FingerprintProtector.Listener { fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() { override fun onHashStorageReset() {
volumeAdapter.refresh() volumeAdapter.refresh()
@ -577,12 +585,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
fingerprintProtector.savePasswordHash(volume, returnedHash) fingerprintProtector.savePasswordHash(volume, returnedHash)
} else {
startExplorer(sessionId, volume.shortName)
} }
else } else {
stopTask { startExplorer(sessionId, volume.shortName) } CustomAlertDialogBuilder(this, themeValue)
} else
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed) .setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg) .setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -593,7 +600,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
} }
}
private fun startExplorer(sessionId: Int, volumeShortName: String) { private fun startExplorer(sessionId: Int, volumeShortName: String) {
var explorerIntent: Intent? = null var explorerIntent: Intent? = null

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,7 +157,21 @@ class CreateVolumeFragment: Fragment() {
removeVolume(volumeName) removeVolume(volumeName)
saveVolume(volume) saveVolume(volume)
} }
stopTask { volume
} else {
null
}
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 @SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) { if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let { fingerprintProtector!!.let {
@ -171,20 +197,8 @@ class CreateVolumeFragment: Fragment() {
} }
} else onVolumeCreated() } else onVolumeCreated()
} }
} else {
stopTask {
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(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) {
override fun doTask(activity: AppCompatActivity) {
var contentType: String? = null var contentType: String? = null
val uris = ArrayList<Uri>() val uris = ArrayList<Uri>(file_paths.size)
object : LoadingTask<String?>(activity, themeValue, R.string.loading_msg_export) {
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)))
}
}
}
}
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) {
override fun doTask(activity: AppCompatActivity) {
val result = exportFile(activity, gocryptfsVolume, file_path, null)
if (result.first != null) {
val openIntent = Intent(Intent.ACTION_VIEW)
openIntent.setDataAndType(result.first, result.second)
stopTask { activity.startActivity(openIntent) }
} else { } else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue) CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(activity.getString(R.string.export_failed, file_path)) .setMessage(activity.getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
} }
} }
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
object : LoadingTask<Intent?>(activity, themeValue, R.string.loading_msg_export) {
override suspend fun doTask(): Intent? {
val result = exportFile(activity, gocryptfsVolume, file_path, null)
return if (result.first != null) {
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(result.first, result.second)
}
} 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){ for (uri in success) {
storedFiles.remove(uri) 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,9 +315,10 @@ 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) {
synchronized(this@BaseExplorerActivity) {
for (element in explorerElements) { for (element in explorerElements) {
if (element.isDirectory) { if (element.isDirectory) {
recursiveSetSize(element) recursiveSetSize(element)
@ -322,11 +326,10 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
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,12 +145,12 @@ 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)
@ -143,7 +158,6 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
} }
} }
}
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
.show() .show()
} else { } else {
@ -310,12 +324,13 @@ 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)
@ -332,12 +347,12 @@ 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)
@ -346,7 +361,6 @@ class ExplorerActivity : BaseExplorerActivity() {
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
} }
}
cancelItemAction() cancelItemAction()
invalidateOptionsMenu() invalidateOptionsMenu()
} }

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,65 +150,60 @@ 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>,
remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
): String? = coroutineScope {
val notification = showNotification(R.string.file_op_copy_msg, items.size) 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 (items[i].explorerElement.isDirectory){
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) { if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) { if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
failedItem = items[i].explorerElement.fullPath failedItem = items[i].explorerElement.fullPath
} }
} }
} else { } else {
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){ if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
failedItem = items[i].explorerElement.fullPath failedItem = items[i].explorerElement.fullPath
} }
} }
if (failedItem == null){ }
updateNotificationProgress(notification, i, items.size) if (failedItem == null) {
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 mergedFolders = ArrayList<String>() val task = async {
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
}
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)){ //folder will be merged
mergedFolders.add(items[i].explorerElement.fullPath) mergedFolders.add(items[i].explorerElement.fullPath)
} else { } else {
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)){ if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)) {
failedItem = items[i].explorerElement.fullPath failedItem = items[i].explorerElement.fullPath
break break
} else { } else {
updateNotificationProgress(notification, i, items.size) updateNotificationProgress(notification, i+1, items.size)
} }
} }
} }
if (failedItem == null){ if (failedItem == null) {
for (i in 0 until mergedFolders.size) { for (i in 0 until mergedFolders.size) {
if (notifications[notification.notificationId]!!){ if (!gocryptfsVolume.rmdir(mergedFolders[i])) {
cancelNotification(notification)
return@Thread
}
if (!gocryptfsVolume.rmdir(mergedFolders[i])){
failedItem = mergedFolders[i] failedItem = mergedFolders[i]
break break
} else { } else {
@ -203,44 +211,44 @@ class FileOperationService : Service() {
} }
} }
} }
cancelNotification(notification) }
callback(failedItem) failedItem
}.start() }
// treat cancellation as success
waitForTask(notification, task).failedItem
} }
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)
return
}
try { try {
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) { if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
failedIndex = i failedIndex = i
} }
} catch (e: FileNotFoundException){ } catch (e: FileNotFoundException) {
failedIndex = i 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)
val dstFiles = arrayListOf<String>() suspend fun importDirectory(
rootDstPath: String,
rootSrcDir: DocumentFile,
): ImportDirectoryResult = coroutineScope {
val notification = showNotification(R.string.file_op_import_msg, null)
val srcUris = arrayListOf<Uri>() val srcUris = arrayListOf<Uri>()
val task = async {
var failedItem: String? = null
val dstFiles = arrayListOf<String>()
val dstDirs = arrayListOf<String>() val dstDirs = arrayListOf<String>()
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
return@Thread withContext(Dispatchers.IO) {
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
return@withContext
} }
// create destination folders so the new files can use them // create destination folders so the new files can use them
for (dir in dstDirs) { for (dir in dstDirs) {
if (notifications[notification.notificationId]!!) {
cancelNotification(notification)
return@Thread
}
if (!gocryptfsVolume.mkdir(dir)) { if (!gocryptfsVolume.mkdir(dir)) {
cancelNotification(notification) failedItem = dir
callback(dir, srcUris)
break break
} }
} }
importFilesFromUris(dstFiles, srcUris, notification) { failedItem ->
callback(failedItem, srcUris)
} }
}.start() if (failedItem == null) {
failedItem = importFilesFromUris(dstFiles, srcUris, notification)
}
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 {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile ->
val notification = showNotification(R.string.file_op_export_msg, items.size) val notification = showNotification(R.string.file_op_export_msg, items.size)
val task = async {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
var failedItem: String? = null var failedItem: String? = null
for (i in items.indices) { for (i in items.indices) {
if (notifications[notification.notificationId]!!){ withContext(Dispatchers.IO) {
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) { if (failedItem == null) {
updateNotificationProgress(notification, i, items.size) updateNotificationProgress(notification, i+1, items.size)
} else { } else {
break break
} }
} }
cancelNotification(notification) failedItem
callback(failedItem)
} }
}.start() 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 {
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope {
val notification = showNotification(R.string.copy_volume_notification, null) val notification = showNotification(R.string.copy_volume_notification, null)
val total = recursiveCountChildElements(src)
updateNotificationProgress(notification, 0, total)
val dstRootDirectory = ObjRef<DocumentFile?>(null) val dstRootDirectory = ObjRef<DocumentFile?>(null)
val failedItem = recursiveCopyVolume(src, dst, dstRootDirectory, notification, total) val task = async(Dispatchers.IO) {
cancelNotification(notification) val total = recursiveCountChildElements(src, this)
callback(dstRootDirectory.value, failedItem) if (isActive) {
}.start() updateNotificationProgress(notification, 0, total)
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
} else {
null
}
}
// treat cancellation as success
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
} }
} }