Browse Source

FileOperationService

master
Hardcore Sushi 4 months ago
parent
commit
1a6c1d2901
Signed by: hardcoresushi GPG Key ID: 007F84120107191E
  1. 2
      app/src/main/AndroidManifest.xml
  2. 194
      app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt
  3. 152
      app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
  4. 589
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
  5. 89
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt
  6. 4
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt
  7. 2
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt
  8. 11
      app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt
  9. 20
      app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt
  10. 12
      app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt
  11. 6
      app/src/main/res/values/strings.xml
  12. 3
      gradle/wrapper/gradle-wrapper.properties

2
app/src/main/AndroidManifest.xml

@ -86,6 +86,8 @@
android:name=".file_viewers.TextEditor"
android:configChanges="screenSize|orientation" />
<service android:name=".FileOperationService" android:exported="false"/>
<provider
android:name=".provider.RestrictedFileProvider"
android:authorities="${applicationId}.temporary_provider"

194
app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt

@ -0,0 +1,194 @@
package sushi.hardcore.droidfs
import android.app.Service
import android.content.Intent
import android.net.Uri
import android.os.*
import androidx.documentfile.provider.DocumentFile
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.io.FileNotFoundException
class FileOperationService : Service() {
private val binder = LocalBinder()
private lateinit var gocryptfsVolume: GocryptfsVolume
inner class LocalBinder : Binder() {
fun getService(): FileOperationService = this@FileOperationService
fun setGocryptfsVolume(g: GocryptfsVolume) {
gocryptfsVolume = g
}
}
override fun onBind(p0: Intent?): IBinder {
return binder
}
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
var success = true
val srcHandleId = remoteGocryptfsVolume.openReadMode(srcPath)
if (srcHandleId != -1){
val dstHandleId = gocryptfsVolume.openWriteMode(dstPath)
if (dstHandleId != -1){
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (remoteGocryptfsVolume.readFile(srcHandleId, offset, ioBuffer).also { length = it } > 0) {
val written = gocryptfsVolume.writeFile(dstHandleId, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
success = false
break
}
}
gocryptfsVolume.closeFile(dstHandleId)
} else {
success = false
}
remoteGocryptfsVolume.closeFile(srcHandleId)
} else {
success = false
}
return success
}
fun copyElements(items: ArrayList<OperationFile>, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){
Thread {
var failedItem: String? = null
for (item in items){
if (item.explorerElement.isDirectory){
if (!gocryptfsVolume.pathExists(item.dstPath!!)) {
if (!gocryptfsVolume.mkdir(item.dstPath!!)) {
failedItem = item.explorerElement.fullPath
}
}
} else {
if (!copyFile(item.explorerElement.fullPath, item.dstPath!!, remoteGocryptfsVolume)){
failedItem = item.explorerElement.fullPath
}
}
if (failedItem != null){
break
}
}
callback(failedItem)
}.start()
}
fun moveElements(items: ArrayList<OperationFile>, callback: (String?) -> Unit){
Thread {
val mergedFolders = ArrayList<String>()
var failedItem: String? = null
for (item in items){
if (item.explorerElement.isDirectory && gocryptfsVolume.pathExists(item.dstPath!!)){ //folder will be merged
mergedFolders.add(item.explorerElement.fullPath)
} else {
if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)){
failedItem = item.explorerElement.fullPath
break
}
}
}
if (failedItem == null){
for (path in mergedFolders) {
if (!gocryptfsVolume.rmdir(path)){
failedItem = path
break
}
}
}
callback(failedItem)
}.start()
}
fun importFilesFromUris(items: ArrayList<OperationFile>, uris: List<Uri>, callback: (String?) -> Unit){
Thread {
var failedIndex = -1
for (i in 0 until items.size) {
try {
if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){
failedIndex = i
}
} catch (e: FileNotFoundException){
failedIndex = i
}
if (failedIndex != -1){
callback(uris[failedIndex].toString())
break
}
}
if (failedIndex == -1){
callback(null)
}
}.start()
}
fun wipeUris(uris: List<Uri>, callback: (String?) -> Unit){
Thread {
var errorMsg: String? = null
for (uri in uris) {
errorMsg = Wiper.wipe(this, uri)
if (errorMsg != null) {
break
}
}
callback(errorMsg)
}.start()
}
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let {
contentResolver.openOutputStream(it)
}
return if (outputStream != null){
gocryptfsVolume.exportFile(srcPath, outputStream)
} else {
false
}
}
private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? {
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val failedItem = recursiveExportDirectory(fullPath, childTree)
failedItem?.let { return it }
} else {
if (!exportFileInto(fullPath, childTree)){
return fullPath
}
}
}
return null
}
return treeDocumentFile.name
}
fun exportFiles(uri: Uri, items: List<ExplorerElement>, callback: (String?) -> Unit){
Thread {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile ->
var failedItem: String? = null
for (element in items) {
failedItem = if (element.isDirectory) {
recursiveExportDirectory(element.fullPath, treeDocumentFile)
} else {
if (exportFileInto(element.fullPath, treeDocumentFile)) null else element.fullPath
}
if (failedItem != null) {
break
}
}
callback(failedItem)
}
}.start()
}
}

