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.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()
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import android.text.InputType
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.lifecycle.lifecycleScope
|
||||||
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
|
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
|
||||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -105,62 +105,63 @@ class ChangePasswordActivity: BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
|
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
|
||||||
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
|
var returnedHash: ByteArray? = null
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
if (binding.checkboxSavePassword.isChecked) {
|
||||||
var returnedHash: ByteArray? = null
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
if (binding.checkboxSavePassword.isChecked) {
|
}
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
var currentPassword: CharArray? = null
|
||||||
}
|
if (givenHash == null) {
|
||||||
var currentPassword: CharArray? = null
|
currentPassword = CharArray(binding.editCurrentPassword.text.length)
|
||||||
if (givenHash == null) {
|
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
|
||||||
currentPassword = CharArray(binding.editCurrentPassword.text.length)
|
}
|
||||||
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
|
object : LoadingTask<Boolean>(this, themeValue, R.string.loading_msg_change_password) {
|
||||||
}
|
override suspend fun doTask(): Boolean {
|
||||||
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) {
|
val success = GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)
|
||||||
|
if (success) {
|
||||||
if (volumeDatabase.isHashSaved(volume.name)) {
|
if (volumeDatabase.isHashSaved(volume.name)) {
|
||||||
volumeDatabase.removeHash(volume)
|
volumeDatabase.removeHash(volume)
|
||||||
}
|
}
|
||||||
stopTask {
|
|
||||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
|
||||||
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
|
||||||
fingerprintProtector!!.let {
|
|
||||||
it.listener = object : FingerprintProtector.Listener {
|
|
||||||
override fun onHashStorageReset() {
|
|
||||||
// retry
|
|
||||||
it.savePasswordHash(volume, returnedHash)
|
|
||||||
}
|
|
||||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
|
||||||
override fun onPasswordHashSaved() {
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
override fun onFailed(pending: Boolean) {
|
|
||||||
if (!pending) {
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.savePasswordHash(volume, returnedHash)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stopTask {
|
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(R.string.change_password_failed)
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (currentPassword != null)
|
if (currentPassword != null)
|
||||||
Arrays.fill(currentPassword, 0.toChar())
|
Arrays.fill(currentPassword, 0.toChar())
|
||||||
Arrays.fill(newPassword, 0.toChar())
|
Arrays.fill(newPassword, 0.toChar())
|
||||||
if (givenHash != null)
|
if (givenHash != null)
|
||||||
Arrays.fill(givenHash, 0)
|
Arrays.fill(givenHash, 0)
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
}.startTask(lifecycleScope) { success ->
|
||||||
|
if (success) {
|
||||||
|
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||||
|
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||||
|
fingerprintProtector!!.let {
|
||||||
|
it.listener = object : FingerprintProtector.Listener {
|
||||||
|
override fun onHashStorageReset() {
|
||||||
|
// retry
|
||||||
|
it.savePasswordHash(volume, returnedHash)
|
||||||
|
}
|
||||||
|
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||||
|
override fun onPasswordHashSaved() {
|
||||||
|
Arrays.fill(returnedHash, 0)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
override fun onFailed(pending: Boolean) {
|
||||||
|
if (!pending) {
|
||||||
|
Arrays.fill(returnedHash, 0)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.savePasswordHash(volume, returnedHash)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CustomAlertDialogBuilder(this, themeValue)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(R.string.change_password_failed)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,34 @@
|
|||||||
package sushi.hardcore.droidfs
|
package sushi.hardcore.droidfs
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sushi.hardcore.droidfs.databinding.DialogLoadingBinding
|
||||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||||
|
|
||||||
abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) {
|
abstract class LoadingTask<T>(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) {
|
||||||
private val dialogLoadingView = activity.layoutInflater.inflate(R.layout.dialog_loading, null)
|
private val dialogLoading = CustomAlertDialogBuilder(activity, themeValue)
|
||||||
private val dialogLoading: AlertDialog = CustomAlertDialogBuilder(activity, themeValue)
|
.setView(
|
||||||
.setView(dialogLoadingView)
|
DialogLoadingBinding.inflate(activity.layoutInflater).apply {
|
||||||
|
textMessage.text = activity.getString(loadingMessageResId)
|
||||||
|
}.root
|
||||||
|
)
|
||||||
.setTitle(R.string.loading)
|
.setTitle(R.string.loading)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.create()
|
.create()
|
||||||
private var isStopped = false
|
|
||||||
init {
|
abstract suspend fun doTask(): T
|
||||||
dialogLoadingView.findViewById<TextView>(R.id.text_message).text = activity.getString(loadingMessageResId)
|
|
||||||
startTask()
|
fun startTask(scope: CoroutineScope, onDone: (T) -> Unit) {
|
||||||
}
|
|
||||||
abstract fun doTask(activity: AppCompatActivity)
|
|
||||||
private fun startTask() {
|
|
||||||
dialogLoading.show()
|
dialogLoading.show()
|
||||||
Thread {
|
scope.launch {
|
||||||
doTask(activity)
|
val result = withContext(Dispatchers.IO) {
|
||||||
if (!isStopped){
|
doTask()
|
||||||
dialogLoading.dismiss()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
fun stopTask(onUiThread: (() -> Unit)?){
|
|
||||||
isStopped = true
|
|
||||||
dialogLoading.dismiss()
|
|
||||||
onUiThread?.let {
|
|
||||||
activity.runOnUiThread {
|
|
||||||
onUiThread()
|
|
||||||
}
|
}
|
||||||
|
dialogLoading.dismiss()
|
||||||
|
onDone(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,9 +16,10 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||||
@ -34,7 +35,6 @@ import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
|||||||
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.NoSuchElementException
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||||
|
|
||||||
@ -389,20 +389,25 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) {
|
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> Volume?) {
|
||||||
fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) { dstRootDirectory, failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
|
||||||
if (failedItem == null) {
|
when {
|
||||||
dstRootDirectory?.let {
|
result.taskResult.cancelled -> {
|
||||||
|
result.dstRootDirectory?.delete()
|
||||||
|
}
|
||||||
|
result.taskResult.failedItem == null -> {
|
||||||
|
result.dstRootDirectory?.let {
|
||||||
getResultVolume(it)?.let { volume ->
|
getResultVolume(it)?.let { volume ->
|
||||||
volumeDatabase.saveVolume(volume)
|
volumeDatabase.saveVolume(volume)
|
||||||
onVolumeAdded()
|
onVolumeAdded()
|
||||||
|
Toast.makeText(this@MainActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
}
|
||||||
} else {
|
else -> {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@MainActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.copy_failed, failedItem.name))
|
.setMessage(getString(R.string.copy_failed, result.taskResult.failedItem.name))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@ -458,20 +463,21 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
volumeAdapter.refresh()
|
volumeAdapter.refresh()
|
||||||
}
|
}
|
||||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||||
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
object : LoadingTask<Int>(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
override suspend fun doTask(): Int {
|
||||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
|
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
|
||||||
Arrays.fill(hash, 0)
|
Arrays.fill(hash, 0)
|
||||||
if (sessionId != -1)
|
return sessionId
|
||||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
}
|
||||||
else
|
}.startTask(lifecycleScope) { sessionId ->
|
||||||
stopTask {
|
if (sessionId != -1) {
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
startExplorer(sessionId, volume.shortName)
|
||||||
.setTitle(R.string.open_volume_failed)
|
} else {
|
||||||
.setMessage(R.string.open_failed_hash_msg)
|
CustomAlertDialogBuilder(this@MainActivity, themeValue)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setTitle(R.string.open_volume_failed)
|
||||||
.show()
|
.setMessage(R.string.open_failed_hash_msg)
|
||||||
}
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -544,53 +550,53 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
|
|
||||||
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
|
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
|
||||||
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||||
object : LoadingTask(this, themeValue, R.string.loading_msg_open) {
|
var returnedHash: ByteArray? = null
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
if (savePasswordHash && usfFingerprint) {
|
||||||
var returnedHash: ByteArray? = null
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
if (savePasswordHash && usfFingerprint) {
|
}
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
object : LoadingTask<Int>(this, themeValue, R.string.loading_msg_open) {
|
||||||
}
|
override suspend fun doTask(): Int {
|
||||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
|
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
|
||||||
Arrays.fill(password, 0.toChar())
|
Arrays.fill(password, 0.toChar())
|
||||||
if (sessionId != -1) {
|
return sessionId
|
||||||
val fingerprintProtector = fingerprintProtector
|
}
|
||||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
}.startTask(lifecycleScope) { sessionId ->
|
||||||
if (savePasswordHash && returnedHash != null && fingerprintProtector != null)
|
if (sessionId != -1) {
|
||||||
stopTask {
|
val fingerprintProtector = fingerprintProtector
|
||||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||||
override fun onHashStorageReset() {
|
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
|
||||||
volumeAdapter.refresh()
|
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||||
}
|
override fun onHashStorageReset() {
|
||||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
volumeAdapter.refresh()
|
||||||
override fun onPasswordHashSaved() {
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
volumeAdapter.onVolumeChanged(position)
|
|
||||||
startExplorer(sessionId, volume.shortName)
|
|
||||||
}
|
|
||||||
private var isClosed = false
|
|
||||||
override fun onFailed(pending: Boolean) {
|
|
||||||
if (!isClosed) {
|
|
||||||
GocryptfsVolume(this@MainActivity, sessionId).close()
|
|
||||||
isClosed = true
|
|
||||||
}
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fingerprintProtector.savePasswordHash(volume, returnedHash)
|
|
||||||
}
|
}
|
||||||
else
|
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
override fun onPasswordHashSaved() {
|
||||||
} else
|
Arrays.fill(returnedHash, 0)
|
||||||
stopTask {
|
volumeAdapter.onVolumeChanged(position)
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
startExplorer(sessionId, volume.shortName)
|
||||||
.setTitle(R.string.open_volume_failed)
|
}
|
||||||
.setMessage(R.string.open_volume_failed_msg)
|
private var isClosed = false
|
||||||
.setPositiveButton(R.string.ok, null)
|
override fun onFailed(pending: Boolean) {
|
||||||
.setOnDismissListener {
|
if (!isClosed) {
|
||||||
askForPassword(volume, position)
|
GocryptfsVolume(this@MainActivity, sessionId).close()
|
||||||
|
isClosed = true
|
||||||
}
|
}
|
||||||
.show()
|
Arrays.fill(returnedHash, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
fingerprintProtector.savePasswordHash(volume, returnedHash)
|
||||||
|
} else {
|
||||||
|
startExplorer(sessionId, volume.shortName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CustomAlertDialogBuilder(this, themeValue)
|
||||||
|
.setTitle(R.string.open_volume_failed)
|
||||||
|
.setMessage(R.string.open_volume_failed_msg)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.setOnDismissListener {
|
||||||
|
askForPassword(volume, position)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import android.widget.ArrayAdapter
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import sushi.hardcore.droidfs.*
|
import sushi.hardcore.droidfs.*
|
||||||
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
|
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
|
||||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||||
@ -123,9 +124,14 @@ class CreateVolumeFragment: Fragment() {
|
|||||||
if (!password.contentEquals(passwordConfirm)) {
|
if (!password.contentEquals(passwordConfirm)) {
|
||||||
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||||
Arrays.fill(password, 0.toChar())
|
Arrays.fill(password, 0.toChar())
|
||||||
|
Arrays.fill(passwordConfirm, 0.toChar())
|
||||||
} else {
|
} else {
|
||||||
object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
|
Arrays.fill(passwordConfirm, 0.toChar())
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
var returnedHash: ByteArray? = null
|
||||||
|
if (binding.checkboxSavePassword.isChecked)
|
||||||
|
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||||
|
object: LoadingTask<Volume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
|
||||||
|
override suspend fun doTask(): Volume? {
|
||||||
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
|
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
|
||||||
0 -> 0
|
0 -> 0
|
||||||
1 -> 1
|
1 -> 1
|
||||||
@ -134,10 +140,16 @@ class CreateVolumeFragment: Fragment() {
|
|||||||
val volumeFile = File(volumePath)
|
val volumeFile = File(volumePath)
|
||||||
if (!volumeFile.exists())
|
if (!volumeFile.exists())
|
||||||
volumeFile.mkdirs()
|
volumeFile.mkdirs()
|
||||||
var returnedHash: ByteArray? = null
|
val volume = if (GocryptfsVolume.createVolume(
|
||||||
if (binding.checkboxSavePassword.isChecked)
|
volumePath,
|
||||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
password,
|
||||||
if (GocryptfsVolume.createVolume(volumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.CREATOR, returnedHash)) {
|
false,
|
||||||
|
xchacha,
|
||||||
|
GocryptfsVolume.ScryptDefaultLogN,
|
||||||
|
ConstValues.CREATOR,
|
||||||
|
returnedHash
|
||||||
|
)
|
||||||
|
) {
|
||||||
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
||||||
val volume = Volume(volumeName, isHiddenVolume)
|
val volume = Volume(volumeName, isHiddenVolume)
|
||||||
volumeDatabase.apply {
|
volumeDatabase.apply {
|
||||||
@ -145,46 +157,48 @@ class CreateVolumeFragment: Fragment() {
|
|||||||
removeVolume(volumeName)
|
removeVolume(volumeName)
|
||||||
saveVolume(volume)
|
saveVolume(volume)
|
||||||
}
|
}
|
||||||
stopTask {
|
volume
|
||||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
|
||||||
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
|
||||||
fingerprintProtector!!.let {
|
|
||||||
it.listener = object : FingerprintProtector.Listener {
|
|
||||||
override fun onHashStorageReset() {
|
|
||||||
hashStorageReset = true
|
|
||||||
// retry
|
|
||||||
it.savePasswordHash(volume, returnedHash)
|
|
||||||
}
|
|
||||||
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
|
|
||||||
override fun onPasswordHashSaved() {
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
onVolumeCreated()
|
|
||||||
}
|
|
||||||
override fun onFailed(pending: Boolean) {
|
|
||||||
if (!pending) {
|
|
||||||
Arrays.fill(returnedHash, 0)
|
|
||||||
onVolumeCreated()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.savePasswordHash(volume, returnedHash)
|
|
||||||
}
|
|
||||||
} else onVolumeCreated()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stopTask {
|
null
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(R.string.create_volume_failed)
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Arrays.fill(password, 0.toChar())
|
Arrays.fill(password, 0.toChar())
|
||||||
|
return volume
|
||||||
|
}
|
||||||
|
}.startTask(lifecycleScope) { volume ->
|
||||||
|
if (volume == null) {
|
||||||
|
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(R.string.create_volume_failed)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||||
|
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||||
|
fingerprintProtector!!.let {
|
||||||
|
it.listener = object : FingerprintProtector.Listener {
|
||||||
|
override fun onHashStorageReset() {
|
||||||
|
hashStorageReset = true
|
||||||
|
// retry
|
||||||
|
it.savePasswordHash(volume, returnedHash)
|
||||||
|
}
|
||||||
|
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
|
||||||
|
override fun onPasswordHashSaved() {
|
||||||
|
Arrays.fill(returnedHash, 0)
|
||||||
|
onVolumeCreated()
|
||||||
|
}
|
||||||
|
override fun onFailed(pending: Boolean) {
|
||||||
|
if (!pending) {
|
||||||
|
Arrays.fill(returnedHash, 0)
|
||||||
|
onVolumeCreated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.savePasswordHash(volume, returnedHash)
|
||||||
|
}
|
||||||
|
} else onVolumeCreated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Arrays.fill(passwordConfirm, 0.toChar())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onVolumeCreated() {
|
private fun onVolumeCreated() {
|
||||||
|
@ -5,6 +5,10 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||||
import sushi.hardcore.droidfs.LoadingTask
|
import sushi.hardcore.droidfs.LoadingTask
|
||||||
import sushi.hardcore.droidfs.R
|
import sushi.hardcore.droidfs.R
|
||||||
@ -13,7 +17,7 @@ import java.io.File
|
|||||||
|
|
||||||
object ExternalProvider {
|
object ExternalProvider {
|
||||||
private const val content_type_all = "*/*"
|
private const val content_type_all = "*/*"
|
||||||
private var storedFiles: MutableList<Uri> = ArrayList()
|
private var storedFiles = HashSet<Uri>()
|
||||||
private fun getContentType(filename: String, previous_content_type: String?): String {
|
private fun getContentType(filename: String, previous_content_type: String?): String {
|
||||||
if (content_type_all != previous_content_type) {
|
if (content_type_all != previous_content_type) {
|
||||||
var contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(filename).extension)
|
var contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(filename).extension)
|
||||||
@ -42,26 +46,23 @@ object ExternalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun share(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) {
|
fun share(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) {
|
||||||
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) {
|
var contentType: String? = null
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
val uris = ArrayList<Uri>(file_paths.size)
|
||||||
var contentType: String? = null
|
object : LoadingTask<String?>(activity, themeValue, R.string.loading_msg_export) {
|
||||||
val uris = ArrayList<Uri>()
|
override suspend fun doTask(): String? {
|
||||||
for (path in file_paths) {
|
for (path in file_paths) {
|
||||||
val result = exportFile(activity, gocryptfsVolume, path, contentType)
|
val result = exportFile(activity, gocryptfsVolume, path, contentType)
|
||||||
contentType = if (result.first != null) {
|
contentType = if (result.first != null) {
|
||||||
uris.add(result.first!!)
|
uris.add(result.first!!)
|
||||||
result.second
|
result.second
|
||||||
} else {
|
} else {
|
||||||
stopTask {
|
return path
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(activity.getString(R.string.export_failed, path))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}.startTask(activity.lifecycleScope) { failedItem ->
|
||||||
|
if (failedItem == null) {
|
||||||
val shareIntent = Intent()
|
val shareIntent = Intent()
|
||||||
shareIntent.type = contentType
|
shareIntent.type = contentType
|
||||||
if (uris.size == 1) {
|
if (uris.size == 1) {
|
||||||
@ -71,45 +72,51 @@ object ExternalProvider {
|
|||||||
shareIntent.action = Intent.ACTION_SEND_MULTIPLE
|
shareIntent.action = Intent.ACTION_SEND_MULTIPLE
|
||||||
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
|
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
|
||||||
}
|
}
|
||||||
stopTask {
|
activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_chooser)))
|
||||||
activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_chooser)))
|
} else {
|
||||||
}
|
CustomAlertDialogBuilder(activity, themeValue)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(activity.getString(R.string.export_failed, failedItem))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
|
fun open(activity: AppCompatActivity, themeValue: String, gocryptfsVolume: GocryptfsVolume, file_path: String) {
|
||||||
object : LoadingTask(activity, themeValue, R.string.loading_msg_export) {
|
object : LoadingTask<Intent?>(activity, themeValue, R.string.loading_msg_export) {
|
||||||
override fun doTask(activity: AppCompatActivity) {
|
override suspend fun doTask(): Intent? {
|
||||||
val result = exportFile(activity, gocryptfsVolume, file_path, null)
|
val result = exportFile(activity, gocryptfsVolume, file_path, null)
|
||||||
if (result.first != null) {
|
return if (result.first != null) {
|
||||||
val openIntent = Intent(Intent.ACTION_VIEW)
|
Intent(Intent.ACTION_VIEW).apply {
|
||||||
openIntent.setDataAndType(result.first, result.second)
|
setDataAndType(result.first, result.second)
|
||||||
stopTask { activity.startActivity(openIntent) }
|
|
||||||
} else {
|
|
||||||
stopTask {
|
|
||||||
CustomAlertDialogBuilder(activity, themeValue)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(activity.getString(R.string.export_failed, file_path))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.startTask(activity.lifecycleScope) { openIntent ->
|
||||||
|
if (openIntent == null) {
|
||||||
|
CustomAlertDialogBuilder(activity, themeValue)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(activity.getString(R.string.export_failed, file_path))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
activity.startActivity(openIntent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFiles(context: Context) {
|
fun removeFilesAsync(context: Context) = GlobalScope.launch(Dispatchers.IO) {
|
||||||
Thread{
|
val success = HashSet<Uri>(storedFiles.size)
|
||||||
val success = ArrayList<Uri>()
|
for (uri in storedFiles) {
|
||||||
for (uri in storedFiles){
|
if (context.contentResolver.delete(uri, null, null) == 1) {
|
||||||
if (context.contentResolver.delete(uri, null, null) == 1){
|
success.add(uri)
|
||||||
success.add(uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (uri in success){
|
}
|
||||||
storedFiles.remove(uri)
|
for (uri in success) {
|
||||||
}
|
storedFiles.remove(uri)
|
||||||
}.start()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,13 +14,16 @@ import android.widget.ImageButton
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import sushi.hardcore.droidfs.BaseActivity
|
import sushi.hardcore.droidfs.BaseActivity
|
||||||
import sushi.hardcore.droidfs.ConstValues
|
import sushi.hardcore.droidfs.ConstValues
|
||||||
import sushi.hardcore.droidfs.ConstValues.isAudio
|
import sushi.hardcore.droidfs.ConstValues.isAudio
|
||||||
@ -312,21 +315,21 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile })
|
displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile })
|
||||||
displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory })
|
displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory })
|
||||||
if (mapFolders) {
|
if (mapFolders) {
|
||||||
Thread {
|
lifecycleScope.launch {
|
||||||
var totalSize: Long = 0
|
var totalSize: Long = 0
|
||||||
synchronized(this) {
|
withContext(Dispatchers.IO) {
|
||||||
for (element in explorerElements) {
|
synchronized(this@BaseExplorerActivity) {
|
||||||
if (element.isDirectory) {
|
for (element in explorerElements) {
|
||||||
recursiveSetSize(element)
|
if (element.isDirectory) {
|
||||||
|
recursiveSetSize(element)
|
||||||
|
}
|
||||||
|
totalSize += element.size
|
||||||
}
|
}
|
||||||
totalSize += element.size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
displayExplorerElements(totalSize)
|
||||||
displayExplorerElements(totalSize)
|
onDisplayed?.invoke()
|
||||||
onDisplayed?.invoke()
|
}
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
} else {
|
} else {
|
||||||
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size })
|
displayExplorerElements(explorerElements.filter { !it.isParentFolder }.sumOf { it.size })
|
||||||
onDisplayed?.invoke()
|
onDisplayed?.invoke()
|
||||||
@ -455,9 +458,12 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
if (items.size > 0) {
|
if (items.size > 0) {
|
||||||
checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
|
checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
|
||||||
checkedItems?.let {
|
checkedItems?.let {
|
||||||
fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris){ failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val taskResult = fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris)
|
||||||
callback(failedItem)
|
if (taskResult.cancelled) {
|
||||||
|
setCurrentPath(currentDirectoryPath)
|
||||||
|
} else {
|
||||||
|
callback(taskResult.failedItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,20 +471,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importDirectory(sourceUri: Uri, callback: (String?, List<Uri>, DocumentFile) -> Unit) {
|
|
||||||
val tree = DocumentFile.fromTreeUri(this, sourceUri)!! //non-null after Lollipop
|
|
||||||
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
|
|
||||||
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
|
|
||||||
checkedOperation?.let {
|
|
||||||
fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) { failedItem, uris ->
|
|
||||||
runOnUiThread {
|
|
||||||
callback(failedItem, uris, tree)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun rename(old_name: String, new_name: String){
|
protected fun rename(old_name: String, new_name: String){
|
||||||
if (new_name.isEmpty()) {
|
if (new_name.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
|
||||||
@ -627,7 +619,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
isStartingActivity = false
|
isStartingActivity = false
|
||||||
ExternalProvider.removeFiles(this)
|
ExternalProvider.removeFilesAsync(this)
|
||||||
setCurrentPath(currentDirectoryPath)
|
setCurrentPath(currentDirectoryPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import android.view.MenuItem
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import sushi.hardcore.droidfs.CameraActivity
|
import sushi.hardcore.droidfs.CameraActivity
|
||||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||||
import sushi.hardcore.droidfs.MainActivity
|
import sushi.hardcore.droidfs.MainActivity
|
||||||
@ -67,19 +69,18 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
if (items == null) {
|
if (items == null) {
|
||||||
remoteGocryptfsVolume.close()
|
remoteGocryptfsVolume.close()
|
||||||
} else {
|
} else {
|
||||||
fileOperationService.copyElements(items, remoteGocryptfsVolume){ failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val failedItem = fileOperationService.copyElements(items, remoteGocryptfsVolume)
|
||||||
if (failedItem == null){
|
if (failedItem == null) {
|
||||||
Toast.makeText(this, R.string.success_import, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.import_failed, failedItem))
|
.setMessage(getString(R.string.import_failed, failedItem))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
|
||||||
setCurrentPath(currentDirectoryPath)
|
|
||||||
}
|
}
|
||||||
|
setCurrentPath(currentDirectoryPath)
|
||||||
remoteGocryptfsVolume.close()
|
remoteGocryptfsVolume.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,14 +100,15 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
}
|
}
|
||||||
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val result = fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] })
|
||||||
if (failedItem == null){
|
if (!result.cancelled) {
|
||||||
Toast.makeText(this, R.string.success_export, Toast.LENGTH_SHORT).show()
|
if (result.failedItem == null) {
|
||||||
|
Toast.makeText(this@ExplorerActivity, R.string.success_export, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.export_failed, failedItem))
|
.setMessage(getString(R.string.export_failed, result.failedItem))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@ -117,7 +119,20 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
}
|
}
|
||||||
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
|
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
|
||||||
rootUri?.let {
|
rootUri?.let {
|
||||||
importDirectory(it, ::onImportComplete)
|
val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop
|
||||||
|
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
|
||||||
|
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
|
||||||
|
checkedOperation?.let {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree)
|
||||||
|
if (result.taskResult.cancelled) {
|
||||||
|
setCurrentPath(currentDirectoryPath)
|
||||||
|
} else {
|
||||||
|
onImportComplete(result.taskResult.failedItem, result.uris, tree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,17 +145,16 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
${getString(R.string.ask_for_wipe)}
|
${getString(R.string.ask_for_wipe)}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
fileOperationService.wipeUris(urisToWipe, rootFile) { errorMsg ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile)
|
||||||
if (errorMsg == null){
|
if (errorMsg == null) {
|
||||||
Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.wipe_failed, errorMsg))
|
.setMessage(getString(R.string.wipe_failed, errorMsg))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,16 +324,17 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
if (currentItemAction == ItemsActions.COPY){
|
if (currentItemAction == ItemsActions.COPY){
|
||||||
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
||||||
items?.let {
|
items?.let {
|
||||||
fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val failedItem = fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>)
|
||||||
if (failedItem == null){
|
if (!isFinishing) {
|
||||||
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
if (failedItem == null) {
|
||||||
|
Toast.makeText(this@ExplorerActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.copy_failed, failedItem))
|
.setMessage(getString(R.string.copy_failed, failedItem))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
setCurrentPath(currentDirectoryPath)
|
setCurrentPath(currentDirectoryPath)
|
||||||
}
|
}
|
||||||
@ -332,19 +347,18 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath)
|
mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath)
|
||||||
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
|
||||||
items?.let {
|
items?.let {
|
||||||
fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
val failedItem = fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>)
|
||||||
if (failedItem == null){
|
if (failedItem == null) {
|
||||||
Toast.makeText(this, R.string.move_success, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
CustomAlertDialogBuilder(this, themeValue)
|
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
|
||||||
.setTitle(R.string.error)
|
.setTitle(R.string.error)
|
||||||
.setMessage(getString(R.string.move_failed, failedItem))
|
.setMessage(getString(R.string.move_failed, failedItem))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
|
||||||
setCurrentPath(currentDirectoryPath)
|
|
||||||
}
|
}
|
||||||
|
setCurrentPath(currentDirectoryPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancelItemAction()
|
cancelItemAction()
|
||||||
|
@ -13,6 +13,7 @@ import android.os.IBinder
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||||
import sushi.hardcore.droidfs.R
|
import sushi.hardcore.droidfs.R
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
||||||
@ -30,7 +31,7 @@ class FileOperationService : Service() {
|
|||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
private lateinit var gocryptfsVolume: GocryptfsVolume
|
private lateinit var gocryptfsVolume: GocryptfsVolume
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
private var notifications = HashMap<Int, Boolean>()
|
private val tasks = HashMap<Int, Job>()
|
||||||
private var lastNotificationId = 0
|
private var lastNotificationId = 0
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
@ -88,7 +89,6 @@ class FileOperationService : Service() {
|
|||||||
.setContentText(getString(R.string.discovering_files))
|
.setContentText(getString(R.string.discovering_files))
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
}
|
}
|
||||||
notifications[lastNotificationId] = false
|
|
||||||
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
||||||
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
||||||
}
|
}
|
||||||
@ -105,7 +105,20 @@ class FileOperationService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelOperation(notificationId: Int){
|
fun cancelOperation(notificationId: Int){
|
||||||
notifications[notificationId] = true
|
tasks[notificationId]?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
open class TaskResult<T>(val cancelled: Boolean, val failedItem: T?)
|
||||||
|
|
||||||
|
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<T> {
|
||||||
|
tasks[notification.notificationId] = task
|
||||||
|
return try {
|
||||||
|
TaskResult(false, task.await())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
TaskResult(true, null)
|
||||||
|
} finally {
|
||||||
|
cancelNotification(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
|
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
|
||||||
@ -137,110 +150,105 @@ class FileOperationService : Service() {
|
|||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyElements(items: ArrayList<OperationFile>, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){
|
suspend fun copyElements(
|
||||||
Thread {
|
items: ArrayList<OperationFile>,
|
||||||
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
|
||||||
|
): String? = coroutineScope {
|
||||||
|
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
||||||
|
val task = async {
|
||||||
var failedItem: String? = null
|
var failedItem: String? = null
|
||||||
for (i in 0 until items.size){
|
for (i in 0 until items.size) {
|
||||||
if (notifications[notification.notificationId]!!){
|
withContext(Dispatchers.IO) {
|
||||||
cancelNotification(notification)
|
if (items[i].explorerElement.isDirectory) {
|
||||||
return@Thread
|
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
||||||
}
|
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
||||||
if (items[i].explorerElement.isDirectory){
|
failedItem = items[i].explorerElement.fullPath
|
||||||
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
}
|
||||||
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
}
|
||||||
|
} else {
|
||||||
|
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
|
||||||
failedItem = items[i].explorerElement.fullPath
|
failedItem = items[i].explorerElement.fullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){
|
|
||||||
failedItem = items[i].explorerElement.fullPath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (failedItem == null){
|
if (failedItem == null) {
|
||||||
updateNotificationProgress(notification, i, items.size)
|
updateNotificationProgress(notification, i+1, items.size)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cancelNotification(notification)
|
failedItem
|
||||||
callback(failedItem)
|
}
|
||||||
}.start()
|
// treat cancellation as success
|
||||||
|
waitForTask(notification, task).failedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveElements(items: ArrayList<OperationFile>, callback: (String?) -> Unit){
|
suspend fun moveElements(items: ArrayList<OperationFile>): String? = coroutineScope {
|
||||||
Thread {
|
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
||||||
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
val task = async {
|
||||||
val mergedFolders = ArrayList<String>()
|
|
||||||
var failedItem: String? = null
|
var failedItem: String? = null
|
||||||
for (i in 0 until items.size){
|
withContext(Dispatchers.IO) {
|
||||||
if (notifications[notification.notificationId]!!){
|
val mergedFolders = ArrayList<String>()
|
||||||
cancelNotification(notification)
|
for (i in 0 until items.size) {
|
||||||
return@Thread
|
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)) { //folder will be merged
|
||||||
}
|
mergedFolders.add(items[i].explorerElement.fullPath)
|
||||||
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)){ //folder will be merged
|
|
||||||
mergedFolders.add(items[i].explorerElement.fullPath)
|
|
||||||
} else {
|
|
||||||
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)){
|
|
||||||
failedItem = items[i].explorerElement.fullPath
|
|
||||||
break
|
|
||||||
} else {
|
} else {
|
||||||
updateNotificationProgress(notification, i, items.size)
|
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)) {
|
||||||
|
failedItem = items[i].explorerElement.fullPath
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
updateNotificationProgress(notification, i+1, items.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failedItem == null) {
|
||||||
|
for (i in 0 until mergedFolders.size) {
|
||||||
|
if (!gocryptfsVolume.rmdir(mergedFolders[i])) {
|
||||||
|
failedItem = mergedFolders[i]
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failedItem == null){
|
failedItem
|
||||||
for (i in 0 until mergedFolders.size) {
|
}
|
||||||
if (notifications[notification.notificationId]!!){
|
// treat cancellation as success
|
||||||
cancelNotification(notification)
|
waitForTask(notification, task).failedItem
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
if (!gocryptfsVolume.rmdir(mergedFolders[i])){
|
|
||||||
failedItem = mergedFolders[i]
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelNotification(notification)
|
|
||||||
callback(failedItem)
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, reuseNotification: FileOperationNotification? = null, callback: (String?) -> Unit){
|
private suspend fun importFilesFromUris(
|
||||||
val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size)
|
dstPaths: List<String>,
|
||||||
|
uris: List<Uri>,
|
||||||
|
notification: FileOperationNotification,
|
||||||
|
): String? {
|
||||||
var failedIndex = -1
|
var failedIndex = -1
|
||||||
for (i in dstPaths.indices) {
|
for (i in dstPaths.indices) {
|
||||||
if (notifications[notification.notificationId]!!){
|
withContext(Dispatchers.IO) {
|
||||||
cancelNotification(notification)
|
try {
|
||||||
return
|
if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
|
||||||
}
|
failedIndex = i
|
||||||
try {
|
}
|
||||||
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) {
|
} catch (e: FileNotFoundException) {
|
||||||
failedIndex = i
|
failedIndex = i
|
||||||
}
|
}
|
||||||
} catch (e: FileNotFoundException){
|
|
||||||
failedIndex = i
|
|
||||||
}
|
}
|
||||||
if (failedIndex == -1) {
|
if (failedIndex == -1) {
|
||||||
updateNotificationProgress(notification, i, dstPaths.size)
|
updateNotificationProgress(notification, i+1, dstPaths.size)
|
||||||
} else {
|
} else {
|
||||||
cancelNotification(notification)
|
return uris[failedIndex].toString()
|
||||||
callback(uris[failedIndex].toString())
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failedIndex == -1){
|
return null
|
||||||
cancelNotification(notification)
|
|
||||||
callback(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) {
|
suspend fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>): TaskResult<String?> = coroutineScope {
|
||||||
Thread {
|
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
|
||||||
importFilesFromUris(dstPaths, uris, null, callback)
|
val task = async {
|
||||||
}.start()
|
importFilesFromUris(dstPaths, uris, notification)
|
||||||
|
}
|
||||||
|
waitForTask(notification, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -256,18 +264,17 @@ class FileOperationService : Service() {
|
|||||||
dstFiles: ArrayList<String>,
|
dstFiles: ArrayList<String>,
|
||||||
srcUris: ArrayList<Uri>,
|
srcUris: ArrayList<Uri>,
|
||||||
dstDirs: ArrayList<String>,
|
dstDirs: ArrayList<String>,
|
||||||
notification: FileOperationNotification
|
scope: CoroutineScope,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
dstDirs.add(rootDstPath)
|
dstDirs.add(rootDstPath)
|
||||||
for (child in rootSrcDir.listFiles()) {
|
for (child in rootSrcDir.listFiles()) {
|
||||||
if (notifications[notification.notificationId]!!) {
|
if (!scope.isActive) {
|
||||||
cancelNotification(notification)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
child.name?.let { name ->
|
child.name?.let { name ->
|
||||||
val subPath = PathUtils.pathJoin(rootDstPath, name)
|
val subPath = PathUtils.pathJoin(rootDstPath, name)
|
||||||
if (child.isDirectory) {
|
if (child.isDirectory) {
|
||||||
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) {
|
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,48 +287,50 @@ class FileOperationService : Service() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List<Uri>) -> Unit) {
|
class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>)
|
||||||
Thread {
|
|
||||||
val notification = showNotification(R.string.file_op_import_msg, null)
|
|
||||||
|
|
||||||
|
suspend fun importDirectory(
|
||||||
|
rootDstPath: String,
|
||||||
|
rootSrcDir: DocumentFile,
|
||||||
|
): ImportDirectoryResult = coroutineScope {
|
||||||
|
val notification = showNotification(R.string.file_op_import_msg, null)
|
||||||
|
val srcUris = arrayListOf<Uri>()
|
||||||
|
val task = async {
|
||||||
|
var failedItem: String? = null
|
||||||
val dstFiles = arrayListOf<String>()
|
val dstFiles = arrayListOf<String>()
|
||||||
val srcUris = arrayListOf<Uri>()
|
|
||||||
val dstDirs = arrayListOf<String>()
|
val dstDirs = arrayListOf<String>()
|
||||||
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
// create destination folders so the new files can use them
|
withContext(Dispatchers.IO) {
|
||||||
for (dir in dstDirs) {
|
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
|
||||||
if (notifications[notification.notificationId]!!) {
|
return@withContext
|
||||||
cancelNotification(notification)
|
|
||||||
return@Thread
|
|
||||||
}
|
}
|
||||||
if (!gocryptfsVolume.mkdir(dir)) {
|
|
||||||
cancelNotification(notification)
|
// create destination folders so the new files can use them
|
||||||
callback(dir, srcUris)
|
for (dir in dstDirs) {
|
||||||
break
|
if (!gocryptfsVolume.mkdir(dir)) {
|
||||||
|
failedItem = dir
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (failedItem == null) {
|
||||||
importFilesFromUris(dstFiles, srcUris, notification) { failedItem ->
|
failedItem = importFilesFromUris(dstFiles, srcUris, notification)
|
||||||
callback(failedItem, srcUris)
|
|
||||||
}
|
}
|
||||||
}.start()
|
failedItem
|
||||||
|
}
|
||||||
|
ImportDirectoryResult(waitForTask(notification, task), srcUris)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null, callback: (String?) -> Unit){
|
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): String? = coroutineScope {
|
||||||
Thread {
|
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
||||||
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
val task = async {
|
||||||
var errorMsg: String? = null
|
var errorMsg: String? = null
|
||||||
for (i in uris.indices) {
|
for (i in uris.indices) {
|
||||||
if (notifications[notification.notificationId]!!){
|
withContext(Dispatchers.IO) {
|
||||||
cancelNotification(notification)
|
errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
|
||||||
return@Thread
|
|
||||||
}
|
}
|
||||||
errorMsg = Wiper.wipe(this, uris[i])
|
|
||||||
if (errorMsg == null) {
|
if (errorMsg == null) {
|
||||||
updateNotificationProgress(notification, i, uris.size)
|
updateNotificationProgress(notification, i+1, uris.size)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -329,9 +338,10 @@ class FileOperationService : Service() {
|
|||||||
if (errorMsg == null) {
|
if (errorMsg == null) {
|
||||||
rootFile?.delete()
|
rootFile?.delete()
|
||||||
}
|
}
|
||||||
cancelNotification(notification)
|
errorMsg
|
||||||
callback(errorMsg)
|
}
|
||||||
}.start()
|
// treat cancellation as success
|
||||||
|
waitForTask(notification, task).failedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
|
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
|
||||||
@ -345,13 +355,20 @@ class FileOperationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? {
|
private fun recursiveExportDirectory(
|
||||||
|
plain_directory_path: String,
|
||||||
|
treeDocumentFile: DocumentFile,
|
||||||
|
scope: CoroutineScope
|
||||||
|
): String? {
|
||||||
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
|
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
|
||||||
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
|
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
|
||||||
for (e in explorerElements) {
|
for (e in explorerElements) {
|
||||||
|
if (!scope.isActive) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
|
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
|
||||||
if (e.isDirectory) {
|
if (e.isDirectory) {
|
||||||
val failedItem = recursiveExportDirectory(fullPath, childTree)
|
val failedItem = recursiveExportDirectory(fullPath, childTree, scope)
|
||||||
failedItem?.let { return it }
|
failedItem?.let { return it }
|
||||||
} else {
|
} else {
|
||||||
if (!exportFileInto(fullPath, childTree)){
|
if (!exportFileInto(fullPath, childTree)){
|
||||||
@ -364,40 +381,40 @@ class FileOperationService : Service() {
|
|||||||
return treeDocumentFile.name
|
return treeDocumentFile.name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportFiles(uri: Uri, items: List<ExplorerElement>, callback: (String?) -> Unit){
|
suspend fun exportFiles(uri: Uri, items: List<ExplorerElement>): TaskResult<String?> = coroutineScope {
|
||||||
Thread {
|
val notification = showNotification(R.string.file_op_export_msg, items.size)
|
||||||
|
val task = async {
|
||||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile ->
|
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
|
||||||
val notification = showNotification(R.string.file_op_export_msg, items.size)
|
var failedItem: String? = null
|
||||||
var failedItem: String? = null
|
for (i in items.indices) {
|
||||||
for (i in items.indices) {
|
withContext(Dispatchers.IO) {
|
||||||
if (notifications[notification.notificationId]!!){
|
|
||||||
cancelNotification(notification)
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
failedItem = if (items[i].isDirectory) {
|
failedItem = if (items[i].isDirectory) {
|
||||||
recursiveExportDirectory(items[i].fullPath, treeDocumentFile)
|
recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this)
|
||||||
} else {
|
} else {
|
||||||
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
|
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
|
||||||
}
|
}
|
||||||
if (failedItem == null) {
|
|
||||||
updateNotificationProgress(notification, i, items.size)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cancelNotification(notification)
|
if (failedItem == null) {
|
||||||
callback(failedItem)
|
updateNotificationProgress(notification, i+1, items.size)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
failedItem
|
||||||
|
}
|
||||||
|
waitForTask(notification, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursiveCountChildElements(rootDirectory: DocumentFile): Int {
|
private fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
|
||||||
|
if (!scope.isActive) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
val children = rootDirectory.listFiles()
|
val children = rootDirectory.listFiles()
|
||||||
var count = children.size
|
var count = children.size
|
||||||
for (child in children) {
|
for (child in children) {
|
||||||
if (child.isDirectory) {
|
if (child.isDirectory) {
|
||||||
count += recursiveCountChildElements(child)
|
count += recursiveCountChildElements(child, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@ -411,12 +428,13 @@ class FileOperationService : Service() {
|
|||||||
dstRootDirectory: ObjRef<DocumentFile?>?,
|
dstRootDirectory: ObjRef<DocumentFile?>?,
|
||||||
notification: FileOperationNotification,
|
notification: FileOperationNotification,
|
||||||
total: Int,
|
total: Int,
|
||||||
|
scope: CoroutineScope,
|
||||||
progress: ObjRef<Int> = ObjRef(0)
|
progress: ObjRef<Int> = ObjRef(0)
|
||||||
): DocumentFile? {
|
): DocumentFile? {
|
||||||
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
|
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
|
||||||
|
dstRootDirectory?.let { it.value = dstDir }
|
||||||
for (child in src.listFiles()) {
|
for (child in src.listFiles()) {
|
||||||
if (notifications[notification.notificationId]!!) {
|
if (!scope.isActive) {
|
||||||
cancelNotification(notification)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (child.isFile) {
|
if (child.isFile) {
|
||||||
@ -429,24 +447,29 @@ class FileOperationService : Service() {
|
|||||||
inputStream.close()
|
inputStream.close()
|
||||||
if (written != child.length()) return child
|
if (written != child.length()) return child
|
||||||
} else {
|
} else {
|
||||||
recursiveCopyVolume(child, dstDir, null, notification, total, progress)?.let { return it }
|
recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
|
||||||
}
|
}
|
||||||
progress.value++
|
progress.value++
|
||||||
updateNotificationProgress(notification, progress.value, total)
|
updateNotificationProgress(notification, progress.value, total)
|
||||||
}
|
}
|
||||||
dstRootDirectory?.let { it.value = dstDir }
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyVolume(src: DocumentFile, dst: DocumentFile, callback: (DocumentFile?, DocumentFile?) -> Unit) {
|
class CopyVolumeResult(val taskResult: TaskResult<DocumentFile?>, val dstRootDirectory: DocumentFile?)
|
||||||
Thread {
|
|
||||||
val notification = showNotification(R.string.copy_volume_notification, null)
|
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope {
|
||||||
val total = recursiveCountChildElements(src)
|
val notification = showNotification(R.string.copy_volume_notification, null)
|
||||||
updateNotificationProgress(notification, 0, total)
|
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
||||||
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
val task = async(Dispatchers.IO) {
|
||||||
val failedItem = recursiveCopyVolume(src, dst, dstRootDirectory, notification, total)
|
val total = recursiveCountChildElements(src, this)
|
||||||
cancelNotification(notification)
|
if (isActive) {
|
||||||
callback(dstRootDirectory.value, failedItem)
|
updateNotificationProgress(notification, 0, total)
|
||||||
}.start()
|
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// treat cancellation as success
|
||||||
|
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user