forked from hardcoresushi/DroidFS
Switch to Kotlin coroutines
This commit is contained in:
parent
72cce1d7e1
commit
e00abdf5bb
@ -25,6 +25,9 @@ import androidx.camera.extensions.ExtensionsManager
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.app.ActivityCompat
|
||||
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.databinding.ActivityCameraBinding
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
@ -386,19 +389,17 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
if (timerDuration > 0){
|
||||
binding.textTimer.visibility = View.VISIBLE
|
||||
isWaitingForTimer = true
|
||||
Thread{
|
||||
lifecycleScope.launch {
|
||||
for (i in timerDuration downTo 1){
|
||||
runOnUiThread { binding.textTimer.text = i.toString() }
|
||||
Thread.sleep(1000)
|
||||
binding.textTimer.text = i.toString()
|
||||
delay(1000)
|
||||
}
|
||||
if (!isFinishing) {
|
||||
runOnUiThread {
|
||||
action()
|
||||
binding.textTimer.visibility = View.GONE
|
||||
}
|
||||
action()
|
||||
binding.textTimer.visibility = View.GONE
|
||||
}
|
||||
isWaitingForTimer = false
|
||||
}.start()
|
||||
}
|
||||
} else {
|
||||
action()
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import android.text.InputType
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.util.*
|
||||
@ -105,62 +105,63 @@ class ChangePasswordActivity: BaseActivity() {
|
||||
}
|
||||
|
||||
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
|
||||
if (binding.checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var currentPassword: CharArray? = null
|
||||
if (givenHash == null) {
|
||||
currentPassword = CharArray(binding.editCurrentPassword.text.length)
|
||||
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
|
||||
}
|
||||
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (binding.checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var currentPassword: CharArray? = null
|
||||
if (givenHash == null) {
|
||||
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 {
|
||||
val success = GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)
|
||||
if (success) {
|
||||
if (volumeDatabase.isHashSaved(volume.name)) {
|
||||
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)
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,34 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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
|
||||
|
||||
abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) {
|
||||
private val dialogLoadingView = activity.layoutInflater.inflate(R.layout.dialog_loading, null)
|
||||
private val dialogLoading: AlertDialog = CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setView(dialogLoadingView)
|
||||
abstract class LoadingTask<T>(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) {
|
||||
private val dialogLoading = CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setView(
|
||||
DialogLoadingBinding.inflate(activity.layoutInflater).apply {
|
||||
textMessage.text = activity.getString(loadingMessageResId)
|
||||
}.root
|
||||
)
|
||||
.setTitle(R.string.loading)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
private var isStopped = false
|
||||
init {
|
||||
dialogLoadingView.findViewById<TextView>(R.id.text_message).text = activity.getString(loadingMessageResId)
|
||||
startTask()
|
||||
}
|
||||
abstract fun doTask(activity: AppCompatActivity)
|
||||
private fun startTask() {
|
||||
|
||||
abstract suspend fun doTask(): T
|
||||
|
||||
fun startTask(scope: CoroutineScope, onDone: (T) -> Unit) {
|
||||
dialogLoading.show()
|
||||
Thread {
|
||||
doTask(activity)
|
||||
if (!isStopped){
|
||||
dialogLoading.dismiss()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
fun stopTask(onUiThread: (() -> Unit)?){
|
||||
isStopped = true
|
||||
dialogLoading.dismiss()
|
||||
onUiThread?.let {
|
||||
activity.runOnUiThread {
|
||||
onUiThread()
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
doTask()
|
||||
}
|
||||
dialogLoading.dismiss()
|
||||
onDone(result)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,9 +16,10 @@ import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.coroutines.launch
|
||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
@ -34,7 +35,6 @@ import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
|
||||
@ -389,20 +389,25 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
}
|
||||
|
||||
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) {
|
||||
fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) { dstRootDirectory, failedItem ->
|
||||
runOnUiThread {
|
||||
if (failedItem == null) {
|
||||
dstRootDirectory?.let {
|
||||
lifecycleScope.launch {
|
||||
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
|
||||
when {
|
||||
result.taskResult.cancelled -> {
|
||||
result.dstRootDirectory?.delete()
|
||||
}
|
||||
result.taskResult.failedItem == null -> {
|
||||
result.dstRootDirectory?.let {
|
||||
getResultVolume(it)?.let { volume ->
|
||||
volumeDatabase.saveVolume(volume)
|
||||
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 {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
}
|
||||
else -> {
|
||||
CustomAlertDialogBuilder(this@MainActivity, themeValue)
|
||||
.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)
|
||||
.show()
|
||||
}
|
||||
@ -458,20 +463,21 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
object : LoadingTask<Int>(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
||||
override suspend fun doTask(): Int {
|
||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
|
||||
Arrays.fill(hash, 0)
|
||||
if (sessionId != -1)
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
return sessionId
|
||||
}
|
||||
}.startTask(lifecycleScope) { sessionId ->
|
||||
if (sessionId != -1) {
|
||||
startExplorer(sessionId, volume.shortName)
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this@MainActivity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.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) {
|
||||
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
|
||||
if (savePasswordHash && usfFingerprint) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var returnedHash: ByteArray? = null
|
||||
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)
|
||||
Arrays.fill(password, 0.toChar())
|
||||
if (sessionId != -1) {
|
||||
val fingerprintProtector = fingerprintProtector
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
if (savePasswordHash && returnedHash != null && fingerprintProtector != null)
|
||||
stopTask {
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
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)
|
||||
return sessionId
|
||||
}
|
||||
}.startTask(lifecycleScope) { sessionId ->
|
||||
if (sessionId != -1) {
|
||||
val fingerprintProtector = fingerprintProtector
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
else
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
} else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_volume_failed_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener {
|
||||
askForPassword(volume, position)
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
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
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kotlinx.coroutines.*
|
||||
import sushi.hardcore.droidfs.ConstValues
|
||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.R
|
||||
@ -112,33 +113,40 @@ class ExplorerElementAdapter(
|
||||
}
|
||||
|
||||
class FileViewHolder(itemView: View) : RegularElementViewHolder(itemView) {
|
||||
var displayThumbnail = true
|
||||
var target: DrawableImageViewTarget? = null
|
||||
private var target: DrawableImageViewTarget? = null
|
||||
private var job: Job? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private fun loadThumbnail(fullPath: String, adapter: ExplorerElementAdapter) {
|
||||
adapter.gocryptfsVolume?.let { volume ->
|
||||
displayThumbnail = true
|
||||
Thread {
|
||||
job = scope.launch {
|
||||
volume.loadWholeFile(fullPath, maxSize = adapter.thumbnailMaxSize).first?.let {
|
||||
if (displayThumbnail) {
|
||||
adapter.activity.runOnUiThread {
|
||||
if (displayThumbnail && !adapter.activity.isFinishing) {
|
||||
if (isActive) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isActive && !adapter.activity.isFinishing) {
|
||||
target = Glide.with(adapter.activity).load(it).skipMemoryCache(true).into(object : DrawableImageViewTarget(icon) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable>?
|
||||
) {
|
||||
target = null
|
||||
val bitmap = resource.toBitmap()
|
||||
adapter.thumbnailsCache!!.put(fullPath, bitmap.copy(bitmap.config, true))
|
||||
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) {
|
||||
if (holder is FileViewHolder) {
|
||||
//cancel pending thumbnail display
|
||||
holder.displayThumbnail = false
|
||||
holder.target?.let {
|
||||
Glide.with(activity).clear(it)
|
||||
}
|
||||
holder.cancelThumbnailLoading(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import sushi.hardcore.droidfs.*
|
||||
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
@ -123,9 +124,14 @@ class CreateVolumeFragment: Fragment() {
|
||||
if (!password.contentEquals(passwordConfirm)) {
|
||||
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
Arrays.fill(password, 0.toChar())
|
||||
Arrays.fill(passwordConfirm, 0.toChar())
|
||||
} else {
|
||||
object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
Arrays.fill(passwordConfirm, 0.toChar())
|
||||
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) {
|
||||
0 -> 0
|
||||
1 -> 1
|
||||
@ -134,10 +140,16 @@ class CreateVolumeFragment: Fragment() {
|
||||
val volumeFile = File(volumePath)
|
||||
if (!volumeFile.exists())
|
||||
volumeFile.mkdirs()
|
||||
var returnedHash: ByteArray? = null
|
||||
if (binding.checkboxSavePassword.isChecked)
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
if (GocryptfsVolume.createVolume(volumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.CREATOR, returnedHash)) {
|
||||
val volume = if (GocryptfsVolume.createVolume(
|
||||
volumePath,
|
||||
password,
|
||||
false,
|
||||
xchacha,
|
||||
GocryptfsVolume.ScryptDefaultLogN,
|
||||
ConstValues.CREATOR,
|
||||
returnedHash
|
||||
)
|
||||
) {
|
||||
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
||||
val volume = Volume(volumeName, isHiddenVolume)
|
||||
volumeDatabase.apply {
|
||||
@ -145,46 +157,48 @@ class CreateVolumeFragment: Fragment() {
|
||||
removeVolume(volumeName)
|
||||
saveVolume(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() {
|
||||
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()
|
||||
}
|
||||
volume
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.create_volume_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
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
|
||||
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() {
|
||||
|
@ -5,6 +5,10 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
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.LoadingTask
|
||||
import sushi.hardcore.droidfs.R
|
||||
@ -13,7 +17,7 @@ import java.io.File
|
||||
|
||||
object ExternalProvider {
|
||||
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 {
|
||||
if (content_type_all != previous_content_type) {
|
||||
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>) {
|
||||
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
var contentType: String? = null
|
||||
val uris = ArrayList<Uri>()
|
||||
var contentType: String? = null
|
||||
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) {
|
||||
val result = exportFile(activity, gocryptfsVolume, path, contentType)
|
||||
contentType = if (result.first != null) {
|
||||
uris.add(result.first!!)
|
||||
result.second
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(activity.getString(R.string.export_failed, path))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
return
|
||||
return path
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}.startTask(activity.lifecycleScope) { failedItem ->
|
||||
if (failedItem == null) {
|
||||
val shareIntent = Intent()
|
||||
shareIntent.type = contentType
|
||||
if (uris.size == 1) {
|
||||
@ -71,45 +72,51 @@ object ExternalProvider {
|
||||
shareIntent.action = Intent.ACTION_SEND_MULTIPLE
|
||||
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) {
|
||||
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
object : LoadingTask<Intent?>(activity, themeValue, R.string.loading_msg_export) {
|
||||
override suspend fun doTask(): Intent? {
|
||||
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 {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(activity.getString(R.string.export_failed, file_path))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
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) {
|
||||
Thread{
|
||||
val success = ArrayList<Uri>()
|
||||
for (uri in storedFiles){
|
||||
if (context.contentResolver.delete(uri, null, null) == 1){
|
||||
success.add(uri)
|
||||
}
|
||||
fun removeFilesAsync(context: Context) = GlobalScope.launch(Dispatchers.IO) {
|
||||
val success = HashSet<Uri>(storedFiles.size)
|
||||
for (uri in storedFiles) {
|
||||
if (context.contentResolver.delete(uri, null, null) == 1) {
|
||||
success.add(uri)
|
||||
}
|
||||
for (uri in success){
|
||||
storedFiles.remove(uri)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
for (uri in success) {
|
||||
storedFiles.remove(uri)
|
||||
}
|
||||
}
|
||||
}
|
@ -14,13 +14,16 @@ import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.ConstValues
|
||||
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(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory })
|
||||
if (mapFolders) {
|
||||
Thread {
|
||||
lifecycleScope.launch {
|
||||
var totalSize: Long = 0
|
||||
synchronized(this) {
|
||||
for (element in explorerElements) {
|
||||
if (element.isDirectory) {
|
||||
recursiveSetSize(element)
|
||||
withContext(Dispatchers.IO) {
|
||||
synchronized(this@BaseExplorerActivity) {
|
||||
for (element in explorerElements) {
|
||||
if (element.isDirectory) {
|
||||
recursiveSetSize(element)
|
||||
}
|
||||
totalSize += element.size
|
||||
}
|
||||
totalSize += element.size
|
||||
}
|
||||
}
|
||||
runOnUiThread {
|
||||
displayExplorerElements(totalSize)
|
||||
onDisplayed?.invoke()
|
||||
}
|
||||
}.start()
|
||||
displayExplorerElements(totalSize)
|
||||
onDisplayed?.invoke()
|
||||
}
|
||||
} else {
|
||||
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size })
|
||||
onDisplayed?.invoke()
|
||||
@ -455,9 +458,12 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
if (items.size > 0) {
|
||||
checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
|
||||
checkedItems?.let {
|
||||
fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris){ failedItem ->
|
||||
runOnUiThread {
|
||||
callback(failedItem)
|
||||
lifecycleScope.launch {
|
||||
val taskResult = fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris)
|
||||
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){
|
||||
if (new_name.isEmpty()) {
|
||||
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
|
||||
@ -627,7 +619,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
finish()
|
||||
} else {
|
||||
isStartingActivity = false
|
||||
ExternalProvider.removeFiles(this)
|
||||
ExternalProvider.removeFilesAsync(this)
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import sushi.hardcore.droidfs.CameraActivity
|
||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.MainActivity
|
||||
@ -67,19 +69,18 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
if (items == null) {
|
||||
remoteGocryptfsVolume.close()
|
||||
} else {
|
||||
fileOperationService.copyElements(items, remoteGocryptfsVolume){ failedItem ->
|
||||
runOnUiThread {
|
||||
if (failedItem == null){
|
||||
Toast.makeText(this, R.string.success_import, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.import_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
lifecycleScope.launch {
|
||||
val failedItem = fileOperationService.copyElements(items, remoteGocryptfsVolume)
|
||||
if (failedItem == null) {
|
||||
Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.import_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
remoteGocryptfsVolume.close()
|
||||
}
|
||||
}
|
||||
@ -99,14 +100,15 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
}
|
||||
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
if (uri != null) {
|
||||
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
|
||||
runOnUiThread {
|
||||
if (failedItem == null){
|
||||
Toast.makeText(this, R.string.success_export, Toast.LENGTH_SHORT).show()
|
||||
lifecycleScope.launch {
|
||||
val result = fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] })
|
||||
if (!result.cancelled) {
|
||||
if (result.failedItem == null) {
|
||||
Toast.makeText(this@ExplorerActivity, R.string.success_export, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.export_failed, failedItem))
|
||||
.setMessage(getString(R.string.export_failed, result.failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
@ -117,7 +119,20 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
}
|
||||
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
|
||||
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)}
|
||||
""".trimIndent())
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
fileOperationService.wipeUris(urisToWipe, rootFile) { errorMsg ->
|
||||
runOnUiThread {
|
||||
if (errorMsg == null){
|
||||
Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.wipe_failed, errorMsg))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile)
|
||||
if (errorMsg == null) {
|
||||
Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.wipe_failed, errorMsg))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -310,16 +324,17 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
if (currentItemAction == ItemsActions.COPY){
|
||||
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
||||
items?.let {
|
||||
fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
|
||||
runOnUiThread {
|
||||
if (failedItem == null){
|
||||
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
||||
lifecycleScope.launch {
|
||||
val failedItem = fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>)
|
||||
if (!isFinishing) {
|
||||
if (failedItem == null) {
|
||||
Toast.makeText(this@ExplorerActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.copy_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.copy_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
}
|
||||
@ -332,19 +347,18 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath)
|
||||
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
||||
items?.let {
|
||||
fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
|
||||
runOnUiThread {
|
||||
if (failedItem == null){
|
||||
Toast.makeText(this, R.string.move_success, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.move_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
lifecycleScope.launch {
|
||||
val failedItem = fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>)
|
||||
if (failedItem == null) {
|
||||
Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(getString(R.string.move_failed, failedItem))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
cancelItemAction()
|
||||
|
@ -13,6 +13,7 @@ import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.*
|
||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
||||
@ -30,7 +31,7 @@ class FileOperationService : Service() {
|
||||
private val binder = LocalBinder()
|
||||
private lateinit var gocryptfsVolume: GocryptfsVolume
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private var notifications = HashMap<Int, Boolean>()
|
||||
private val tasks = HashMap<Int, Job>()
|
||||
private var lastNotificationId = 0
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
@ -88,7 +89,6 @@ class FileOperationService : Service() {
|
||||
.setContentText(getString(R.string.discovering_files))
|
||||
.setProgress(0, 0, true)
|
||||
}
|
||||
notifications[lastNotificationId] = false
|
||||
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
||||
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
||||
}
|
||||
@ -105,7 +105,20 @@ class FileOperationService : Service() {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -137,110 +150,105 @@ class FileOperationService : Service() {
|
||||
return success
|
||||
}
|
||||
|
||||
fun copyElements(items: ArrayList<OperationFile>, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){
|
||||
Thread {
|
||||
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
||||
suspend fun copyElements(
|
||||
items: ArrayList<OperationFile>,
|
||||
remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
|
||||
): String? = coroutineScope {
|
||||
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
||||
val task = async {
|
||||
var failedItem: String? = null
|
||||
for (i in 0 until items.size){
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
return@Thread
|
||||
}
|
||||
if (items[i].explorerElement.isDirectory){
|
||||
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
||||
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
||||
for (i in 0 until items.size) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (items[i].explorerElement.isDirectory) {
|
||||
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
||||
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
||||
failedItem = items[i].explorerElement.fullPath
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
|
||||
failedItem = items[i].explorerElement.fullPath
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){
|
||||
failedItem = items[i].explorerElement.fullPath
|
||||
}
|
||||
}
|
||||
if (failedItem == null){
|
||||
updateNotificationProgress(notification, i, items.size)
|
||||
if (failedItem == null) {
|
||||
updateNotificationProgress(notification, i+1, items.size)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
cancelNotification(notification)
|
||||
callback(failedItem)
|
||||
}.start()
|
||||
failedItem
|
||||
}
|
||||
// treat cancellation as success
|
||||
waitForTask(notification, task).failedItem
|
||||
}
|
||||
|
||||
fun moveElements(items: ArrayList<OperationFile>, callback: (String?) -> Unit){
|
||||
Thread {
|
||||
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
||||
val mergedFolders = ArrayList<String>()
|
||||
suspend fun moveElements(items: ArrayList<OperationFile>): String? = coroutineScope {
|
||||
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
||||
val task = async {
|
||||
var failedItem: String? = null
|
||||
for (i in 0 until items.size){
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
return@Thread
|
||||
}
|
||||
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
|
||||
withContext(Dispatchers.IO) {
|
||||
val mergedFolders = ArrayList<String>()
|
||||
for (i in 0 until items.size) {
|
||||
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)) { //folder will be merged
|
||||
mergedFolders.add(items[i].explorerElement.fullPath)
|
||||
} 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){
|
||||
for (i in 0 until mergedFolders.size) {
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
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()
|
||||
failedItem
|
||||
}
|
||||
// treat cancellation as success
|
||||
waitForTask(notification, task).failedItem
|
||||
}
|
||||
|
||||
private fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, reuseNotification: FileOperationNotification? = null, callback: (String?) -> Unit){
|
||||
val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size)
|
||||
private suspend fun importFilesFromUris(
|
||||
dstPaths: List<String>,
|
||||
uris: List<Uri>,
|
||||
notification: FileOperationNotification,
|
||||
): String? {
|
||||
var failedIndex = -1
|
||||
for (i in dstPaths.indices) {
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
|
||||
failedIndex = i
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
failedIndex = i
|
||||
}
|
||||
} catch (e: FileNotFoundException){
|
||||
failedIndex = i
|
||||
}
|
||||
if (failedIndex == -1) {
|
||||
updateNotificationProgress(notification, i, dstPaths.size)
|
||||
updateNotificationProgress(notification, i+1, dstPaths.size)
|
||||
} else {
|
||||
cancelNotification(notification)
|
||||
callback(uris[failedIndex].toString())
|
||||
break
|
||||
return uris[failedIndex].toString()
|
||||
}
|
||||
}
|
||||
if (failedIndex == -1){
|
||||
cancelNotification(notification)
|
||||
callback(null)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) {
|
||||
Thread {
|
||||
importFilesFromUris(dstPaths, uris, null, callback)
|
||||
}.start()
|
||||
suspend fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>): TaskResult<String?> = coroutineScope {
|
||||
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
|
||||
val task = async {
|
||||
importFilesFromUris(dstPaths, uris, notification)
|
||||
}
|
||||
waitForTask(notification, task)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -256,18 +264,17 @@ class FileOperationService : Service() {
|
||||
dstFiles: ArrayList<String>,
|
||||
srcUris: ArrayList<Uri>,
|
||||
dstDirs: ArrayList<String>,
|
||||
notification: FileOperationNotification
|
||||
scope: CoroutineScope,
|
||||
): Boolean {
|
||||
dstDirs.add(rootDstPath)
|
||||
for (child in rootSrcDir.listFiles()) {
|
||||
if (notifications[notification.notificationId]!!) {
|
||||
cancelNotification(notification)
|
||||
if (!scope.isActive) {
|
||||
return false
|
||||
}
|
||||
child.name?.let { name ->
|
||||
val subPath = PathUtils.pathJoin(rootDstPath, name)
|
||||
if (child.isDirectory) {
|
||||
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) {
|
||||
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -280,48 +287,50 @@ class FileOperationService : Service() {
|
||||
return true
|
||||
}
|
||||
|
||||
fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List<Uri>) -> Unit) {
|
||||
Thread {
|
||||
val notification = showNotification(R.string.file_op_import_msg, null)
|
||||
class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>)
|
||||
|
||||
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 srcUris = arrayListOf<Uri>()
|
||||
val dstDirs = arrayListOf<String>()
|
||||
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
|
||||
return@Thread
|
||||
}
|
||||
|
||||
// create destination folders so the new files can use them
|
||||
for (dir in dstDirs) {
|
||||
if (notifications[notification.notificationId]!!) {
|
||||
cancelNotification(notification)
|
||||
return@Thread
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
|
||||
return@withContext
|
||||
}
|
||||
if (!gocryptfsVolume.mkdir(dir)) {
|
||||
cancelNotification(notification)
|
||||
callback(dir, srcUris)
|
||||
break
|
||||
|
||||
// create destination folders so the new files can use them
|
||||
for (dir in dstDirs) {
|
||||
if (!gocryptfsVolume.mkdir(dir)) {
|
||||
failedItem = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importFilesFromUris(dstFiles, srcUris, notification) { failedItem ->
|
||||
callback(failedItem, srcUris)
|
||||
if (failedItem == null) {
|
||||
failedItem = importFilesFromUris(dstFiles, srcUris, notification)
|
||||
}
|
||||
}.start()
|
||||
failedItem
|
||||
}
|
||||
ImportDirectoryResult(waitForTask(notification, task), srcUris)
|
||||
}
|
||||
|
||||
fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null, callback: (String?) -> Unit){
|
||||
Thread {
|
||||
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
||||
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): String? = coroutineScope {
|
||||
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
||||
val task = async {
|
||||
var errorMsg: String? = null
|
||||
for (i in uris.indices) {
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
return@Thread
|
||||
withContext(Dispatchers.IO) {
|
||||
errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
|
||||
}
|
||||
errorMsg = Wiper.wipe(this, uris[i])
|
||||
if (errorMsg == null) {
|
||||
updateNotificationProgress(notification, i, uris.size)
|
||||
updateNotificationProgress(notification, i+1, uris.size)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@ -329,9 +338,10 @@ class FileOperationService : Service() {
|
||||
if (errorMsg == null) {
|
||||
rootFile?.delete()
|
||||
}
|
||||
cancelNotification(notification)
|
||||
callback(errorMsg)
|
||||
}.start()
|
||||
errorMsg
|
||||
}
|
||||
// treat cancellation as success
|
||||
waitForTask(notification, task).failedItem
|
||||
}
|
||||
|
||||
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 ->
|
||||
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
|
||||
for (e in explorerElements) {
|
||||
if (!scope.isActive) {
|
||||
return null
|
||||
}
|
||||
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
|
||||
if (e.isDirectory) {
|
||||
val failedItem = recursiveExportDirectory(fullPath, childTree)
|
||||
val failedItem = recursiveExportDirectory(fullPath, childTree, scope)
|
||||
failedItem?.let { return it }
|
||||
} else {
|
||||
if (!exportFileInto(fullPath, childTree)){
|
||||
@ -364,40 +381,40 @@ class FileOperationService : Service() {
|
||||
return treeDocumentFile.name
|
||||
}
|
||||
|
||||
fun exportFiles(uri: Uri, items: List<ExplorerElement>, callback: (String?) -> Unit){
|
||||
Thread {
|
||||
suspend fun exportFiles(uri: Uri, items: List<ExplorerElement>): TaskResult<String?> = coroutineScope {
|
||||
val notification = showNotification(R.string.file_op_export_msg, items.size)
|
||||
val task = async {
|
||||
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)
|
||||
var failedItem: String? = null
|
||||
for (i in items.indices) {
|
||||
if (notifications[notification.notificationId]!!){
|
||||
cancelNotification(notification)
|
||||
return@Thread
|
||||
}
|
||||
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
|
||||
var failedItem: String? = null
|
||||
for (i in items.indices) {
|
||||
withContext(Dispatchers.IO) {
|
||||
failedItem = if (items[i].isDirectory) {
|
||||
recursiveExportDirectory(items[i].fullPath, treeDocumentFile)
|
||||
recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this)
|
||||
} else {
|
||||
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
|
||||
}
|
||||
if (failedItem == null) {
|
||||
updateNotificationProgress(notification, i, items.size)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
cancelNotification(notification)
|
||||
callback(failedItem)
|
||||
if (failedItem == null) {
|
||||
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()
|
||||
var count = children.size
|
||||
for (child in children) {
|
||||
if (child.isDirectory) {
|
||||
count += recursiveCountChildElements(child)
|
||||
count += recursiveCountChildElements(child, scope)
|
||||
}
|
||||
}
|
||||
return count
|
||||
@ -411,12 +428,13 @@ class FileOperationService : Service() {
|
||||
dstRootDirectory: ObjRef<DocumentFile?>?,
|
||||
notification: FileOperationNotification,
|
||||
total: Int,
|
||||
scope: CoroutineScope,
|
||||
progress: ObjRef<Int> = ObjRef(0)
|
||||
): DocumentFile? {
|
||||
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
|
||||
dstRootDirectory?.let { it.value = dstDir }
|
||||
for (child in src.listFiles()) {
|
||||
if (notifications[notification.notificationId]!!) {
|
||||
cancelNotification(notification)
|
||||
if (!scope.isActive) {
|
||||
return null
|
||||
}
|
||||
if (child.isFile) {
|
||||
@ -429,24 +447,29 @@ class FileOperationService : Service() {
|
||||
inputStream.close()
|
||||
if (written != child.length()) return child
|
||||
} else {
|
||||
recursiveCopyVolume(child, dstDir, null, notification, total, progress)?.let { return it }
|
||||
recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
|
||||
}
|
||||
progress.value++
|
||||
updateNotificationProgress(notification, progress.value, total)
|
||||
}
|
||||
dstRootDirectory?.let { it.value = dstDir }
|
||||
return null
|
||||
}
|
||||
|
||||
fun copyVolume(src: DocumentFile, dst: DocumentFile, callback: (DocumentFile?, DocumentFile?) -> Unit) {
|
||||
Thread {
|
||||
val notification = showNotification(R.string.copy_volume_notification, null)
|
||||
val total = recursiveCountChildElements(src)
|
||||
updateNotificationProgress(notification, 0, total)
|
||||
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
||||
val failedItem = recursiveCopyVolume(src, dst, dstRootDirectory, notification, total)
|
||||
cancelNotification(notification)
|
||||
callback(dstRootDirectory.value, failedItem)
|
||||
}.start()
|
||||
class CopyVolumeResult(val taskResult: TaskResult<DocumentFile?>, val dstRootDirectory: DocumentFile?)
|
||||
|
||||
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope {
|
||||
val notification = showNotification(R.string.copy_volume_notification, null)
|
||||
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
||||
val task = async(Dispatchers.IO) {
|
||||
val total = recursiveCountChildElements(src, this)
|
||||
if (isActive) {
|
||||
updateNotificationProgress(notification, 0, total)
|
||||
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
// treat cancellation as success
|
||||
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user