DroidFS/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt

496 lines
21 KiB
Kotlin
Raw Normal View History

2020-07-17 16:35:39 +02:00
package sushi.hardcore.droidfs
2022-03-06 14:45:52 +01:00
import android.content.ComponentName
import android.content.Context
2020-07-17 16:35:39 +02:00
import android.content.Intent
2022-03-06 14:45:52 +01:00
import android.content.ServiceConnection
import android.net.Uri
2020-07-17 16:35:39 +02:00
import android.os.Bundle
2022-03-06 14:45:52 +01:00
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
2022-03-05 12:51:02 +01:00
import android.view.View
2022-03-06 14:45:52 +01:00
import android.widget.Toast
2022-09-23 12:09:22 +02:00
import androidx.activity.addCallback
2022-03-05 12:51:02 +01:00
import androidx.activity.result.contract.ActivityResultContracts
2022-03-06 14:45:52 +01:00
import androidx.documentfile.provider.DocumentFile
2022-04-20 15:17:33 +02:00
import androidx.lifecycle.lifecycleScope
2022-03-05 12:51:02 +01:00
import androidx.recyclerview.widget.LinearLayoutManager
2022-04-20 15:17:33 +02:00
import kotlinx.coroutines.launch
2023-02-06 10:52:51 +01:00
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
2022-03-05 12:51:02 +01:00
import sushi.hardcore.droidfs.adapters.VolumeAdapter
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
2021-06-11 20:23:54 +02:00
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
2022-03-05 12:51:02 +01:00
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter
2022-03-06 14:45:52 +01:00
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.util.IntentUtils
2022-03-05 12:51:02 +01:00
import sushi.hardcore.droidfs.util.PathUtils
2021-11-09 11:12:09 +01:00
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
2022-03-24 20:08:23 +01:00
import sushi.hardcore.droidfs.widgets.EditTextDialog
2022-03-05 12:51:02 +01:00
import java.io.File
2020-07-17 16:35:39 +02:00
2022-04-17 15:52:34 +02:00
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
2023-03-07 23:25:17 +01:00
companion object {
private const val OPEN_DEFAULT_VOLUME = "openDefault"
}
2022-03-05 12:51:02 +01:00
private lateinit var binding: ActivityMainBinding
private lateinit var volumeDatabase: VolumeDatabase
2023-03-07 23:25:17 +01:00
private lateinit var volumeManager: VolumeManager
2022-03-05 12:51:02 +01:00
private lateinit var volumeAdapter: VolumeAdapter
private lateinit var volumeOpener: VolumeOpener
2022-03-05 12:51:02 +01:00
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if ((explorerRouter.pickMode || explorerRouter.dropMode) && result.resultCode != AddVolumeActivity.RESULT_USER_BACK) {
setResult(result.resultCode, result.data) // forward result
finish()
2022-03-05 12:51:02 +01:00
}
}
private var changePasswordPosition: Int? = null
private var changePassword = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
2022-03-06 14:45:52 +01:00
changePasswordPosition?.let { unselect(it) }
}
private val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
if (uri != null)
onDirectoryPicked(uri)
2022-03-05 12:51:02 +01:00
}
2022-03-06 14:45:52 +01:00
private lateinit var fileOperationService: FileOperationService
private lateinit var explorerRouter: ExplorerRouter
2022-03-05 12:51:02 +01:00
2020-07-17 16:35:39 +02:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2022-03-05 12:51:02 +01:00
binding = ActivityMainBinding.inflate(layoutInflater)
2021-06-11 20:23:54 +02:00
setContentView(binding.root)
2021-11-09 11:12:09 +01:00
if (sharedPrefs.getBoolean("applicationFirstOpening", true)) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(R.string.usf_home_warning_msg)
.setCancelable(false)
.setPositiveButton(R.string.see_unsafe_features) { _, _ ->
val intent = Intent(this, SettingsActivity::class.java)
intent.putExtra("screen", "UnsafeFeaturesSettingsFragment")
startActivity(intent)
}
.setNegativeButton(R.string.ok, null)
2022-03-24 20:08:23 +01:00
.setOnDismissListener {
with (sharedPrefs.edit()) {
putBoolean("applicationFirstOpening", false)
apply()
}
}
2021-11-09 11:12:09 +01:00
.show()
2020-07-17 16:35:39 +02:00
}
explorerRouter = ExplorerRouter(this, intent)
2023-03-07 23:25:17 +01:00
volumeManager = (application as VolumeManagerApp).volumeManager
2022-03-05 12:51:02 +01:00
volumeDatabase = VolumeDatabase(this)
volumeAdapter = VolumeAdapter(
this,
volumeDatabase,
2023-03-07 23:25:17 +01:00
(application as VolumeManagerApp).volumeManager,
!explorerRouter.pickMode && !explorerRouter.dropMode,
!explorerRouter.dropMode,
2022-04-17 15:52:34 +02:00
this,
2022-03-05 12:51:02 +01:00
)
binding.recyclerViewVolumes.adapter = volumeAdapter
binding.recyclerViewVolumes.layoutManager = LinearLayoutManager(this)
if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE
2021-06-07 16:34:50 +02:00
}
if (explorerRouter.pickMode) {
2022-03-05 12:51:02 +01:00
title = getString(R.string.select_volume)
}
binding.fab.setOnClickListener {
addVolume.launch(Intent(this, AddVolumeActivity::class.java).also {
if (explorerRouter.dropMode || explorerRouter.pickMode) {
IntentUtils.forwardIntent(intent, it)
}
})
2021-06-07 16:34:50 +02:00
}
volumeOpener = VolumeOpener(this)
2022-09-23 12:09:22 +02:00
onBackPressedDispatcher.addCallback(this) {
if (volumeAdapter.selectedItems.isNotEmpty()) {
unselectAll()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
2023-03-07 23:25:17 +01:00
volumeOpener.defaultVolumeName?.let { name ->
val state = savedInstanceState?.getBoolean(OPEN_DEFAULT_VOLUME)
if (state == true || state == null) {
val volumeData = volumeAdapter.volumes.first { it.name == name }
if (!volumeManager.isOpen(volumeData)) {
try {
openVolume(volumeData)
} catch (e: NoSuchElementException) {
unsetDefaultVolume()
}
}
}
}
2022-03-06 14:45:52 +01:00
Intent(this, FileOperationService::class.java).also {
bindService(it, object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
fileOperationService = (service as FileOperationService.LocalBinder).getService()
}
override fun onServiceDisconnected(arg0: ComponentName) {}
}, Context.BIND_AUTO_CREATE)
}
2022-03-05 12:51:02 +01:00
}
override fun onStart() {
super.onStart()
2022-09-23 12:09:22 +02:00
// refresh theme if changed in SettingsActivity
2023-02-06 10:52:51 +01:00
val newThemeValue = sharedPrefs.getString(Constants.THEME_VALUE_KEY, Constants.DEFAULT_THEME_VALUE)!!
2022-09-23 12:09:22 +02:00
onThemeChanged(newThemeValue)
volumeOpener.themeValue = newThemeValue
volumeAdapter.refresh()
if (volumeAdapter.volumes.isNotEmpty()) {
binding.textNoVolumes.visibility = View.GONE
}
// refresh this in case another instance of MainActivity changes its value
volumeOpener.defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
}
2023-03-07 23:25:17 +01:00
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(OPEN_DEFAULT_VOLUME, false)
}
2022-04-17 15:52:34 +02:00
override fun onSelectionChanged(size: Int) {
title = if (size == 0) {
getString(R.string.app_name)
} else {
getString(R.string.elements_selected, size, volumeAdapter.volumes.size)
}
}
override fun onVolumeItemClick(volume: VolumeData, position: Int) {
2022-03-05 12:51:02 +01:00
if (volumeAdapter.selectedItems.isEmpty())
openVolume(volume)
2022-03-05 12:51:02 +01:00
else
invalidateOptionsMenu()
}
2022-04-17 15:52:34 +02:00
override fun onVolumeItemLongClick() {
2022-03-05 12:51:02 +01:00
invalidateOptionsMenu()
}
private fun unselectAll(notifyChange: Boolean = true) {
volumeAdapter.unSelectAll(notifyChange)
2022-03-05 12:51:02 +01:00
invalidateOptionsMenu()
}
2022-03-06 14:45:52 +01:00
private fun unselect(position: Int) {
volumeAdapter.selectedItems.remove(position)
volumeAdapter.onVolumeChanged(position)
2022-04-17 15:52:34 +02:00
onSelectionChanged(0) // unselect() is always called when only one element is selected
2022-03-06 14:45:52 +01:00
invalidateOptionsMenu()
}
2023-03-07 23:25:17 +01:00
private fun removeVolume(volume: VolumeData) {
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
volumeDatabase.removeVolume(volume.name)
}
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
2022-03-05 12:51:02 +01:00
if (i < volumes.size) {
if (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) {
val dialogBinding = DialogDeleteVolumeBinding.inflate(layoutInflater)
dialogBinding.textContent.text = getString(R.string.delete_hidden_volume_question, volumes[i].name)
// show checkbox only if there is at least one other hidden volume
for (j in (i+1 until volumes.size)) {
if (volumes[j].isHidden) {
dialogBinding.checkboxApplyToAll.visibility = View.VISIBLE
break
}
}
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setView(dialogBinding.root)
.setPositiveButton(R.string.forget_only) { _, _ ->
2023-03-07 23:25:17 +01:00
removeVolume(volumes[i])
2022-03-05 12:51:02 +01:00
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) false else null)
}
.setNegativeButton(R.string.delete_volume) { _, _ ->
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
2023-03-07 23:25:17 +01:00
removeVolume(volumes[i])
2022-03-05 12:51:02 +01:00
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
}
.setOnCancelListener {
volumeAdapter.refresh()
invalidateOptionsMenu()
}
.show()
} else {
if (doDeleteVolumeContent) {
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
}
2023-03-07 23:25:17 +01:00
removeVolume(volumes[i])
2022-03-05 12:51:02 +01:00
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
2023-03-07 23:25:17 +01:00
removeVolume(volumes[i])
2022-03-05 12:51:02 +01:00
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
volumeAdapter.refresh()
invalidateOptionsMenu()
if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE
}
2021-06-07 16:34:50 +02:00
}
2020-07-17 16:35:39 +02:00
}
private fun unsetDefaultVolume() {
with (sharedPrefs.edit()) {
remove(DEFAULT_VOLUME_KEY)
apply()
}
volumeOpener.defaultVolumeName = null
}
2020-07-17 16:35:39 +02:00
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
2022-03-05 12:51:02 +01:00
android.R.id.home -> {
if (explorerRouter.pickMode || explorerRouter.dropMode) {
2022-03-05 12:51:02 +01:00
finish()
} else {
unselectAll()
}
true
}
R.id.select_all -> {
volumeAdapter.selectAll()
invalidateOptionsMenu()
true
}
2023-03-07 23:25:17 +01:00
R.id.lock -> {
volumeAdapter.selectedItems.forEach {
volumeManager.getVolumeId(volumeAdapter.volumes[it])?.let { id ->
volumeManager.closeVolume(id)
}
}
unselectAll()
true
}
2022-03-05 12:51:02 +01:00
R.id.remove -> {
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
removeVolumes(selectedVolumes)
true
}
R.id.delete_password_hash -> {
2022-03-05 12:51:02 +01:00
for (i in volumeAdapter.selectedItems) {
if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
volumeAdapter.onVolumeChanged(i)
}
unselectAll(false)
2022-03-05 12:51:02 +01:00
true
}
R.id.change_password -> {
changePasswordPosition = volumeAdapter.selectedItems.elementAt(0)
changePassword.launch(Intent(this, ChangePasswordActivity::class.java).apply {
putExtra("volume", volumeAdapter.volumes[changePasswordPosition!!])
})
true
}
R.id.remove_default_open -> {
unsetDefaultVolume()
unselect(volumeAdapter.selectedItems.first())
true
}
2022-03-06 14:45:52 +01:00
R.id.copy -> {
val position = volumeAdapter.selectedItems.elementAt(0)
val volume = volumeAdapter.volumes[position]
when {
volume.isHidden -> {
PathUtils.safePickDirectory(pickDirectory, this, themeValue)
}
File(filesDir, volume.shortName).exists() -> {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.hidden_volume_already_exists)
.setPositiveButton(R.string.ok, null)
.show()
}
else -> {
unselect(position)
copyVolume(
DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(filesDir),
) {
VolumeData(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
2022-03-06 14:45:52 +01:00
}
}
}
true
}
2022-03-24 20:08:23 +01:00
R.id.rename -> {
val position = volumeAdapter.selectedItems.elementAt(0)
renameVolume(volumeAdapter.volumes[position], position)
true
}
2022-03-05 12:51:02 +01:00
R.id.settings -> {
2020-07-17 16:35:39 +02:00
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu)
menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode
2022-03-05 12:51:02 +01:00
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = isSelecting
2023-03-07 23:25:17 +01:00
menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any {
i -> volumeManager.isOpen(volumeAdapter.volumes[i])
}
menu.findItem(R.id.remove).isVisible = isSelecting
menu.findItem(R.id.delete_password_hash).isVisible =
isSelecting &&
!volumeAdapter.selectedItems.any { i -> volumeAdapter.volumes[i].encryptedHash == null }
val onlyOneSelected = volumeAdapter.selectedItems.size == 1
val onlyOneAndWriteable =
onlyOneSelected &&
volumeAdapter.volumes[volumeAdapter.selectedItems.first()].canWrite(filesDir.path)
menu.findItem(R.id.change_password).isVisible = onlyOneAndWriteable
menu.findItem(R.id.remove_default_open).isVisible =
onlyOneSelected &&
volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == volumeOpener.defaultVolumeName
2022-03-06 14:45:52 +01:00
with(menu.findItem(R.id.copy)) {
isVisible = onlyOneSelected
2022-03-06 14:45:52 +01:00
if (isVisible) {
setTitle(if (volumeAdapter.volumes[volumeAdapter.selectedItems.elementAt(0)].isHidden)
R.string.copy_hidden_volume
else
R.string.copy_external_volume
)
}
}
2022-03-24 20:08:23 +01:00
menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || explorerRouter.pickMode || explorerRouter.dropMode)
2020-07-17 16:35:39 +02:00
return true
}
2022-03-06 14:45:52 +01:00
private fun onDirectoryPicked(uri: Uri) {
val position = volumeAdapter.selectedItems.elementAt(0)
val volume = volumeAdapter.volumes[position]
unselect(position)
val dstDocumentFile = DocumentFile.fromTreeUri(this, uri)
if (dstDocumentFile == null) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.path_error)
.setPositiveButton(R.string.ok, null)
.show()
} else {
copyVolume(
DocumentFile.fromFile(File(volume.getFullPath(filesDir.path))),
dstDocumentFile,
) { dstRootDirectory ->
dstRootDirectory.name?.let { name ->
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null
else VolumeData(
2022-03-06 14:45:52 +01:00
PathUtils.pathJoin(path, name),
false,
2022-06-18 21:13:16 +02:00
volume.type,
2022-03-06 14:45:52 +01:00
volume.encryptedHash,
volume.iv
)
}
}
}
}
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> VolumeData?) {
2022-04-20 15:17:33 +02:00
lifecycleScope.launch {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when {
result.taskResult.cancelled -> {
result.dstRootDirectory?.delete()
}
result.taskResult.failedItem == null -> {
result.dstRootDirectory?.let {
2022-03-06 14:45:52 +01:00
getResultVolume(it)?.let { volume ->
volumeDatabase.saveVolume(volume)
volumeAdapter.apply {
volumes = volumeDatabase.getVolumes()
notifyItemInserted(volumes.size)
}
binding.textNoVolumes.visibility = View.GONE
2022-04-20 15:17:33 +02:00
Toast.makeText(this@MainActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
2022-03-06 14:45:52 +01:00
}
}
2022-04-20 15:17:33 +02:00
}
else -> {
CustomAlertDialogBuilder(this@MainActivity, themeValue)
2022-03-06 14:45:52 +01:00
.setTitle(R.string.error)
2022-04-20 15:17:33 +02:00
.setMessage(getString(R.string.copy_failed, result.taskResult.failedItem.name))
2022-03-06 14:45:52 +01:00
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
private fun renameVolume(volume: VolumeData, position: Int) {
with (EditTextDialog(this, R.string.new_volume_name) { newName ->
2022-03-24 20:08:23 +01:00
val srcPath = File(volume.getFullPath(filesDir.path))
val dstPath = File(srcPath.parent, newName).canonicalFile
val newDBName: String
val success = if (volume.isHidden) {
2022-06-18 21:13:16 +02:00
if (newName.contains(PathUtils.SEPARATOR)) {
2022-03-24 20:08:23 +01:00
Toast.makeText(this, R.string.error_slash_in_name, Toast.LENGTH_SHORT).show()
renameVolume(volume, position)
return@EditTextDialog
}
newDBName = newName
srcPath.renameTo(dstPath)
} else {
newDBName = dstPath.path
DocumentFile.fromFile(srcPath).renameTo(newName)
}
if (success) {
volumeDatabase.renameVolume(volume.name, newDBName)
unselect(position)
if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) {
putString(DEFAULT_VOLUME_KEY, newDBName)
apply()
}
volumeOpener.defaultVolumeName = newDBName
}
2022-03-24 20:08:23 +01:00
} else {
Toast.makeText(this, R.string.volume_rename_failed, Toast.LENGTH_SHORT).show()
}
}) {
setSelectedText(volume.shortName)
show()
}
2022-03-24 20:08:23 +01:00
}
private fun openVolume(volume: VolumeData) {
volumeOpener.openVolume(volume, true, object : VolumeOpener.VolumeOpenerCallbacks {
override fun onHashStorageReset() {
volumeAdapter.refresh()
2022-03-05 12:51:02 +01:00
}
2023-03-07 23:25:17 +01:00
override fun onVolumeOpened(id: Int) {
startActivity(explorerRouter.getExplorerIntent(id, volume.shortName))
if (explorerRouter.dropMode || explorerRouter.pickMode) {
finish()
2022-04-20 15:17:33 +02:00
}
2022-03-05 12:51:02 +01:00
}
})
2022-03-05 12:51:02 +01:00
}
override fun onStop() {
super.onStop()
2023-02-02 19:37:10 +01:00
volumeOpener.wipeSensitive()
2020-07-17 16:35:39 +02:00
}
}