152
app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt

@ -1,12 +1,12 @@
package sushi.hardcore.droidfs.explorers
import android.content.DialogInterface
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -26,10 +26,12 @@ import sushi.hardcore.droidfs.ConstValues.Companion.isAudio
import sushi.hardcore.droidfs.ConstValues.Companion.isImage
import sushi.hardcore.droidfs.ConstValues.Companion.isText
import sushi.hardcore.droidfs.ConstValues.Companion.isVideo
import sushi.hardcore.droidfs.FileOperationService
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_viewers.AudioPlayer
import sushi.hardcore.droidfs.file_viewers.ImageViewer
import sushi.hardcore.droidfs.file_viewers.TextEditor
@ -37,11 +39,8 @@ import sushi.hardcore.droidfs.file_viewers.VideoPlayer
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
import sushi.hardcore.droidfs.util.ExternalProvider
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.LoadingTask
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
import java.io.FileNotFoundException
open class BaseExplorerActivity : BaseActivity() {
private lateinit var sortOrderEntries: Array<String>
@ -55,6 +54,7 @@ open class BaseExplorerActivity : BaseActivity() {
field = value
explorerViewModel.currentDirectoryPath = value
}
protected lateinit var fileOperationService: FileOperationService
protected lateinit var explorerElements: MutableList<ExplorerElement>
protected lateinit var explorerAdapter: ExplorerElementAdapter
private var isCreating = true
@ -87,6 +87,7 @@ open class BaseExplorerActivity : BaseActivity() {
setCurrentPath(currentDirectoryPath)
refresher.isRefreshing = false
}
bindFileOperationService()
}
class ExplorerViewModel: ViewModel() {
@ -97,6 +98,21 @@ open class BaseExplorerActivity : BaseActivity() {
setContentView(R.layout.activity_explorer_base)
}
protected open fun bindFileOperationService(){
Intent(this, FileOperationService::class.java).also {
bindService(it, object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FileOperationService.LocalBinder
fileOperationService = binder.getService()
binder.setGocryptfsVolume(gocryptfsVolume)
}
override fun onServiceDisconnected(arg0: ComponentName) {
}
}, Context.BIND_AUTO_CREATE)
}
}
private fun startFileViewer(cls: Class<*>, filePath: String, sortOrder: String = ""){
val intent = Intent(this, cls)
intent.putExtra("path", filePath)
@ -274,97 +290,97 @@ open class BaseExplorerActivity : BaseActivity() {
dialog.show()
}
protected fun checkPathOverwrite(path: String, isDirectory: Boolean): String? {
var outputPath: String? = null
if (gocryptfsVolume.pathExists(path)){
val fileName = File(path).name
val handler = Handler{ msg ->
outputPath = msg.obj as String?
throw RuntimeException()
protected fun checkPathOverwrite(items: ArrayList<OperationFile>, dstDirectoryPath: String, callback: (ArrayList<OperationFile>?) -> Unit) {
val srcDirectoryPath = items[0].explorerElement.parentPath
var ready = true
for (i in 0 until items.size) {
val testDstPath: String
if (items[i].dstPath == null){
testDstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.fullPath))
if (gocryptfsVolume.pathExists(testDstPath)){
ready = false
} else {
items[i].dstPath = testDstPath
}
} else {
testDstPath = items[i].dstPath!!
if (gocryptfsVolume.pathExists(testDstPath) && !items[i].overwriteConfirmed){
ready = false
}
}
runOnUiThread {
val dialog = ColoredAlertDialogBuilder(this)
if (!ready){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(getString(if (isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, path))
.setMessage(getString(if (items[i].explorerElement.isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, testDstPath))
.setPositiveButton(R.string.yes) {_, _ ->
items[i].dstPath = testDstPath
items[i].overwriteConfirmed = true
checkPathOverwrite(items, dstDirectoryPath, callback)
}
.setNegativeButton(R.string.no) { _, _ ->
val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null)
val dialogEditText = dialogEditTextView.findViewById<EditText>(R.id.dialog_edit_text)
dialogEditText.setText(fileName)
dialogEditText.setText(items[i].explorerElement.name)
dialogEditText.selectAll()
val dialog = ColoredAlertDialogBuilder(this)
.setView(dialogEditTextView)
.setTitle(R.string.enter_new_name)
.setPositiveButton(R.string.ok) { _, _ ->
handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.pathJoin(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) })
}
.setNegativeButton(R.string.cancel) { _, _ -> handler.sendMessage(Message().apply { obj = null }) }
.create()
.setView(dialogEditTextView)
.setTitle(R.string.enter_new_name)
.setPositiveButton(R.string.ok) { _, _ ->
items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.parentPath), dialogEditText.text.toString())
checkPathOverwrite(items, dstDirectoryPath, callback)
}
.setOnCancelListener{
callback(null)
}
.create()
dialogEditText.setOnEditorActionListener { _, _, _ ->
dialog.dismiss()
handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.pathJoin(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) })
items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.parentPath), dialogEditText.text.toString())
checkPathOverwrite(items, dstDirectoryPath, callback)
true
}
dialog.setOnCancelListener { handler.sendMessage(Message().apply { obj = null }) }
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
dialog.show()
}
.setPositiveButton(R.string.yes) { _, _ -> handler.sendMessage(Message().apply { obj = path }) }
.create()
dialog.setOnCancelListener { handler.sendMessage(Message().apply { obj = null }) }
dialog.show()
.setOnCancelListener{
callback(null)
}
.show()
break
}
try { Looper.loop() }
catch (e: RuntimeException) {}
} else {
outputPath = path
}
return outputPath
if (ready){
callback(items)
}
}
protected fun importFilesFromUris(uris: List<Uri>, task: LoadingTask, callback: (DialogInterface.OnClickListener)? = null): Boolean {
var success = false
protected fun importFilesFromUris(uris: List<Uri>, callback: (String?) -> Unit) {
val items = ArrayList<OperationFile>()
for (uri in uris) {
val fileName = PathUtils.getFilenameFromURI(task.activity, uri)
if (fileName == null){
task.stopTask {
ColoredAlertDialogBuilder(task.activity)
val fileName = PathUtils.getFilenameFromURI(this, uri)
if (fileName == null) {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.error_retrieving_filename, uri))
.setPositiveButton(R.string.ok, null)
.show()
}
success = false
items.clear()
break
} else {
val dstPath = checkPathOverwrite(PathUtils.pathJoin(currentDirectoryPath, fileName), false)
if (dstPath == null){
break
} else {
var message: String? = null
try {
success = gocryptfsVolume.importFile(task.activity, uri, dstPath)
} catch (e: FileNotFoundException){
message = if (e.message != null){
e.message!!+"\n"
} else {
""
}
}
if (!success || message != null) {
task.stopTask {
ColoredAlertDialogBuilder(task.activity)
.setTitle(R.string.error)
.setMessage((message ?: "")+getString(R.string.import_failed, uri))
.setCancelable(callback == null)
.setPositiveButton(R.string.ok, callback)
.show()
items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, -1, -1, currentDirectoryPath)))
}
}
if (items.size > 0) {
checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
checkedItems?.let {
fileOperationService.importFilesFromUris(checkedItems, uris){ failedItem ->
runOnUiThread {
callback(failedItem)
}
break
}
}
}
}
return success
}
protected fun rename(old_name: String, new_name: String){

589
app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt

@ -3,20 +3,20 @@ package sushi.hardcore.droidfs.explorers
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Looper
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import sushi.hardcore.droidfs.CameraActivity
import sushi.hardcore.droidfs.OpenActivity
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.util.*
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.util.ExternalProvider
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
@ -30,7 +30,7 @@ class ExplorerActivity : BaseExplorerActivity() {
private var usf_decrypt = false
private var usf_share = false
private var currentItemAction = ItemsActions.NONE
private val itemsToProcess = ArrayList<ExplorerElement>()
private val itemsToProcess = ArrayList<OperationFile>()
override fun init() {
setContentView(R.layout.activity_explorer)
usf_decrypt = sharedPrefs.getBoolean("usf_decrypt", false)
@ -47,19 +47,17 @@ class ExplorerActivity : BaseExplorerActivity() {
if (fileName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
checkPathOverwrite(PathUtils.pathJoin(currentDirectoryPath, fileName), false)?.let {
val handleID = gocryptfsVolume.openWriteMode(it)
if (handleID == -1) {
ColoredAlertDialogBuilder(this)
val handleID = gocryptfsVolume.openWriteMode(fileName) //don't check overwrite because openWriteMode open in read-write (doesn't erase content)
if (handleID == -1) {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
gocryptfsVolume.closeFile(handleID)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
} else {
gocryptfsVolume.closeFile(handleID)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
}
}
@ -138,162 +136,137 @@ class ExplorerActivity : BaseExplorerActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == PICK_FILES_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
object : LoadingTask(this, R.string.loading_msg_import){
override fun doTask(activity: AppCompatActivity) {
val uris: MutableList<Uri> = ArrayList()
val singleUri = data.data
if (singleUri == null) { //multiples choices
val clipData = data.clipData
if (clipData != null){
for (i in 0 until clipData.itemCount) {
uris.add(clipData.getItemAt(i).uri)
}
}
} else {
uris.add(singleUri)
val uris: MutableList<Uri> = ArrayList()
val singleUri = data.data
if (singleUri == null) { //multiples choices
val clipData = data.clipData
if (clipData != null){
for (i in 0 until clipData.itemCount) {
uris.add(clipData.getItemAt(i).uri)
}
Looper.prepare()
if (importFilesFromUris(uris, this)) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.success_import)
.setMessage("""
}
} else {
uris.add(singleUri)
}
importFilesFromUris(uris){ failedItem ->
if (failedItem == null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.success_import)
.setMessage("""
${getString(R.string.success_import_msg)}
${getString(R.string.ask_for_wipe)}
""".trimIndent())
.setPositiveButton(R.string.yes) { _, _ ->
object : LoadingTask(activity, R.string.loading_msg_wipe){
override fun doTask(activity: AppCompatActivity) {
var success = true
for (uri in uris) {
val errorMsg = Wiper.wipe(activity, uri)
if (errorMsg != null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.wipe_failed, errorMsg))
.setPositiveButton(R.string.ok, null)
.show()
}
success = false
break
}
}
if (success) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.wipe_successful)
.setMessage(R.string.wipe_success_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
.setPositiveButton(R.string.yes) { _, _ ->
fileOperationService.wipeUris(uris) { errorMsg ->
runOnUiThread {
if (errorMsg == null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.wipe_successful)
.setMessage(R.string.wipe_success_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.wipe_failed, errorMsg))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
.setNegativeButton(R.string.no, null)
.show()
}
}
}
override fun doFinally(activity: AppCompatActivity){
setCurrentPath(currentDirectoryPath)
}
.setNegativeButton(R.string.no, null)
.show()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
} else if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
object : LoadingTask(this, R.string.loading_msg_export){
override fun doTask(activity: AppCompatActivity) {
data.data?.let {uri ->
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
DocumentFile.fromTreeUri(activity, uri)?.let { treeDocumentFile ->
var failedItem: String? = null
for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.getItem(i)
val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name)
failedItem = if (element.isDirectory) {
recursiveExportDirectory(fullPath, treeDocumentFile)
} else {
if (exportFileInto(fullPath, treeDocumentFile)) null else fullPath
}
if (failedItem != null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
break
}
}
if (failedItem == null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.success_export)
.setMessage(R.string.success_export_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
data.data?.let { uri ->
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
runOnUiThread {
if (failedItem == null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.success_export)
.setMessage(R.string.success_export_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
override fun doFinally(activity: AppCompatActivity) {
unselectAll()
}
unselectAll()
}
}
} else if (requestCode == PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
object : LoadingTask(this, R.string.loading_msg_import){
override fun doTask(activity: AppCompatActivity) {
val remoteSessionID = data.getIntExtra("sessionID", -1)
val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID)
val path = data.getStringExtra("path")
var failedItem: String? = null
Looper.prepare()
if (path == null) {
val paths = data.getStringArrayListExtra("paths")
val types = data.getIntegerArrayListExtra("types")
if (types != null && paths != null){
for (i in paths.indices) {
failedItem = if (types[i] == 0) { //directory
recursiveImportDirectoryFromOtherVolume(remoteGocryptfsVolume, paths[i], currentDirectoryPath)
} else {
safeImportFileFromOtherVolume(remoteGocryptfsVolume, paths[i], PathUtils.pathJoin(currentDirectoryPath, File(paths[i]).name))
}
if (failedItem != null) {
break
}
val remoteSessionID = data.getIntExtra("sessionID", -1)
val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID)
val path = data.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements
val paths = data.getStringArrayListExtra("paths")
val types = data.getIntegerArrayListExtra("types")
if (types != null && paths != null){
for (i in paths.indices) {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(paths[i]).name, types[i].toShort(), -1, -1, PathUtils.getParentPath(paths[i]))
)
)
if (types[i] == 0){ //directory
remoteGocryptfsVolume.recursiveMapFiles(paths[i]).forEach {
operationFiles.add(OperationFile.fromExplorerElement(it))
}
}
} else {
failedItem = safeImportFileFromOtherVolume(remoteGocryptfsVolume, path, PathUtils.pathJoin(currentDirectoryPath, File(path).name))
}
if (failedItem == null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.success_import)
.setMessage(R.string.success_import_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} else if (failedItem!!.isNotEmpty()){
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
} else {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(path).name, 1, -1, -1, PathUtils.getParentPath(path))
)
)
}
if (operationFiles.size > 0){
checkPathOverwrite(operationFiles, currentDirectoryPath) { items ->
if (items == null) {
remoteGocryptfsVolume.close()
} else {
fileOperationService.copyElements(items, remoteGocryptfsVolume){ failedItem ->
runOnUiThread {
if (failedItem == null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.success_import)
.setMessage(R.string.success_import_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
remoteGocryptfsVolume.close()
}
}
remoteGocryptfsVolume.close()
}
override fun doFinally(activity: AppCompatActivity) {
setCurrentPath(currentDirectoryPath)
}
} else {
remoteGocryptfsVolume.close()
}
}
}
@ -344,7 +317,7 @@ class ExplorerActivity : BaseExplorerActivity() {
}
R.id.cut -> {
for (i in explorerAdapter.selectedItems){
itemsToProcess.add(explorerElements[i])
itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i]))
}
currentItemAction = ItemsActions.MOVE
unselectAll()
@ -352,7 +325,12 @@ class ExplorerActivity : BaseExplorerActivity() {
}
R.id.copy -> {
for (i in explorerAdapter.selectedItems){
itemsToProcess.add(explorerElements[i])
itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i]))
if (explorerElements[i].isDirectory){
gocryptfsVolume.recursiveMapFiles(explorerElements[i].fullPath).forEach {
itemsToProcess.add(OperationFile.fromExplorerElement(it))
}
}
}
currentItemAction = ItemsActions.COPY
unselectAll()
@ -360,81 +338,45 @@ class ExplorerActivity : BaseExplorerActivity() {
}
R.id.validate -> {
if (currentItemAction == ItemsActions.COPY){
object : LoadingTask(this, R.string.loading_msg_copy){
override fun doTask(activity: AppCompatActivity) {
var failedItem: String? = null
Looper.prepare()
for (element in itemsToProcess) {
val dstPath = checkPathOverwrite(PathUtils.pathJoin(currentDirectoryPath, element.name), element.isDirectory)
failedItem = if (dstPath == null){
""
} else {
if (element.isDirectory) {
recursiveCopyDirectory(element.fullPath, dstPath)
} else {
if (copyFile(element.fullPath, dstPath)) null else element.fullPath
}
}
if (failedItem != null){
if (failedItem.isNotEmpty()) {
stopTask {
ColoredAlertDialogBuilder(activity)
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
items?.let {
fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
runOnUiThread {
if (failedItem != null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(
R.string.copy_failed,
failedItem
))
.setMessage(getString(R.string.copy_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
} else {
Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show()
}
break
}
}
if (failedItem == null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(getString(R.string.copy_success))
.setMessage(getString(R.string.copy_success_msg))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
override fun doFinally(activity: AppCompatActivity) {
cancelItemAction()
unselectAll()
setCurrentPath(currentDirectoryPath)
}
cancelItemAction()
unselectAll()
}
} else if (currentItemAction == ItemsActions.MOVE){
object : LoadingTask(this, R.string.loading_msg_move){
override fun doTask(activity: AppCompatActivity) {
Looper.prepare()
val failedItem = moveElements(itemsToProcess, currentDirectoryPath)
if (failedItem == null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(getString(R.string.move_success))
.setMessage(getString(R.string.move_success_msg))
.setPositiveButton(R.string.ok, null)
.show()
}
} else if (failedItem.isNotEmpty()){
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath)
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
items?.let {
fileOperationService.moveElements(it.toMutableList() as ArrayList<OperationFile>){ failedItem ->
runOnUiThread {
if (failedItem != null){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
} else {
Toast.makeText(this, R.string.move_success, Toast.LENGTH_SHORT).show()
}
}
}
}
override fun doFinally(activity: AppCompatActivity) {
cancelItemAction()
unselectAll()
setCurrentPath(currentDirectoryPath)
}
cancelItemAction()
unselectAll()
}
}
true
@ -473,6 +415,24 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
private fun mapFileForMove(items: ArrayList<OperationFile>, srcDirectoryPath: String): ArrayList<OperationFile> {
val newItems = ArrayList<OperationFile>()
items.forEach {
if (it.explorerElement.isDirectory){
if (gocryptfsVolume.pathExists(PathUtils.pathJoin(currentDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, it.explorerElement.fullPath)))){
newItems.addAll(
mapFileForMove(
gocryptfsVolume.listDir(it.explorerElement.fullPath).map { e -> OperationFile.fromExplorerElement(e) } as ArrayList<OperationFile>,
srcDirectoryPath
)
)
}
}
}
items.addAll(newItems)
return items
}
private fun cancelItemAction() {
if (currentItemAction != ItemsActions.NONE){
currentItemAction = ItemsActions.NONE
@ -489,220 +449,13 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
private fun copyFile(srcPath: String, dstPath: String): Boolean {
var success = true
val originalHandleId = gocryptfsVolume.openReadMode(srcPath)
if (originalHandleId != -1){
val newHandleId = gocryptfsVolume.openWriteMode(dstPath)
if (newHandleId != -1){
var offset: Long = 0
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (gocryptfsVolume.readFile(originalHandleId, offset, ioBuffer).also { length = it } > 0) {
val written = gocryptfsVolume.writeFile(newHandleId, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += written
} else {
success = false
break
}
}
gocryptfsVolume.closeFile(newHandleId)
} else {
success = false
}
gocryptfsVolume.closeFile(originalHandleId)
} else {
success = false
}
return success
}
private fun recursiveCopyDirectory(srcDirectoryPath: String, dstDirectoryPath: String): String? {
val mappedElements = gocryptfsVolume.recursiveMapFiles(srcDirectoryPath)
if (!gocryptfsVolume.pathExists(dstDirectoryPath)){
if (!gocryptfsVolume.mkdir(dstDirectoryPath)) {
return srcDirectoryPath
}
}
for (e in mappedElements) {
val dstPath = checkPathOverwrite(PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, e.fullPath)), e.isDirectory)
if (dstPath == null){
return ""
} else {
if (e.isDirectory) {
if (!gocryptfsVolume.pathExists(dstPath)){
if (!gocryptfsVolume.mkdir(dstPath)){
return e.fullPath
}
}
} else {
if (!copyFile(e.fullPath, dstPath)) {
return e.fullPath
}
}
}
}
return null
}
private fun moveDirectory(srcDirectoryPath: String, dstDirectoryPath: String): String? {
if (!gocryptfsVolume.pathExists(dstDirectoryPath)) {
if (!gocryptfsVolume.rename(srcDirectoryPath, dstDirectoryPath)) {
return srcDirectoryPath
}
} else {
moveElements(gocryptfsVolume.listDir(srcDirectoryPath), dstDirectoryPath)
gocryptfsVolume.rmdir(srcDirectoryPath)
}
return null
}
private fun moveElements(elements: List<ExplorerElement>, dstDirectoryPath: String): String? {
for (element in elements){
val dstPath = checkPathOverwrite(PathUtils.pathJoin(dstDirectoryPath, element.name), element.isDirectory)
if (dstPath == null){
return ""
} else {
if (element.isDirectory){
moveDirectory(element.fullPath, dstPath)?.let{
return it
}
} else {
if (!gocryptfsVolume.rename(element.fullPath, dstPath)){
return element.fullPath
}
}
}
}
return null
}
private fun importFileFromOtherVolume(remoteGocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): Boolean {
var success = true
val srcHandleID = remoteGocryptfsVolume.openReadMode(srcPath)
if (srcHandleID != -1) {
val dstHandleID = gocryptfsVolume.openWriteMode(dstPath)
if (dstHandleID != -1) {
var length: Int
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var offset: Long = 0
while (remoteGocryptfsVolume.readFile(srcHandleID, offset, ioBuffer).also { length = it } > 0) {
val written =
gocryptfsVolume.writeFile(dstHandleID, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += length.toLong()
} else {
success = false
break
}
}
gocryptfsVolume.closeFile(dstHandleID)
}
remoteGocryptfsVolume.closeFile(srcHandleID)
}
return success
}
private fun safeImportFileFromOtherVolume(remoteGocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): String? {
val checkedDstPath = checkPathOverwrite(dstPath, false)
return if (checkedDstPath == null){
""
} else {
if (importFileFromOtherVolume(remoteGocryptfsVolume, srcPath, checkedDstPath)) null else srcPath
}
}
private fun recursiveImportDirectoryFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, remote_directory_path: String, outputPath: String): String? {
val mappedElements = remote_gocryptfsVolume.recursiveMapFiles(remote_directory_path)
val dstDirectoryPath = checkPathOverwrite(PathUtils.pathJoin(outputPath, File(remote_directory_path).name), true)
if (dstDirectoryPath == null){
return ""
} else {
if (!gocryptfsVolume.pathExists(dstDirectoryPath)) {
if (!gocryptfsVolume.mkdir(dstDirectoryPath)) {
return remote_directory_path
}
}
for (e in mappedElements) {
val dstPath = checkPathOverwrite(PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(remote_directory_path, e.fullPath)), e.isDirectory)
if (dstPath == null){
return ""
} else {
if (e.isDirectory) {
if (!gocryptfsVolume.pathExists(dstPath)){
if (!gocryptfsVolume.mkdir(dstPath)){
return e.fullPath
}
}
} else {
if (!importFileFromOtherVolume(remote_gocryptfsVolume, e.fullPath, dstPath)) {
return e.fullPath
}
}
}
}
}
return null
}
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let {
contentResolver.openOutputStream(it)
}
return if (outputStream != null){
gocryptfsVolume.exportFile(srcPath, outputStream)
} else {
false
}
}
private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? {
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let {childTree ->
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val failedItem = recursiveExportDirectory(fullPath, childTree)
failedItem?.let { return it }
} else {
if (!exportFileInto(fullPath, childTree)){
return fullPath
}
}
}
return null
}
return treeDocumentFile.name
}
private fun recursiveRemoveDirectory(plain_directory_path: String): String? {
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!gocryptfsVolume.removeFile(fullPath)) {
return fullPath
}
}
}
return if (!gocryptfsVolume.rmdir(plain_directory_path)) {
plain_directory_path
} else {
null
}
}
private fun removeSelectedItems() {
var failedItem: String? = null
for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.getItem(i)
val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name)
if (element.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath)
result?.let{ failedItem = it }
} else {
if (!gocryptfsVolume.removeFile(fullPath)) {

89
app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt

@ -1,15 +1,11 @@
package sushi.hardcore.droidfs.explorers
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Looper
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.LoadingTask
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
class ExplorerActivityDrop : BaseExplorerActivity() {
@ -31,54 +27,61 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.validate -> {
object : LoadingTask(this, R.string.loading_msg_import) {
override fun doTask(activity: AppCompatActivity) {
val alertDialog = ColoredAlertDialogBuilder(activity)
alertDialog.setCancelable(false)
alertDialog.setPositiveButton(R.string.ok) { _, _ -> finish() }
val errorMsg: String?
val extras = intent.extras
if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)){
Looper.prepare()
when (intent.action) {
Intent.ACTION_SEND -> {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
errorMsg = if (uri == null){
getString(R.string.share_intent_parsing_failed)
} else {
if (importFilesFromUris(listOf(uri), this){ _, _ -> finish() }) null else ""
}
}
Intent.ACTION_SEND_MULTIPLE -> {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
errorMsg = if (uris != null){
if (importFilesFromUris(uris, this){ _, _ -> finish() }) null else ""
} else {
getString(R.string.share_intent_parsing_failed)
}
}
else -> {
errorMsg = getString(R.string.share_intent_parsing_failed)
}
val extras = intent.extras
val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) {
when (intent.action) {
Intent.ACTION_SEND -> {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
if (uri == null) {
getString(R.string.share_intent_parsing_failed)
} else {
importFilesFromUris(listOf(uri), ::onImported)
null
}
} else {
errorMsg = getString(R.string.share_intent_parsing_failed)
}
if (errorMsg == null || errorMsg.isNotEmpty()){
if (errorMsg == null) {
alertDialog.setTitle(R.string.success_import)
alertDialog.setMessage(R.string.success_import_msg)
} else if (errorMsg.isNotEmpty()) {
alertDialog.setTitle(R.string.error)
alertDialog.setMessage(errorMsg)
Intent.ACTION_SEND_MULTIPLE -> {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (uris != null) {
importFilesFromUris(uris, ::onImported)
null
} else {
getString(R.string.share_intent_parsing_failed)
}
stopTask { alertDialog.show() }
}
else -> getString(R.string.share_intent_parsing_failed)
}
} else {
getString(R.string.share_intent_parsing_failed)
}
errorMsg?.let {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(it)
.setPositiveButton(R.string.ok, null)
.show()
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun onImported(failedItem: String?){
if (failedItem == null) {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.success_import)
.setMessage(R.string.success_import_msg)
.setCancelable(false)
.setPositiveButton(R.string.ok){_, _ ->
finish()
}
.show()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}

4
app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt

@ -17,6 +17,10 @@ class ExplorerActivityPick : BaseExplorerActivity() {
resultIntent.putExtra("sessionID", gocryptfsVolume.sessionID)
}
override fun bindFileOperationService() {
//don't bind
}
override fun onExplorerItemClick(position: Int) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)

2
app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt

@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.explorers
import sushi.hardcore.droidfs.util.PathUtils
import java.util.*
class ExplorerElement(val name: String, val elementType: Short, var size: Long, mtime: Long, parentPath: String) {
class ExplorerElement(val name: String, val elementType: Short, var size: Long, val mtime: Long, val parentPath: String) {
val mTime = Date((mtime * 1000).toString().toLong())
val fullPath: String = PathUtils.pathJoin(parentPath, name)

11
app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt

@ -0,0 +1,11 @@
package sushi.hardcore.droidfs.file_operations
import sushi.hardcore.droidfs.explorers.ExplorerElement
class OperationFile(val explorerElement: ExplorerElement, var dstPath: String? = null, var overwriteConfirmed: Boolean = false) {
companion object {
fun fromExplorerElement(e: ExplorerElement): OperationFile {
return OperationFile(e, null)
}
}
}

20
app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt

@ -187,4 +187,24 @@ class GocryptfsVolume(var sessionID: Int) {
}
return result
}
fun recursiveRemoveDirectory(plain_directory_path: String): String? {
val explorerElements = listDir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!removeFile(fullPath)) {
return fullPath
}
}
}
return if (!rmdir(plain_directory_path)) {
plain_directory_path
} else {
null
}
}
}

12
app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt

@ -43,7 +43,17 @@ object PathUtils {
}
fun getRelativePath(parentPath: String, childPath: String): String {
return childPath.substring(parentPath.length + 1)
return when {
parentPath.isEmpty() -> {
childPath
}
parentPath.length == childPath.length -> {
""
}
else -> {
childPath.substring(parentPath.length + 1)
}
}
}
fun getFilenameFromURI(context: Context, uri: Uri): String? {

6
app/src/main/res/values/strings.xml

@ -133,8 +133,6 @@
<string name="loading_msg_create">Creating volume…</string>
<string name="loading_msg_change_password">Changing password…</string>
<string name="loading_msg_open">Opening volume…</string>
<string name="loading_msg_import">Importing selected files…</string>
<string name="loading_msg_wipe">Wiping original files…</string>
<string name="loading_msg_export">Exporting files…</string>
<string name="query_cursor_null_error_msg">Unable to access this file</string>
<string name="about">About</string>
@ -144,9 +142,7 @@
<string name="gitea_summary">The DroidFS repository on the DryCat Gitea instance. Gitea is fully free and self-hosted. Source code, documentation, bug tracker…</string>
<string name="share">Share</string>
<string name="decrypt_files">Export/Decrypt</string>
<string name="loading_msg_copy">Copying selected items…</string>
<string name="copy_failed">Copy of %s failed.</string>
<string name="copy_success_msg">The selected items have been successfully copied.</string>
<string name="copy_success">Copy successful !</string>
<string name="fab_dialog_title">Add</string>
<string name="take_photo">Take photo</string>
@ -159,9 +155,7 @@
<string name="reset_theme_color">Reset theme color</string>
<string name="reset_theme_color_summary">Reset theme color to the default one</string>
<string name="copy_menu_title">Copy</string>
<string name="loading_msg_move">Moving selected items…</string>
<string name="move_failed">Move of %s failed.</string>
<string name="move_success_msg">The selected items have been successfully moved.</string>
<string name="move_success">Move successful !</string>
<string name="enter_timer_duration">Enter the timer duration (in s)</string>
<string name="timer_empty_error_msg">Please enter a numeric value</string>

3
gradle/wrapper/gradle-wrapper.properties

@ -1,6 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading…
Cancel
Save