Fixed - File loading bugs, optimized refresh using DiffUtil more efficiently

This commit is contained in:
Binondi
2025-06-07 19:24:32 +05:30
parent 5c5e0e4be8
commit 2de1b28afe
10 changed files with 623 additions and 731 deletions

View File

@@ -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)

View File

@@ -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 }

View File

@@ -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)
}
}

View File

@@ -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 }
}
}

View File

@@ -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

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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()) {

View File

@@ -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"

View File

@@ -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"