Fixed - File loading bugs, optimized refresh using DiffUtil more efficiently
This commit is contained in:
@@ -11,9 +11,10 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "devs.org.calculator"
|
||||
minSdk = 26
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 34
|
||||
versionCode = 5
|
||||
versionName = "1.4.0"
|
||||
versionCode = 6
|
||||
versionName = "1.4.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -81,6 +82,7 @@ dependencies {
|
||||
implementation(libs.androidx.viewpager)
|
||||
implementation(libs.zoomage)
|
||||
implementation(libs.lottie)
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-beta01")
|
||||
|
||||
// Room dependencies
|
||||
implementation(libs.androidx.room.runtime)
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
@@ -23,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.adapters.FileAdapter
|
||||
import devs.org.calculator.adapters.FolderSelectionAdapter
|
||||
import devs.org.calculator.callbacks.FileProcessCallback
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
@@ -41,6 +39,8 @@ import java.io.File
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.app.AlertDialog
|
||||
import android.view.WindowManager
|
||||
import devs.org.calculator.adapters.FileAdapter
|
||||
|
||||
class ViewFolderActivity : AppCompatActivity() {
|
||||
|
||||
@@ -151,7 +151,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun updateFilesToAdapter() {
|
||||
openFolder(currentFolder!!)
|
||||
val files = folderManager.getFilesInFolder(currentFolder!!)
|
||||
fileAdapter?.submitList(files)
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +224,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
).show()
|
||||
dismissCustomDialog()
|
||||
}
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,9 +232,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshCurrentFolder()
|
||||
setupFlagSecure()
|
||||
}
|
||||
|
||||
private fun openFolder(folder: File) {
|
||||
private fun setupFlagSecure() {
|
||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openFolder(folder: File) {
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs()
|
||||
File(folder, ".nomedia").createNewFile()
|
||||
@@ -246,11 +258,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
binding.swipeLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
binding.noItems.visibility = View.VISIBLE
|
||||
binding.recyclerView.visibility = View.GONE
|
||||
binding.swipeLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showFileList(files: List<File>, folder: File) {
|
||||
@@ -261,17 +274,20 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
onFolderLongClick = { isSelected ->
|
||||
handleFileSelectionModeChange(isSelected)
|
||||
}).apply {
|
||||
setFileOperationCallback(object : FileAdapter.FileOperationCallback {
|
||||
setFilesOperationCallback(object : FileAdapter.FilesOperationCallback {
|
||||
override fun onFileDeleted(file: File) {
|
||||
refreshCurrentFolder()
|
||||
updateFilesToAdapter()
|
||||
}
|
||||
|
||||
override fun onFileRenamed(oldFile: File, newFile: File) {
|
||||
refreshCurrentFolder()
|
||||
updateFilesToAdapter()
|
||||
}
|
||||
|
||||
override fun onRefreshNeeded() {
|
||||
refreshCurrentFolder()
|
||||
updateFilesToAdapter()
|
||||
}
|
||||
|
||||
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
|
||||
@@ -287,7 +303,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
binding.recyclerView.adapter = fileAdapter
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.swipeLayout.visibility = View.VISIBLE
|
||||
binding.noItems.visibility = View.GONE
|
||||
|
||||
binding.menuButton.setOnClickListener {
|
||||
@@ -381,10 +397,10 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
getString(R.string.decrypt_file) -> {
|
||||
lifecycleScope.launch {
|
||||
val filesWithoutMetadata = selectedFiles.filter { file ->
|
||||
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
||||
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
||||
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
|
||||
}
|
||||
|
||||
|
||||
if (filesWithoutMetadata.isNotEmpty()) {
|
||||
showDecryptionTypeDialog(filesWithoutMetadata)
|
||||
} else {
|
||||
@@ -455,21 +471,18 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val decryptedFiles = mutableMapOf<File, File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
|
||||
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
|
||||
|
||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
|
||||
hiddenFile.let {
|
||||
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
@@ -479,27 +492,23 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
if (file.delete()) {
|
||||
|
||||
decryptedFiles[file] = decryptedFile
|
||||
successCount++
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
|
||||
|
||||
val extension = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> ".jpg"
|
||||
FileManager.FileType.VIDEO -> ".mp4"
|
||||
@@ -509,11 +518,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||
|
||||
|
||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
|
||||
|
||||
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = decryptedFile.absolutePath,
|
||||
@@ -525,37 +531,32 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
)
|
||||
)
|
||||
if (file.delete()) {
|
||||
|
||||
decryptedFiles[file] = decryptedFile
|
||||
successCount++
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
failCount++
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
fileAdapter?.exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
@@ -567,7 +568,10 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
refreshCurrentFolder()
|
||||
if (successCount > 0) {
|
||||
refreshCurrentFolder()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,10 +631,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
val files = folderManager.getFilesInFolder(folder)
|
||||
mainHandler.post {
|
||||
if (files.isNotEmpty()) {
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.swipeLayout.visibility = View.VISIBLE
|
||||
binding.noItems.visibility = View.GONE
|
||||
|
||||
fileAdapter?.submitList(files.toMutableList())
|
||||
val currentFiles = fileAdapter?.currentList ?: emptyList()
|
||||
val hasChanges = files.size != currentFiles.size ||
|
||||
files.any { newFile ->
|
||||
currentFiles.none { it.absolutePath == newFile.absolutePath }
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
fileAdapter?.submitList(files.toMutableList())
|
||||
}
|
||||
|
||||
fileAdapter?.let { adapter ->
|
||||
if (adapter.isInSelectionMode()) {
|
||||
showFileSelectionIcons()
|
||||
@@ -642,8 +655,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
} catch (_: Exception) {
|
||||
mainHandler.post {
|
||||
showEmptyState()
|
||||
}
|
||||
@@ -659,6 +671,9 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
binding.back.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
binding.swipeLayout.setOnRefreshListener {
|
||||
openFolder(currentFolder!!)
|
||||
}
|
||||
|
||||
binding.addImage.setOnClickListener { openFilePicker("image/*") }
|
||||
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
|
||||
@@ -748,6 +763,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
private fun performFileUnhiding(selectedFiles: List<File>) {
|
||||
lifecycleScope.launch {
|
||||
var allUnhidden = true
|
||||
val unhiddenFiles = mutableListOf<File>()
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
@@ -767,6 +783,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
file.delete()
|
||||
decryptedFile.delete()
|
||||
unhiddenFiles.add(file)
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
@@ -794,6 +811,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||
}
|
||||
file.delete()
|
||||
unhiddenFiles.add(file)
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
@@ -802,8 +820,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
allUnhidden = false
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,8 +833,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -824,17 +842,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
private fun performFileDeletion(selectedFiles: List<File>) {
|
||||
lifecycleScope.launch {
|
||||
var allDeleted = true
|
||||
val deletedFiles = mutableListOf<File>()
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
hiddenFile?.let {
|
||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||
}
|
||||
if (!file.delete()) {
|
||||
if (file.delete()) {
|
||||
deletedFiles.add(file)
|
||||
} else {
|
||||
allDeleted = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
} catch (_: Exception) {
|
||||
allDeleted = false
|
||||
}
|
||||
}
|
||||
@@ -847,8 +867,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -862,6 +882,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||
lifecycleScope.launch {
|
||||
var allCopied = true
|
||||
val copiedFiles = mutableListOf<File>()
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val newFile = File(destinationFolder, file.name)
|
||||
@@ -880,17 +901,18 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
)
|
||||
)
|
||||
}
|
||||
copiedFiles.add(file)
|
||||
} catch (e: Exception) {
|
||||
|
||||
allCopied = false
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -898,6 +920,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||
lifecycleScope.launch {
|
||||
var allMoved = true
|
||||
val movedFiles = mutableListOf<File>()
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val newFile = File(destinationFolder, file.name)
|
||||
@@ -913,22 +936,27 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
file.delete()
|
||||
if (file.delete()) {
|
||||
movedFiles.add(file)
|
||||
} else {
|
||||
allMoved = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
allMoved = false
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||
.filter { it != currentFolder }
|
||||
|
||||
@@ -9,9 +9,6 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -26,7 +23,9 @@ import devs.org.calculator.activities.PreviewActivity
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.databinding.ListItemFileBinding
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.FolderManager
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||
@@ -40,22 +39,18 @@ class FileAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val currentFolder: File,
|
||||
private val showFileName: Boolean,
|
||||
private val onFolderLongClick: (Boolean) -> Unit
|
||||
) : ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback()) {
|
||||
private val onFolderLongClick: (Boolean) -> Unit,
|
||||
) : ListAdapter<File, FileAdapter.FilesViewHolder>(FileDiffCallback()) {
|
||||
|
||||
|
||||
private var filesOperationCallback: WeakReference<FilesOperationCallback>? = null
|
||||
private val selectedItems = mutableSetOf<Int>()
|
||||
private var isSelectionMode = false
|
||||
|
||||
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
|
||||
|
||||
private val fileExecutor = Executors.newSingleThreadExecutor()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
interface FileOperationCallback {
|
||||
interface FilesOperationCallback {
|
||||
fun onFileDeleted(file: File)
|
||||
fun onFileRenamed(oldFile: File, newFile: File)
|
||||
fun onRefreshNeeded()
|
||||
@@ -63,197 +58,77 @@ class FileAdapter(
|
||||
fun onSelectionCountChanged(selectedCount: Int)
|
||||
}
|
||||
|
||||
fun setFileOperationCallback(callback: FileOperationCallback?) {
|
||||
fileOperationCallback = callback?.let { WeakReference(it) }
|
||||
fun setFilesOperationCallback(callback: FilesOperationCallback?) {
|
||||
filesOperationCallback = callback?.let { WeakReference(it) }
|
||||
}
|
||||
|
||||
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
|
||||
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
|
||||
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
|
||||
val selectedLayer: View = view.findViewById(R.id.selectedLayer)
|
||||
val shade: View = view.findViewById(R.id.shade)
|
||||
val selected: ImageView = view.findViewById(R.id.selected)
|
||||
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
|
||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): FilesViewHolder {
|
||||
val binding =
|
||||
ListItemFileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return FilesViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: FilesViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.bind(getItem(position))
|
||||
|
||||
}
|
||||
|
||||
|
||||
inner class FilesViewHolder(private val binding: ListItemFileBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("FileEndsWithExt")
|
||||
fun bind(file: File) {
|
||||
val position = adapterPosition
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
|
||||
else {
|
||||
FileManager(context, lifecycleOwner).getFileType(file)
|
||||
}
|
||||
|
||||
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
|
||||
setupClickListeners(file, fileType)
|
||||
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
val currentFileData =
|
||||
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
val currentFileType = currentFileData?.fileType ?: FileManager(
|
||||
context,
|
||||
lifecycleOwner
|
||||
).getFileType(file)
|
||||
|
||||
|
||||
val isCurrentFileEncrypted = currentFileData?.isEncrypted ?: file.endsWith(
|
||||
SecurityUtils.ENCRYPTED_EXTENSION
|
||||
)
|
||||
|
||||
setupClickListeners(file, currentFileType)
|
||||
setupDisplay(
|
||||
file,
|
||||
currentFileType,
|
||||
isCurrentFileEncrypted,
|
||||
currentFileData
|
||||
)
|
||||
binding.fileNameTextView.text = if (isCurrentFileEncrypted) currentFileData?.fileName else file.name
|
||||
binding.fileNameTextView.visibility =
|
||||
if (showFileName) View.VISIBLE else View.GONE
|
||||
binding.shade.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val isSelected = selectedItems.contains(position)
|
||||
updateSelectionUI(isSelected)
|
||||
}
|
||||
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
|
||||
binding.encrypted.visibility =
|
||||
if (isCurrentFileEncrypted) View.VISIBLE else View.GONE
|
||||
} catch (e: Exception) {
|
||||
Log.e("FileAdapter", "Error in bind: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun bind(file: File, payloads: List<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
bind(file)
|
||||
return
|
||||
}
|
||||
|
||||
val changes = payloads.firstOrNull() as? List<String>
|
||||
changes?.forEach { change ->
|
||||
when (change) {
|
||||
"NAME_CHANGED" -> {
|
||||
fileNameTextView.text = file.name
|
||||
}
|
||||
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
|
||||
|
||||
}
|
||||
"SELECTION_CHANGED" -> {
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val isSelected = selectedItems.contains(position)
|
||||
updateSelectionUI(isSelected)
|
||||
notifySelectionModeChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectionUI(isSelected: Boolean) {
|
||||
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
selected.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
|
||||
fileNameTextView.text = metadata?.fileName ?: file.name
|
||||
|
||||
when (fileType) {
|
||||
FileManager.FileType.IMAGE -> {
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
try {
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.ic_image)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
FileManager.FileType.VIDEO -> {
|
||||
playIcon.visibility = View.VISIBLE
|
||||
if (isEncrypted) {
|
||||
try {
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.ic_video)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.ic_audio)
|
||||
}
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
else -> {
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.ic_document)
|
||||
}
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
|
||||
itemView.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
|
||||
if (isSelectionMode) {
|
||||
toggleSelection(position)
|
||||
} else {
|
||||
openFile(file, fileType)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
toggleSelection(position)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFile(file: File, fileType: FileManager.FileType) {
|
||||
if (!file.exists()) {
|
||||
Toast.makeText(context,
|
||||
@@ -272,7 +147,7 @@ class FileAdapter(
|
||||
showDecryptionTypeDialog(file)
|
||||
} else {
|
||||
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
||||
|
||||
|
||||
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
||||
if (tempFile.exists() && tempFile.length() > 0) {
|
||||
mainHandler.post {
|
||||
@@ -305,7 +180,7 @@ class FileAdapter(
|
||||
} else {
|
||||
openInPreview(fileType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -317,6 +192,39 @@ class FileAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDecryptionTypeDialog(file: File) {
|
||||
val options = arrayOf("Image", "Video", "Audio")
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Select File Type")
|
||||
.setMessage("Please select the type of file to decrypt")
|
||||
.setItems(options) { _, which ->
|
||||
val selectedType = when (which) {
|
||||
0 -> FileManager.FileType.IMAGE
|
||||
1 -> FileManager.FileType.VIDEO
|
||||
2 -> FileManager.FileType.AUDIO
|
||||
else -> FileManager.FileType.DOCUMENT
|
||||
}
|
||||
performDecryptionWithType(file, selectedType)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun openInPreview(fileType: FileManager.FileType) {
|
||||
val fileTypeString = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypeString)
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun openAudioFile(file: File) {
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||
try {
|
||||
@@ -332,7 +240,7 @@ class FileAdapter(
|
||||
putExtra("position", adapterPosition)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_audio_player_found),
|
||||
@@ -354,7 +262,7 @@ class FileAdapter(
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
@@ -364,84 +272,126 @@ class FileAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInPreview(fileType: FileManager.FileType) {
|
||||
val fileTypeString = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||
else -> "unknown"
|
||||
private fun setupDisplay(
|
||||
file: File,
|
||||
type: FileManager.FileType,
|
||||
isCurrentFileEncrypted: Boolean,
|
||||
metadata: HiddenFileEntity?,
|
||||
) {
|
||||
when (type) {
|
||||
FileManager.FileType.IMAGE -> {
|
||||
binding.videoPlay.visibility = View.GONE
|
||||
binding.fileIconImageView.setPadding(0, 0, 0, 0)
|
||||
if (isCurrentFileEncrypted) {
|
||||
try {
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.skipMemoryCache(false)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(binding.fileIconImageView)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("FileAdapter", "Error displaying encrypted image: ${e.message}")
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.into(binding.fileIconImageView)
|
||||
}
|
||||
}
|
||||
|
||||
FileManager.FileType.VIDEO -> {
|
||||
binding.fileIconImageView.setPadding(0, 0, 0, 0)
|
||||
binding.videoPlay.visibility = View.VISIBLE
|
||||
if (isCurrentFileEncrypted) {
|
||||
try {
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(binding.fileIconImageView)
|
||||
binding.fileIconImageView.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("FileAdapter", "Error displaying encrypted video: ${e.message}")
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.into(binding.fileIconImageView)
|
||||
}
|
||||
}
|
||||
|
||||
FileManager.FileType.AUDIO -> {
|
||||
binding.videoPlay.visibility = View.GONE
|
||||
binding.fileIconImageView.setPadding(25, 25, 25, 25)
|
||||
binding.fileIconImageView.setImageResource(R.drawable.ic_audio)
|
||||
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.videoPlay.visibility = View.GONE
|
||||
binding.fileIconImageView.setPadding(25, 25, 25, 25)
|
||||
binding.fileIconImageView.setImageResource(R.drawable.ic_document)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEncryptedIcon() {
|
||||
binding.fileIconImageView.setImageResource(R.drawable.encrypted)
|
||||
}
|
||||
|
||||
|
||||
private fun updateSelectionUI(isSelected: Boolean) {
|
||||
binding.selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
binding.selected.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
|
||||
itemView.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
|
||||
if (isSelectionMode) {
|
||||
toggleSelection(position)
|
||||
} else {
|
||||
openFile(file,fileType)
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypeString)
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||
|
||||
@SuppressLint("MissingInflatedId")
|
||||
private fun renameFile(file: File) {
|
||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
inputEditText.setText(file.name)
|
||||
inputEditText.selectAll()
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(context.getString(R.string.rename_file))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(context.getString(R.string.rename)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty() && newName != file.name) {
|
||||
if (isValidFileName(newName)) {
|
||||
renameFileAsync(file, newName)
|
||||
} else {
|
||||
Toast.makeText(context, "Invalid file name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun isValidFileName(fileName: String): Boolean {
|
||||
val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
|
||||
return fileName.isNotBlank() &&
|
||||
fileName.none { it in forbiddenChars } &&
|
||||
!fileName.startsWith(".") &&
|
||||
fileName.length <= 255
|
||||
}
|
||||
|
||||
private fun renameFileAsync(file: File, newName: String) {
|
||||
fileExecutor.execute {
|
||||
val parentDir = file.parentFile
|
||||
if (parentDir != null) {
|
||||
val newFile = File(parentDir, newName)
|
||||
if (newFile.exists()) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "File with this name already exists", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val success = try {
|
||||
file.renameTo(newFile)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
if (success) {
|
||||
fileOperationCallback?.get()?.onFileRenamed(file, newFile)
|
||||
Toast.makeText(context, "File renamed", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to rename file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
toggleSelection(position)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,29 +409,6 @@ class FileAdapter(
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
private fun showEncryptedIcon() {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
|
||||
private fun showDecryptionTypeDialog(file: File) {
|
||||
val options = arrayOf("Image", "Video", "Audio")
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Select File Type")
|
||||
.setMessage("Please select the type of file to decrypt")
|
||||
.setItems(options) { _, which ->
|
||||
val selectedType = when (which) {
|
||||
0 -> FileManager.FileType.IMAGE
|
||||
1 -> FileManager.FileType.VIDEO
|
||||
2 -> FileManager.FileType.AUDIO
|
||||
else -> FileManager.FileType.DOCUMENT
|
||||
}
|
||||
performDecryptionWithType(file, selectedType)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
@@ -491,7 +418,7 @@ class FileAdapter(
|
||||
FileManager.FileType.AUDIO -> ".mp3"
|
||||
else -> ".txt"
|
||||
}
|
||||
|
||||
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
@@ -536,7 +463,7 @@ class FileAdapter(
|
||||
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -545,39 +472,20 @@ class FileAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_file, parent, false)
|
||||
return FileViewHolder(view)
|
||||
}
|
||||
fun isInSelectionMode(): Boolean = isSelectionMode
|
||||
|
||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
|
||||
if (position < itemCount) {
|
||||
val file = getItem(position)
|
||||
holder.bind(file)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
fun onBackPressed(): Boolean {
|
||||
return if (isSelectionMode) {
|
||||
exitSelectionMode()
|
||||
true
|
||||
} else {
|
||||
if (position < itemCount) {
|
||||
val file = getItem(position)
|
||||
holder.bind(file, payloads)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitList(list: List<File>?) {
|
||||
val currentList = currentList.toMutableList()
|
||||
if (list == null) {
|
||||
currentList.clear()
|
||||
super.submitList(null)
|
||||
} else {
|
||||
val newList = list.filter { it.name != ".nomedia" }.toMutableList()
|
||||
super.submitList(newList)
|
||||
}
|
||||
|
||||
private fun onSelectionCountChanged(count: Int) {
|
||||
filesOperationCallback?.get()?.onSelectionCountChanged(count)
|
||||
}
|
||||
|
||||
fun enterSelectionMode() {
|
||||
@@ -591,92 +499,35 @@ class FileAdapter(
|
||||
fun exitSelectionMode() {
|
||||
if (isSelectionMode) {
|
||||
isSelectionMode = false
|
||||
selectedItems.forEach { position ->
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
selectedItems.clear()
|
||||
notifySelectionModeChange()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
if (selectedItems.isNotEmpty()) {
|
||||
val previouslySelected = selectedItems.toSet()
|
||||
selectedItems.clear()
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(0)
|
||||
previouslySelected.forEach { position ->
|
||||
if (position < itemCount) {
|
||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
}
|
||||
val previouslySelected = selectedItems.toSet()
|
||||
selectedItems.clear()
|
||||
for (i in 0 until itemCount) {
|
||||
selectedItems.add(i)
|
||||
}
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
|
||||
updateSelectionItems(selectedItems.toSet(), previouslySelected)
|
||||
}
|
||||
|
||||
private fun updateSelectionItems(newSelections: Set<Int>, oldSelections: Set<Int>) {
|
||||
val changedItems = (oldSelections - newSelections) + (newSelections - oldSelections)
|
||||
changedItems.forEach { position ->
|
||||
if (position < itemCount) {
|
||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifySelectionModeChange() {
|
||||
fileOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
|
||||
filesOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
|
||||
onFolderLongClick(isSelectionMode)
|
||||
}
|
||||
|
||||
fun getSelectedItems(): List<File> {
|
||||
return selectedItems.mapNotNull { position ->
|
||||
if (position < itemCount) getItem(position) else null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getSelectedCount(): Int = selectedItems.size
|
||||
|
||||
|
||||
fun isInSelectionMode(): Boolean = isSelectionMode
|
||||
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return if (isSelectionMode) {
|
||||
exitSelectionMode()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
try {
|
||||
if (!fileExecutor.isShutdown) {
|
||||
fileExecutor.shutdown()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
fileOperationCallback?.clear()
|
||||
fileOperationCallback = null
|
||||
}
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
cleanup()
|
||||
filesOperationCallback?.clear()
|
||||
filesOperationCallback = null
|
||||
}
|
||||
|
||||
private fun onSelectionCountChanged(count: Int) {
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(count)
|
||||
fun getSelectedItems(): List<File> {
|
||||
return selectedItems.mapNotNull { position ->
|
||||
if (position < itemCount) getItem(position) else null
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptSelectedFiles() {
|
||||
@@ -687,85 +538,16 @@ class FileAdapter(
|
||||
.setTitle(context.getString(R.string.encrypt_files))
|
||||
.setMessage(context.getString(R.string.encryption_disclaimer))
|
||||
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
|
||||
performEncryption(selectedFiles)
|
||||
FileManager(context, lifecycleOwner).performEncryption(
|
||||
selectedFiles
|
||||
) {
|
||||
updateItemsAfterEncryption(it)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performEncryption(selectedFiles: List<File>) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val updatedFiles = mutableListOf<File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true) continue
|
||||
val originalExtension = ".${file.extension.lowercase()}"
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
|
||||
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
|
||||
if (encryptedFile.exists()) {
|
||||
if (hiddenFile == null){
|
||||
hiddenFileRepository.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = encryptedFile.absolutePath,
|
||||
isEncrypted = true,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
fileType = fileType,
|
||||
fileName = file.name,
|
||||
originalExtension = originalExtension
|
||||
)
|
||||
)
|
||||
}else{
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = hiddenFile.filePath,
|
||||
newFilePath = encryptedFile.absolutePath,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
isEncrypted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
if (file.delete()) {
|
||||
updatedFiles.add(encryptedFile)
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
failCount > 0 -> {
|
||||
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||
submitList(currentFiles)
|
||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptSelectedFiles() {
|
||||
val selectedFiles = getSelectedItems()
|
||||
if (selectedFiles.isEmpty()) return
|
||||
@@ -774,73 +556,40 @@ class FileAdapter(
|
||||
.setTitle(context.getString(R.string.decrypt_files))
|
||||
.setMessage(context.getString(R.string.decryption_disclaimer))
|
||||
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
|
||||
performDecryption(selectedFiles)
|
||||
FileManager(context, lifecycleOwner).performDecryption(selectedFiles) {
|
||||
updateItemsAfterDecryption(it)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performDecryption(selectedFiles: List<File>) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val updatedFiles = mutableListOf<File>()
|
||||
fun updateItemsAfterEncryption(encryptedFiles: Map<File, File>) {
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted != true) continue
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
newFilePath = decryptedFile.absolutePath,
|
||||
encryptedFileName = decryptedFile.name,
|
||||
isEncrypted = false
|
||||
)
|
||||
}
|
||||
if (file.delete()) {
|
||||
updatedFiles.add(decryptedFile)
|
||||
successCount++
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
val currentList = FolderManager().getFilesInFolder(currentFolder)
|
||||
val updatedList = currentList.map { file ->
|
||||
encryptedFiles[file] ?: file
|
||||
}.toMutableList()
|
||||
selectedItems.clear()
|
||||
exitSelectionMode()
|
||||
submitList(updatedList)
|
||||
mainHandler.postDelayed({
|
||||
|
||||
mainHandler.post {
|
||||
exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
failCount > 0 -> {
|
||||
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||
submitList(currentFiles)
|
||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||
}
|
||||
}
|
||||
filesOperationCallback?.get()?.onRefreshNeeded()
|
||||
},20)
|
||||
}
|
||||
|
||||
fun updateItemsAfterDecryption(decryptedFiles: Map<File, File>) {
|
||||
val currentList = FolderManager().getFilesInFolder(currentFolder)
|
||||
val updatedList = currentList.map { file ->
|
||||
decryptedFiles[file] ?: file
|
||||
}.toMutableList()
|
||||
selectedItems.clear()
|
||||
exitSelectionMode()
|
||||
submitList(updatedList)
|
||||
mainHandler.postDelayed({
|
||||
|
||||
filesOperationCallback?.get()?.onRefreshNeeded()
|
||||
},20)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||
changes.add("EXISTENCE_CHANGED")
|
||||
}
|
||||
|
||||
if (oldItem.absolutePath != newItem.absolutePath) {
|
||||
changes.add("FILE_CHANGED")
|
||||
}
|
||||
|
||||
return changes.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -71,109 +72,124 @@ class ImagePreviewAdapter(
|
||||
private var currentPosition = 0
|
||||
private var tempDecryptedFile: File? = null
|
||||
|
||||
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
|
||||
fun bind(file: File, position: Int, decryptedFileType: FileManager.FileType) {
|
||||
currentPosition = position
|
||||
|
||||
releaseMediaPlayer()
|
||||
resetAudioUI()
|
||||
cleanupTempFile()
|
||||
|
||||
try {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile != null){
|
||||
if (hiddenFile != null) {
|
||||
val isEncrypted = hiddenFile.isEncrypted
|
||||
val fileType = hiddenFile.fileType
|
||||
if (isEncrypted) {
|
||||
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
||||
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
|
||||
displayFile(tempDecryptedFile, fileType,true)
|
||||
tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
||||
if (tempDecryptedFile != null && tempDecryptedFile!!.exists() && tempDecryptedFile!!.length() > 0) {
|
||||
displayFile(tempDecryptedFile!!, fileType, true)
|
||||
} else {
|
||||
Log.e("ImagePreviewAdapter", "Failed to get decrypted preview file for: ${file.absolutePath}")
|
||||
showEncryptedError()
|
||||
}
|
||||
} else {
|
||||
displayFile(file, decryptedFileType,false)
|
||||
displayFile(file, decryptedFileType, false)
|
||||
}
|
||||
}else{
|
||||
displayFile(file, decryptedFileType,false)
|
||||
} else {
|
||||
displayFile(file, decryptedFileType, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
|
||||
displayFile(file, decryptedFileType,false)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImagePreviewAdapter", "Error in bind: ${e.message}")
|
||||
displayFile(file, decryptedFileType, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
|
||||
val uri = getUriForPreviewFile(context, file)
|
||||
when (fileType) {
|
||||
FileManager.FileType.VIDEO -> {
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.VISIBLE
|
||||
private fun displayFile(file: File, fileType: FileManager.FileType, isEncrypted: Boolean = false) {
|
||||
try {
|
||||
val uri = if (isEncrypted) {
|
||||
getUriForPreviewFile(context, file)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
val videoUri = if (isEncrypted){
|
||||
uri
|
||||
}else{
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
binding.videoView.setVideoURI(videoUri)
|
||||
binding.videoView.start()
|
||||
if (uri == null) {
|
||||
Log.e("ImagePreviewAdapter", "Failed to get URI for file: ${file.absolutePath}")
|
||||
showEncryptedError()
|
||||
return
|
||||
}
|
||||
|
||||
val mediaController = MediaController(context)
|
||||
mediaController.setAnchorView(binding.videoView)
|
||||
binding.videoView.setMediaController(mediaController)
|
||||
when (fileType) {
|
||||
FileManager.FileType.VIDEO -> {
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.VISIBLE
|
||||
|
||||
mediaController.setPrevNextListeners(
|
||||
{
|
||||
binding.videoView.setVideoURI(uri)
|
||||
binding.videoView.start()
|
||||
|
||||
val mediaController = MediaController(context)
|
||||
mediaController.setAnchorView(binding.videoView)
|
||||
binding.videoView.setMediaController(mediaController)
|
||||
|
||||
mediaController.setPrevNextListeners(
|
||||
{
|
||||
val nextPosition = (adapterPosition + 1) % images.size
|
||||
playVideoAtPosition(nextPosition)
|
||||
},
|
||||
{
|
||||
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
|
||||
playVideoAtPosition(prevPosition)
|
||||
}
|
||||
)
|
||||
|
||||
binding.videoView.setOnCompletionListener {
|
||||
val nextPosition = (adapterPosition + 1) % images.size
|
||||
playVideoAtPosition(nextPosition)
|
||||
},
|
||||
{
|
||||
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
|
||||
playVideoAtPosition(prevPosition)
|
||||
}
|
||||
)
|
||||
}
|
||||
FileManager.FileType.IMAGE -> {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(binding.imageView)
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
val audioFile: File? = if (isEncrypted) {
|
||||
getFileFromUri(context, uri)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
if (audioFile == null) {
|
||||
Log.e("ImagePreviewAdapter", "Failed to get audio file from URI")
|
||||
showEncryptedError()
|
||||
return
|
||||
}
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioTitle.text = file.name
|
||||
|
||||
binding.videoView.setOnCompletionListener {
|
||||
val nextPosition = (adapterPosition + 1) % images.size
|
||||
playVideoAtPosition(nextPosition)
|
||||
setupAudioPlayer(audioFile)
|
||||
setupPlaybackControls()
|
||||
}
|
||||
else -> {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.GONE
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(binding.imageView)
|
||||
}
|
||||
}
|
||||
FileManager.FileType.IMAGE -> {
|
||||
val imageUri = if (isEncrypted){
|
||||
uri
|
||||
}else{
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
Glide.with(context)
|
||||
.load(imageUri)
|
||||
.into(binding.imageView)
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
val audioFile: File? = if (isEncrypted) {
|
||||
getFileFromUri(context, uri!!)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioTitle.text = file.name
|
||||
|
||||
setupAudioPlayer(audioFile!!)
|
||||
setupPlaybackControls()
|
||||
}
|
||||
else -> {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImagePreviewAdapter", "Error displaying file: ${e.message}")
|
||||
showEncryptedError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,11 +215,12 @@ class ImagePreviewAdapter(
|
||||
}
|
||||
|
||||
private fun cleanupTempFile() {
|
||||
tempDecryptedFile?.let { file ->
|
||||
if (file.exists()) {
|
||||
tempDecryptedFile?.let {
|
||||
if (it.exists()) {
|
||||
try {
|
||||
file.delete()
|
||||
} catch (_: Exception) {
|
||||
it.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImagePreviewAdapter", "Error cleaning up temp file: ${e.message}")
|
||||
}
|
||||
}
|
||||
tempDecryptedFile = null
|
||||
|
||||
@@ -25,6 +25,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import android.Manifest
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.content.FileProvider
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
@@ -37,7 +39,6 @@ import java.io.FileOutputStream
|
||||
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
|
||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||
val intent = Intent()
|
||||
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
|
||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
@@ -67,15 +68,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
return dir
|
||||
}
|
||||
|
||||
fun getFilesInHiddenDir(type: FileType): List<File> {
|
||||
val hiddenDir = getHiddenDirectory()
|
||||
val typeDir = File(hiddenDir, type.dirName)
|
||||
if (!typeDir.exists()) {
|
||||
typeDir.mkdirs()
|
||||
File(typeDir, ".nomedia").createNewFile()
|
||||
}
|
||||
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
|
||||
}
|
||||
fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List<File> {
|
||||
val typeDir = File(folder)
|
||||
if (!typeDir.exists()) {
|
||||
@@ -117,8 +109,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
throw Exception("File copy failed")
|
||||
}
|
||||
|
||||
// Encrypt file if encryption is enabled
|
||||
|
||||
// Media scan the new file to hide it
|
||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||
mediaScanIntent.data = Uri.fromFile(targetDir)
|
||||
@@ -172,62 +162,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun unHideFile(file: File, onSuccess: (() -> Unit)? = null, onError: ((String) -> Unit)? = null) {
|
||||
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Create target directory (Downloads)
|
||||
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
targetDir.mkdirs()
|
||||
|
||||
// Create target file with same name or timestamp
|
||||
val targetFile = File(targetDir, file.name)
|
||||
|
||||
// If file with same name exists, add timestamp
|
||||
val finalTargetFile = if (targetFile.exists()) {
|
||||
val nameWithoutExt = file.nameWithoutExtension
|
||||
val extension = file.extension
|
||||
File(targetDir, "${nameWithoutExt}_${System.currentTimeMillis()}.${extension}")
|
||||
} else {
|
||||
targetFile
|
||||
}
|
||||
|
||||
// Check if file is encrypted
|
||||
if (file.extension == ENCRYPTED_EXTENSION) {
|
||||
// Decrypt file
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, SecurityUtils.getFileExtension(file))
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
decryptedFile.copyTo(finalTargetFile, overwrite = false)
|
||||
decryptedFile.delete()
|
||||
} else {
|
||||
throw Exception("Failed to decrypt file")
|
||||
}
|
||||
} else {
|
||||
// Copy file content
|
||||
file.copyTo(finalTargetFile, overwrite = false)
|
||||
}
|
||||
|
||||
// Verify copy success
|
||||
if (!finalTargetFile.exists() || finalTargetFile.length() == 0L) {
|
||||
throw Exception("File copy failed")
|
||||
}
|
||||
|
||||
// Media scan the new file
|
||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
|
||||
context.sendBroadcast(mediaScanIntent)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
onError?.invoke(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -400,5 +334,141 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
ALL("all")
|
||||
}
|
||||
|
||||
fun performEncryption(selectedFiles: List<File>,onEncryptionEnded :(MutableMap<File, File>)-> Unit) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val encryptedFiles = mutableMapOf<File, File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true) continue
|
||||
val originalExtension = ".${file.extension.lowercase()}"
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||
val encryptedFile = SecurityUtils.changeFileExtension(file, devs.org.calculator.utils.FileManager.ENCRYPTED_EXTENSION)
|
||||
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
|
||||
if (encryptedFile.exists()) {
|
||||
if (hiddenFile == null){
|
||||
hiddenFileRepository.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = encryptedFile.absolutePath,
|
||||
isEncrypted = true,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
fileType = fileType,
|
||||
fileName = file.name,
|
||||
originalExtension = originalExtension
|
||||
)
|
||||
)
|
||||
}else{
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = hiddenFile.filePath,
|
||||
newFilePath = encryptedFile.absolutePath,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
isEncrypted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
if (file.delete()) {
|
||||
encryptedFiles[file] = encryptedFile
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post{
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Files encrypted successfully", Toast.LENGTH_SHORT).show()
|
||||
onEncryptionEnded(encryptedFiles)
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Some files could not be encrypted", Toast.LENGTH_SHORT).show()
|
||||
onEncryptionEnded(encryptedFiles)
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Failed to encrypt files", Toast.LENGTH_SHORT).show()
|
||||
onEncryptionEnded(encryptedFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun performDecryption(selectedFiles: List<File>,onDecryptionEnded :(MutableMap<File, File>) -> Unit) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val decryptedFiles = mutableMapOf<File, File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
newFilePath = decryptedFile.absolutePath,
|
||||
encryptedFileName = decryptedFile.name,
|
||||
isEncrypted = false
|
||||
)
|
||||
}
|
||||
if (file.delete()) {
|
||||
decryptedFiles[file] = decryptedFile
|
||||
successCount++
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post{
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Files decrypted successfully", Toast.LENGTH_SHORT).show()
|
||||
onDecryptionEnded(decryptedFiles)
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Some files could not be decrypted", Toast.LENGTH_SHORT).show()
|
||||
onDecryptionEnded(decryptedFiles)
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Failed to decrypt files", Toast.LENGTH_SHORT).show()
|
||||
onDecryptionEnded(decryptedFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -17,11 +17,13 @@ import android.content.SharedPreferences
|
||||
import androidx.core.content.FileProvider
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import androidx.core.content.edit
|
||||
import android.util.Log
|
||||
|
||||
object SecurityUtils {
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
private const val KEY_SIZE = 256
|
||||
val ENCRYPTED_EXTENSION = ".enc"
|
||||
|
||||
private fun getSecretKey(context: Context): SecretKey {
|
||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||
@@ -109,44 +111,59 @@ object SecurityUtils {
|
||||
try {
|
||||
val encryptedFile = File(meta.filePath)
|
||||
if (!encryptedFile.exists()) {
|
||||
Log.e("SecurityUtils", "Encrypted file does not exist: ${meta.filePath}")
|
||||
return null
|
||||
}
|
||||
|
||||
val tempDir = File(context.cacheDir, "preview_temp")
|
||||
if (!tempDir.exists()) tempDir.mkdirs()
|
||||
if (!tempDir.exists()) {
|
||||
if (!tempDir.mkdirs()) {
|
||||
Log.e("SecurityUtils", "Failed to create temp directory")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old preview files
|
||||
tempDir.listFiles()?.forEach {
|
||||
if (it.lastModified() < System.currentTimeMillis() - 5 * 60 * 1000) { // 5 minutes
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
|
||||
|
||||
tempDir.listFiles()?.forEach { it.delete() }
|
||||
|
||||
|
||||
val success = decryptFile(context, encryptedFile, tempFile)
|
||||
|
||||
if (success && tempFile.exists() && tempFile.length() > 0) {
|
||||
return tempFile
|
||||
} else {
|
||||
Log.e("SecurityUtils", "Failed to decrypt preview file: ${meta.filePath}")
|
||||
if (tempFile.exists()) tempFile.delete()
|
||||
return null
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecurityUtils", "Error in getDecryptedPreviewFile: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getUriForPreviewFile(context: Context, file: File): Uri? {
|
||||
return try {
|
||||
if (!file.exists() || file.length() == 0L) {
|
||||
Log.e("SecurityUtils", "Preview file does not exist or is empty: ${file.absolutePath}")
|
||||
return null
|
||||
}
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider", // Must match AndroidManifest
|
||||
"${context.packageName}.provider",
|
||||
file
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecurityUtils", "Error getting URI for preview file: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||
return try {
|
||||
if (!inputFile.exists()) {
|
||||
|
||||
@@ -49,16 +49,22 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:id="@+id/swipeLayout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolBar">
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolBar"
|
||||
android:layout_height="0dp">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/noItems"
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/add_image" />
|
||||
android:scaleType="centerCrop"/>
|
||||
<View
|
||||
android:id="@+id/shade"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
Reference in New Issue
Block a user