Added - file encryption, custom key creation.

This commit is contained in:
Binondi
2025-06-06 15:16:21 +05:30
parent 5aafc7e411
commit 191368bdf8
15 changed files with 646 additions and 203 deletions

View File

@@ -13,7 +13,6 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.adapters.FolderAdapter import devs.org.calculator.adapters.FolderAdapter
@@ -138,7 +137,7 @@ class HiddenActivity : AppCompatActivity() {
} else { } else {
showEmptyState() showEmptyState()
} }
} catch (e: Exception) { } catch (_: Exception) {
showEmptyState() showEmptyState()
} }
@@ -158,25 +157,25 @@ class HiddenActivity : AppCompatActivity() {
val inputEditText = dialogView.findViewById<EditText>(R.id.editText) val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Enter Folder Name To Create") .setTitle(getString(R.string.enter_folder_name_to_create))
.setView(dialogView) .setView(dialogView)
.setPositiveButton("Create") { dialog, _ -> .setPositiveButton(getString(R.string.create)) { dialog, _ ->
val newName = inputEditText.text.toString().trim() val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty()) { if (newName.isNotEmpty()) {
try { try {
folderManager.createFolder(hiddenDir, newName) folderManager.createFolder(hiddenDir, newName)
refreshCurrentView() refreshCurrentView()
} catch (e: Exception) { } catch (_: Exception) {
Toast.makeText( Toast.makeText(
this@HiddenActivity, this@HiddenActivity,
"Failed to create folder", getString(R.string.failed_to_create_folder),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} }
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> .setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
.show() .show()
@@ -214,7 +213,7 @@ class HiddenActivity : AppCompatActivity() {
} else { } else {
showEmptyState() showEmptyState()
} }
} catch (e: Exception) { } catch (_: Exception) {
showEmptyState() showEmptyState()
} }
} }
@@ -434,7 +433,8 @@ class HiddenActivity : AppCompatActivity() {
} }
if (selectedFolders.size != 1) { if (selectedFolders.size != 1) {
Toast.makeText(this, "Please select exactly one folder to edit", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.please_select_exactly_one_folder_to_edit), Toast.LENGTH_SHORT).show()
return return
} }
@@ -449,20 +449,21 @@ class HiddenActivity : AppCompatActivity() {
inputEditText.selectAll() inputEditText.selectAll()
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Rename Folder") .setTitle(getString(R.string.rename_folder))
.setView(dialogView) .setView(dialogView)
.setPositiveButton("Rename") { dialog, _ -> .setPositiveButton(getString(R.string.rename)) { dialog, _ ->
val newName = inputEditText.text.toString().trim() val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != folder.name) { if (newName.isNotEmpty() && newName != folder.name) {
if (isValidFolderName(newName)) { if (isValidFolderName(newName)) {
renameFolder(folder, newName) renameFolder(folder, newName)
} else { } else {
Toast.makeText(this, "Invalid folder name", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.invalid_folder_name), Toast.LENGTH_SHORT).show()
} }
} }
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> .setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
.show() .show()
@@ -481,7 +482,8 @@ class HiddenActivity : AppCompatActivity() {
if (parentDir != null) { if (parentDir != null) {
val newFolder = File(parentDir, newName) val newFolder = File(parentDir, newName)
if (newFolder.exists()) { if (newFolder.exists()) {
Toast.makeText(this, "Folder with this name already exists", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.folder_with_this_name_already_exists), Toast.LENGTH_SHORT).show()
return return
} }
@@ -492,7 +494,7 @@ class HiddenActivity : AppCompatActivity() {
refreshCurrentView() refreshCurrentView()
} else { } else {
Toast.makeText(this, "Failed to rename folder", Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.failed_to_rename_folder), Toast.LENGTH_SHORT).show()
} }
} }
} }

View File

@@ -24,7 +24,6 @@ import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder import net.objecthunter.exp4j.ExpressionBuilder
import java.util.regex.Pattern import java.util.regex.Pattern
import androidx.core.content.edit import androidx.core.content.edit
import com.google.android.material.color.DynamicColors
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback { class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -37,7 +36,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
private val dialogUtil = DialogUtil(this) private val dialogUtil = DialogUtil(this)
private val fileManager = FileManager(this, this) private val fileManager = FileManager(this, this)
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) } private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -276,7 +274,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
private fun evaluateExpression(expression: String): Double { private fun evaluateExpression(expression: String): Double {
return try { return try {
ExpressionBuilder(expression).build().evaluate() ExpressionBuilder(expression).build().evaluate()
} catch (e: Exception) { } catch (_: Exception) {
expression.toDouble() expression.toDouble()
} }
} }
@@ -359,7 +357,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){ if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
binding.total.text = getString(R.string.now_enter_button) binding.total.text = getString(R.string.now_enter_button)
}else binding.total.text = formattedResult }else binding.total.text = formattedResult
} catch (e: Exception) { } catch (_: Exception) {
binding.total.text = "" binding.total.text = ""
} }
} }
@@ -372,7 +370,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
val lastChar = currentExpression.last() val lastChar = currentExpression.last()
currentExpression = currentExpression.substring(0, currentExpression.length - 1) currentExpression = currentExpression.substring(0, currentExpression.length - 1)
// Update flags based on what was removed
if (lastChar == '%') { if (lastChar == '%') {
lastWasPercent = false lastWasPercent = false
} else if (isOperator(lastChar.toString())) { } else if (isOperator(lastChar.toString())) {

View File

@@ -6,18 +6,16 @@ import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.color.DynamicColors
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.adapters.ImagePreviewAdapter import devs.org.calculator.adapters.ImagePreviewAdapter
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ActivityPreviewBinding import devs.org.calculator.databinding.ActivityPreviewBinding
import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil import devs.org.calculator.utils.PrefsUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import android.util.Log
class PreviewActivity : AppCompatActivity() { class PreviewActivity : AppCompatActivity() {
@@ -35,10 +33,6 @@ class PreviewActivity : AppCompatActivity() {
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao()) HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
} }
companion object {
private const val TAG = "PreviewActivity"
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityPreviewBinding.inflate(layoutInflater) binding = ActivityPreviewBinding.inflate(layoutInflater)
@@ -73,7 +67,6 @@ class PreviewActivity : AppCompatActivity() {
} }
private fun setupFlagSecure() { private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) { if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
@@ -188,18 +181,15 @@ class PreviewActivity : AppCompatActivity() {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
// First delete from database
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
hiddenFile?.let { hiddenFile?.let {
hiddenFileRepository.deleteHiddenFile(it) hiddenFileRepository.deleteHiddenFile(it)
Log.d(TAG, "Deleted file metadata from database: ${it.filePath}")
} }
// Then delete the actual file
fileManager.deletePhotoFromExternalStorage(fileUri) fileManager.deletePhotoFromExternalStorage(fileUri)
removeFileFromList(currentPosition) removeFileFromList(currentPosition)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error deleting file: ${e.message}", e) e.printStackTrace()
} }
} }
} }
@@ -228,19 +218,17 @@ class PreviewActivity : AppCompatActivity() {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
// First copy the file to normal directory
val result = fileManager.copyFileToNormalDir(fileUri) val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) { if (result != null) {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
hiddenFile?.let { hiddenFile?.let {
hiddenFileRepository.deleteHiddenFile(it) hiddenFileRepository.deleteHiddenFile(it)
Log.d(TAG, "Deleted file metadata from database: ${it.filePath}")
} }
removeFileFromList(currentPosition) removeFileFromList(currentPosition)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error unhiding file: ${e.message}", e) e.printStackTrace()
} }
} }
} }

View File

@@ -4,6 +4,8 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -13,11 +15,12 @@ import com.google.android.material.snackbar.Snackbar
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.databinding.ActivitySettingsBinding import devs.org.calculator.databinding.ActivitySettingsBinding
import devs.org.calculator.utils.PrefsUtil import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding private lateinit var binding: ActivitySettingsBinding
private val prefs:PrefsUtil by lazy { PrefsUtil(this) } private lateinit var prefs: PrefsUtil
private var DEV_GITHUB_URL = "" private var DEV_GITHUB_URL = ""
private var GITHUB_URL = "" private var GITHUB_URL = ""
@@ -25,6 +28,7 @@ class SettingsActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
prefs = PrefsUtil(this)
DEV_GITHUB_URL = getString(R.string.github_profile) DEV_GITHUB_URL = getString(R.string.github_profile)
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL) GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
setupUI() setupUI()
@@ -52,6 +56,8 @@ class SettingsActivity : AppCompatActivity() {
else -> binding.systemThemeRadio.isChecked = true else -> binding.systemThemeRadio.isChecked = true
} }
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
binding.customKeyStatus.isChecked = isUsingCustomKey
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true) binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true) binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false) binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
@@ -113,6 +119,10 @@ class SettingsActivity : AppCompatActivity() {
binding.showFileNames.setOnCheckedChangeListener { _, isChecked -> binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
prefs.setBoolean("showFileName", isChecked) prefs.setBoolean("showFileName", isChecked)
} }
binding.customKeyStatus.setOnClickListener {
showCustomKeyDialog()
}
} }
private fun updateThemeModeVisibility() { private fun updateThemeModeVisibility() {
@@ -154,4 +164,57 @@ class SettingsActivity : AppCompatActivity() {
getString(R.string.could_not_open_url), Snackbar.LENGTH_SHORT).show() getString(R.string.could_not_open_url), Snackbar.LENGTH_SHORT).show()
} }
} }
private fun showCustomKeyDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_custom_key, null)
val keyInput = dialogView.findViewById<EditText>(R.id.keyInput)
val confirmKeyInput = dialogView.findViewById<EditText>(R.id.confirmKeyInput)
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.set_custom_encryption_key))
.setView(dialogView)
.setPositiveButton(getString(R.string.set)) { dialog, _ ->
val key = keyInput.text.toString()
val confirmKey = confirmKeyInput.text.toString()
if (key.isEmpty()) {
Toast.makeText(this, getString(R.string.key_cannot_be_empty), Toast.LENGTH_SHORT).show()
updateUI()
return@setPositiveButton
}
if (key != confirmKey) {
Toast.makeText(this, getString(R.string.keys_do_not_match), Toast.LENGTH_SHORT).show()
updateUI()
return@setPositiveButton
}
if (SecurityUtils.setCustomKey(this, key)) {
Toast.makeText(this,
getString(R.string.custom_key_set_successfully), Toast.LENGTH_SHORT).show()
updateUI()
} else {
Toast.makeText(this,
getString(R.string.failed_to_set_custom_key), Toast.LENGTH_SHORT).show()
updateUI()
}
}
.setNegativeButton(getString(R.string.cancel)){ _, _ ->
updateUI()
}
.setNeutralButton(getString(R.string.delete_key)) { _, _ ->
SecurityUtils.clearCustomKey(this)
Toast.makeText(this,
getString(R.string.custom_encryption_key_cleared), Toast.LENGTH_SHORT).show()
updateUI()
}
.show()
}
private fun updateUI() {
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
binding.customKeyStatus.isChecked = isUsingCustomKey
}
} }

View File

@@ -38,6 +38,9 @@ import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import android.widget.CheckBox
import android.widget.CompoundButton
import android.app.AlertDialog
class ViewFolderActivity : AppCompatActivity() { class ViewFolderActivity : AppCompatActivity() {
@@ -166,7 +169,6 @@ class ViewFolderActivity : AppCompatActivity() {
object : FileProcessCallback { object : FileProcessCallback {
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) { override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
mainHandler.post { mainHandler.post {
// Add files to Room database
copiedFiles.forEach { file -> copiedFiles.forEach { file ->
val fileType = fileManager.getFileType(file) val fileType = fileManager.getFileType(file)
var finalFile = file var finalFile = file
@@ -331,14 +333,19 @@ class ViewFolderActivity : AppCompatActivity() {
if (selectedFiles.isEmpty()) return if (selectedFiles.isEmpty()) return
lifecycleScope.launch { lifecycleScope.launch {
// Check if any files are encrypted
var hasEncryptedFiles = false var hasEncryptedFiles = false
var hasDecryptedFiles = false var hasDecryptedFiles = false
var hasEncFilesWithoutMetadata = false
for (file in selectedFiles) { for (file in selectedFiles) {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath) val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
hasEncryptedFiles = true if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
if (hiddenFile?.isEncrypted == true) {
hasEncryptedFiles = true
} else {
hasEncFilesWithoutMetadata = true
}
} else { } else {
hasDecryptedFiles = true hasDecryptedFiles = true
} }
@@ -351,14 +358,14 @@ class ViewFolderActivity : AppCompatActivity() {
getString(R.string.move_to_another_folder) getString(R.string.move_to_another_folder)
) )
// Add encryption/decryption options based on file status
if (hasDecryptedFiles) { if (hasDecryptedFiles) {
options.add(getString(R.string.encrypt_file)) options.add(getString(R.string.encrypt_file))
} }
if (hasEncryptedFiles) { if (hasEncryptedFiles || hasEncFilesWithoutMetadata) {
options.add(getString(R.string.decrypt_file)) options.add(getString(R.string.decrypt_file))
} }
MaterialAlertDialogBuilder(this@ViewFolderActivity) MaterialAlertDialogBuilder(this@ViewFolderActivity)
.setTitle(getString(R.string.file_options)) .setTitle(getString(R.string.file_options))
.setItems(options.toTypedArray()) { _, which -> .setItems(options.toTypedArray()) { _, which ->
@@ -371,7 +378,20 @@ class ViewFolderActivity : AppCompatActivity() {
val option = options[which] val option = options[which]
when (option) { when (option) {
getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles() getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles()
getString(R.string.decrypt_file) -> fileAdapter?.decryptSelectedFiles() getString(R.string.decrypt_file) -> {
lifecycleScope.launch {
val filesWithoutMetadata = selectedFiles.filter { file ->
file.name.endsWith(ENCRYPTED_EXTENSION) &&
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
}
if (filesWithoutMetadata.isNotEmpty()) {
showDecryptionTypeDialog(filesWithoutMetadata)
} else {
fileAdapter?.decryptSelectedFiles()
}
}
}
} }
} }
} }
@@ -380,6 +400,178 @@ class ViewFolderActivity : AppCompatActivity() {
} }
} }
private fun showDecryptionTypeDialog(selectedFiles: List<File>) {
val dialogView = layoutInflater.inflate(R.layout.dialog_file_type_selection, null)
val imageCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxImage)
val videoCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxVideo)
val audioCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxAudio)
val checkboxes = listOf(imageCheckbox, videoCheckbox, audioCheckbox)
checkboxes.forEach { checkbox ->
checkbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
checkboxes.filter { it != checkbox }.forEach { it.isChecked = false }
}
}
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.select_file_type))
.setMessage(getString(R.string.please_select_the_type_of_file_to_decrypt))
.setView(dialogView)
.setNegativeButton(getString(R.string.cancel), null)
.setPositiveButton(getString(R.string.decrypt), null)
.create()
dialog.setOnShowListener {
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.isEnabled = false
val checkboxListener = CompoundButton.OnCheckedChangeListener { _, _ ->
positiveButton.isEnabled = checkboxes.any { it.isChecked }
}
checkboxes.forEach { it.setOnCheckedChangeListener(checkboxListener) }
}
dialog.setOnDismissListener {
checkboxes.forEach { it.setOnCheckedChangeListener(null) }
}
dialog.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val selectedType = when {
imageCheckbox.isChecked -> FileManager.FileType.IMAGE
videoCheckbox.isChecked -> FileManager.FileType.VIDEO
audioCheckbox.isChecked -> FileManager.FileType.AUDIO
else -> return@setOnClickListener
}
lifecycleScope.launch {
performDecryptionWithType(selectedFiles, selectedType)
}
dialog.dismiss()
}
}
private fun performDecryptionWithType(selectedFiles: List<File>, fileType: FileManager.FileType) {
lifecycleScope.launch {
var successCount = 0
var failCount = 0
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,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
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"
FileManager.FileType.AUDIO -> ".mp3"
else -> ".txt"
}
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,
fileName = decryptedFile.name,
encryptedFileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = false
)
)
if (file.delete()) {
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
mainHandler.post {
fileAdapter?.exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
refreshCurrentFolder()
}
}
}
private fun moveToAnotherFolder(selectedFiles: List<File>) { private fun moveToAnotherFolder(selectedFiles: List<File>) {
showFolderSelectionDialog { destinationFolder -> showFolderSelectionDialog { destinationFolder ->
moveFilesToFolder(selectedFiles, destinationFolder) moveFilesToFolder(selectedFiles, destinationFolder)
@@ -437,7 +629,7 @@ class ViewFolderActivity : AppCompatActivity() {
if (files.isNotEmpty()) { if (files.isNotEmpty()) {
binding.recyclerView.visibility = View.VISIBLE binding.recyclerView.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE binding.noItems.visibility = View.GONE
// Submit new list directly
fileAdapter?.submitList(files.toMutableList()) fileAdapter?.submitList(files.toMutableList())
fileAdapter?.let { adapter -> fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) { if (adapter.isInSelectionMode()) {
@@ -451,7 +643,7 @@ class ViewFolderActivity : AppCompatActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ViewFolderActivity", "Error refreshing folder: ${e.message}")
mainHandler.post { mainHandler.post {
showEmptyState() showEmptyState()
} }
@@ -610,7 +802,7 @@ class ViewFolderActivity : AppCompatActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ViewFolderActivity", "Error unhiding file: ${e.message}")
allUnhidden = false allUnhidden = false
} }
} }
@@ -642,7 +834,7 @@ class ViewFolderActivity : AppCompatActivity() {
allDeleted = false allDeleted = false
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ViewFolderActivity", "Error deleting file: ${e.message}")
allDeleted = false allDeleted = false
} }
} }
@@ -689,7 +881,7 @@ class ViewFolderActivity : AppCompatActivity() {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ViewFolderActivity", "Error copying file: ${e.message}")
allCopied = false allCopied = false
} }
} }
@@ -723,7 +915,7 @@ class ViewFolderActivity : AppCompatActivity() {
file.delete() file.delete()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ViewFolderActivity", "Error moving file: ${e.message}")
allMoved = false allMoved = false
} }
} }

View File

@@ -55,10 +55,6 @@ class FileAdapter(
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao()) HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
} }
companion object {
private const val TAG = "FileAdapter"
}
interface FileOperationCallback { interface FileOperationCallback {
fun onFileDeleted(file: File) fun onFileDeleted(file: File)
fun onFileRenamed(oldFile: File, newFile: File) fun onFileRenamed(oldFile: File, newFile: File)
@@ -76,6 +72,7 @@ class FileAdapter(
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView) val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
val playIcon: ImageView = view.findViewById(R.id.videoPlay) val playIcon: ImageView = view.findViewById(R.id.videoPlay)
val selectedLayer: View = view.findViewById(R.id.selectedLayer) 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 selected: ImageView = view.findViewById(R.id.selected)
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted) val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
@@ -91,6 +88,7 @@ class FileAdapter(
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile) setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
setupClickListeners(file, fileType) setupClickListeners(file, fileType)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
val position = adapterPosition val position = adapterPosition
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
@@ -99,11 +97,11 @@ class FileAdapter(
} }
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error binding file: ${e.message}")
} }
} }
} }
fun bind(file: File, payloads: List<Any>) { fun bind(file: File, payloads: List<Any>) {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
bind(file) bind(file)
@@ -163,7 +161,6 @@ class FileAdapter(
showEncryptedIcon() showEncryptedIcon()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error loading encrypted image preview: ${e.message}")
showEncryptedIcon() showEncryptedIcon()
} }
} else { } else {
@@ -200,7 +197,6 @@ class FileAdapter(
showEncryptedIcon() showEncryptedIcon()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error loading encrypted video preview: ${e.message}")
showEncryptedIcon() showEncryptedIcon()
} }
} else { } else {
@@ -260,7 +256,8 @@ class FileAdapter(
private fun openFile(file: File, fileType: FileManager.FileType) { private fun openFile(file: File, fileType: FileManager.FileType) {
if (!file.exists()) { if (!file.exists()) {
Toast.makeText(context, "File no longer exists", Toast.LENGTH_SHORT).show() Toast.makeText(context,
context.getString(R.string.file_no_longer_exists), Toast.LENGTH_SHORT).show()
return return
} }
@@ -270,46 +267,45 @@ class FileAdapter(
lifecycleOwner.lifecycleScope.launch { lifecycleOwner.lifecycleScope.launch {
try { try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) { if (hiddenFile?.isEncrypted == true || file.extension == FileManager.ENCRYPTED_EXTENSION) {
val tempFile = File(context.cacheDir, "preview_${file.name}") if (file.extension == FileManager.ENCRYPTED_EXTENSION && hiddenFile == null) {
Log.d(TAG, "Attempting to decrypt file for preview: ${file.absolutePath}") showDecryptionTypeDialog(file)
} else {
if (SecurityUtils.decryptFile(context, file, tempFile)) { val tempFile = File(context.cacheDir, "preview_${file.name}")
Log.d(TAG, "Successfully decrypted file for preview: ${tempFile.absolutePath}")
if (tempFile.exists() && tempFile.length() > 0) { if (SecurityUtils.decryptFile(context, file, tempFile)) {
mainHandler.post { if (tempFile.exists() && tempFile.length() > 0) {
val fileTypeString = when (fileType) { mainHandler.post {
FileManager.FileType.IMAGE -> context.getString(R.string.image) val fileTypeString = when (fileType) {
FileManager.FileType.VIDEO -> context.getString(R.string.video) FileManager.FileType.IMAGE -> context.getString(R.string.image)
else -> "unknown" FileManager.FileType.VIDEO -> context.getString(R.string.video)
} else -> "unknown"
}
val intent = Intent(context, PreviewActivity::class.java).apply { val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString) putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString()) putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition) putExtra("position", adapterPosition)
putExtra("isEncrypted", true) putExtra("isEncrypted", true)
putExtra("tempFile", tempFile.absolutePath) putExtra("tempFile", tempFile.absolutePath)
}
context.startActivity(intent)
}
} else {
mainHandler.post {
Toast.makeText(context, "Failed to prepare file for preview", Toast.LENGTH_SHORT).show()
} }
context.startActivity(intent)
} }
} else { } else {
Log.e(TAG, "Decrypted preview file is empty or doesn't exist")
mainHandler.post { mainHandler.post {
Toast.makeText(context, "Failed to prepare file for preview", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Failed to decrypt file for preview", Toast.LENGTH_SHORT).show()
} }
} }
} else {
Log.e(TAG, "Failed to decrypt file for preview")
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file for preview", Toast.LENGTH_SHORT).show()
}
} }
} else { } else {
openInPreview(fileType) openInPreview(fileType)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error preparing file for preview: ${e.message}", e)
mainHandler.post { mainHandler.post {
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
} }
@@ -337,7 +333,6 @@ class FileAdapter(
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to open audio file: ${e.message}")
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.no_audio_player_found), context.getString(R.string.no_audio_player_found),
@@ -360,7 +355,7 @@ class FileAdapter(
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to open document file: ${e.message}")
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.no_suitable_app_found_to_open_this_document), context.getString(R.string.no_suitable_app_found_to_open_this_document),
@@ -435,7 +430,6 @@ class FileAdapter(
val success = try { val success = try {
file.renameTo(newFile) file.renameTo(newFile)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to rename file: ${e.message}")
false false
} }
@@ -469,6 +463,86 @@ class FileAdapter(
imageView.setImageResource(R.drawable.encrypted) imageView.setImageResource(R.drawable.encrypted)
imageView.setPadding(50, 50, 50, 50) 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 {
val extension = when (fileType) {
FileManager.FileType.IMAGE -> ".jpg"
FileManager.FileType.VIDEO -> ".mp4"
FileManager.FileType.AUDIO -> ".mp3"
else -> ".txt"
}
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = decryptedFile.absolutePath,
fileName = decryptedFile.name,
encryptedFileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = false
)
)
when (fileType) {
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", if (fileType == FileManager.FileType.IMAGE) "image" else "video")
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
putExtra("isEncrypted", false)
putExtra("file", decryptedFile.absolutePath)
}
context.startActivity(intent)
}
FileManager.FileType.AUDIO -> openAudioFile(decryptedFile)
else -> openDocumentFile(decryptedFile)
}
file.delete()
} else {
decryptedFile.delete()
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
}
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
mainHandler.post {
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
}
}
}
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
@@ -591,7 +665,6 @@ class FileAdapter(
fileExecutor.shutdown() fileExecutor.shutdown()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error shutting down executor: ${e.message}")
} }
fileOperationCallback?.clear() fileOperationCallback?.clear()
@@ -669,7 +742,6 @@ class FileAdapter(
failCount++ failCount++
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error encrypting file: ${e.message}")
failCount++ failCount++
} }
} }
@@ -748,7 +820,6 @@ class FileAdapter(
failCount++ failCount++
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error decrypting file: ${e.message}")
failCount++ failCount++
} }
} }

View File

@@ -10,21 +10,19 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import devs.org.calculator.databinding.ViewpagerItemsBinding
import devs.org.calculator.utils.FileManager
import java.io.File
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.database.AppDatabase import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.utils.SecurityUtils import devs.org.calculator.databinding.ViewpagerItemsBinding
import android.util.Log import devs.org.calculator.utils.FileManager
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
class ImagePreviewAdapter( class ImagePreviewAdapter(
private val context: Context, private val context: Context,
@@ -38,10 +36,6 @@ class ImagePreviewAdapter(
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao()) HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
} }
companion object {
private const val TAG = "ImagePreviewAdapter"
}
var images: List<File> var images: List<File>
get() = differ.currentList get() = differ.currentList
set(value) = differ.submitList(value) set(value) = differ.submitList(value)
@@ -53,10 +47,10 @@ class ImagePreviewAdapter(
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = images[position] val imageUrl = images[position]
val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
stopAndResetCurrentAudio() stopAndResetCurrentAudio()
holder.bind(imageUrl, position) holder.bind(imageUrl, position,fileType)
} }
override fun getItemCount(): Int = images.size override fun getItemCount(): Int = images.size
@@ -77,36 +71,42 @@ class ImagePreviewAdapter(
private var currentPosition = 0 private var currentPosition = 0
private var tempDecryptedFile: File? = null private var tempDecryptedFile: File? = null
fun bind(file: File, position: Int) { fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
currentPosition = position currentPosition = position
releaseMediaPlayer() releaseMediaPlayer()
resetAudioUI() resetAudioUI()
cleanupTempFile() cleanupTempFile()
lifecycleOwner.lifecycleScope.launch { try {
try { lifecycleOwner.lifecycleScope.launch {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
val isEncrypted = hiddenFile?.isEncrypted == true if (hiddenFile != null){
val fileType = hiddenFile!!.fileType val isEncrypted = hiddenFile.isEncrypted
if (isEncrypted) { val fileType = hiddenFile.fileType
if (isEncrypted) {
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile) val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) { if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
displayFile(tempDecryptedFile, fileType) displayFile(tempDecryptedFile, fileType,true)
} else {
showEncryptedError()
}
} else { } else {
showEncryptedError() displayFile(file, decryptedFileType,false)
} }
} else { }else{
displayFile(file, fileType) displayFile(file, decryptedFileType,false)
} }
} catch (e: Exception) {
Log.e(TAG, "Error binding file: ${e.message}", e)
showEncryptedError()
} }
} catch (_: Exception) {
displayFile(file, decryptedFileType,false)
} }
} }
private fun displayFile(file: File, fileType: FileManager.FileType) { private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
val uri = getUriForPreviewFile(context, file) val uri = getUriForPreviewFile(context, file)
when (fileType) { when (fileType) {
FileManager.FileType.VIDEO -> { FileManager.FileType.VIDEO -> {
@@ -114,7 +114,11 @@ class ImagePreviewAdapter(
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE binding.videoView.visibility = View.VISIBLE
val videoUri = uri val videoUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.videoView.setVideoURI(videoUri) binding.videoView.setVideoURI(videoUri)
binding.videoView.start() binding.videoView.start()
@@ -139,21 +143,30 @@ class ImagePreviewAdapter(
} }
} }
FileManager.FileType.IMAGE -> { FileManager.FileType.IMAGE -> {
val imageUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
Glide.with(context) Glide.with(context)
.load(uri) .load(imageUri)
.into(binding.imageView) .into(binding.imageView)
} }
FileManager.FileType.AUDIO -> { FileManager.FileType.AUDIO -> {
val audioFile: File? = if (isEncrypted) {
getFileFromUri(context, uri!!)
} else {
file
}
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name binding.audioTitle.text = file.name
setupAudioPlayer(file) setupAudioPlayer(audioFile!!)
setupPlaybackControls() setupPlaybackControls()
} }
else -> { else -> {
@@ -164,6 +177,20 @@ class ImagePreviewAdapter(
} }
} }
fun getFileFromUri(context: Context, uri: Uri): File? {
return try {
val inputStream = context.contentResolver.openInputStream(uri)
val tempFile = File.createTempFile("temp_audio", null, context.cacheDir)
tempFile.outputStream().use { output ->
inputStream?.copyTo(output)
}
tempFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun showEncryptedError() { private fun showEncryptedError() {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
@@ -176,9 +203,7 @@ class ImagePreviewAdapter(
if (file.exists()) { if (file.exists()) {
try { try {
file.delete() file.delete()
Log.d(TAG, "Cleaned up temporary decrypted file: ${file.absolutePath}") } catch (_: Exception) {
} catch (e: Exception) {
Log.e(TAG, "Error cleaning up temporary file: ${e.message}", e)
} }
} }
tempDecryptedFile = null tempDecryptedFile = null
@@ -188,7 +213,7 @@ class ImagePreviewAdapter(
private fun resetAudioUI() { private fun resetAudioUI() {
binding.playPause.setImageResource(R.drawable.play) binding.playPause.setImageResource(R.drawable.play)
binding.audioSeekBar.value = 0f binding.audioSeekBar.value = 0f
binding.audioSeekBar.valueTo = 100f // Default value binding.audioSeekBar.valueTo = 100f
seekRunnable?.let { seekHandler.removeCallbacks(it) } seekRunnable?.let { seekHandler.removeCallbacks(it) }
} }

View File

@@ -51,6 +51,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
const val ENCRYPTED_EXTENSION = ".enc" const val ENCRYPTED_EXTENSION = ".enc"
} }
fun getHiddenDirectory(): File { fun getHiddenDirectory(): File {
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR) val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
if (!dir.exists()) { if (!dir.exists()) {
@@ -58,7 +59,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
if (!created) { if (!created) {
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}") throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
} }
// Create .nomedia file to hide from media scanners
val nomediaFile = File(dir, ".nomedia") val nomediaFile = File(dir, ".nomedia")
if (!nomediaFile.exists()) { if (!nomediaFile.exists()) {
nomediaFile.createNewFile() nomediaFile.createNewFile()
@@ -399,4 +399,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
DOCUMENT(DOCS_DIR), DOCUMENT(DOCS_DIR),
ALL("all") ALL("all")
} }
} }

View File

@@ -2,7 +2,6 @@ package devs.org.calculator.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -17,27 +16,38 @@ import javax.crypto.spec.SecretKeySpec
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import devs.org.calculator.database.HiddenFileEntity import devs.org.calculator.database.HiddenFileEntity
import androidx.core.content.edit
object SecurityUtils { object SecurityUtils {
private const val ALGORITHM = "AES" private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val KEY_SIZE = 256 private const val KEY_SIZE = 256
private const val TAG = "SecurityUtils"
private fun getSecretKey(context: Context): SecretKey { private fun getSecretKey(context: Context): SecretKey {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE) val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
val encodedKey = keyStore.getString("secret_key", null) val useCustomKey = keyStore.getBoolean("use_custom_key", false)
if (useCustomKey) {
val customKey = keyStore.getString("custom_key", null)
if (customKey != null) {
try {
val messageDigest = java.security.MessageDigest.getInstance("SHA-256")
val keyBytes = messageDigest.digest(customKey.toByteArray())
return SecretKeySpec(keyBytes, ALGORITHM)
} catch (_: Exception) {
}
}
}
val encodedKey = keyStore.getString("secret_key", null)
return if (encodedKey != null) { return if (encodedKey != null) {
try { try {
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT) val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
SecretKeySpec(decodedKey, ALGORITHM) SecretKeySpec(decodedKey, ALGORITHM)
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error decoding stored key, generating new key", e)
generateAndStoreNewKey(keyStore) generateAndStoreNewKey(keyStore)
} }
} else { } else {
Log.d(TAG, "No stored key found, generating new key")
generateAndStoreNewKey(keyStore) generateAndStoreNewKey(keyStore)
} }
} }
@@ -47,14 +57,13 @@ object SecurityUtils {
keyGenerator.init(KEY_SIZE, SecureRandom()) keyGenerator.init(KEY_SIZE, SecureRandom())
val key = keyGenerator.generateKey() val key = keyGenerator.generateKey()
val encodedKey = android.util.Base64.encodeToString(key.encoded, android.util.Base64.DEFAULT) val encodedKey = android.util.Base64.encodeToString(key.encoded, android.util.Base64.DEFAULT)
keyStore.edit().putString("secret_key", encodedKey).apply() keyStore.edit { putString("secret_key", encodedKey) }
return key return key
} }
fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean { fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try { return try {
if (!inputFile.exists()) { if (!inputFile.exists()) {
Log.e(TAG, "Input file does not exist: ${inputFile.absolutePath}")
return false return false
} }
@@ -66,7 +75,6 @@ object SecurityUtils {
FileInputStream(inputFile).use { input -> FileInputStream(inputFile).use { input ->
FileOutputStream(outputFile).use { output -> FileOutputStream(outputFile).use { output ->
// Write IV at the beginning of the file
output.write(iv) output.write(iv)
CipherOutputStream(output, cipher).use { cipherOutput -> CipherOutputStream(output, cipher).use { cipherOutput ->
input.copyTo(cipherOutput) input.copyTo(cipherOutput)
@@ -74,26 +82,22 @@ object SecurityUtils {
} }
} }
// Verify the encrypted file exists and has content
if (!outputFile.exists() || outputFile.length() == 0L) { if (!outputFile.exists() || outputFile.length() == 0L) {
Log.e(TAG, "Encrypted file is empty or does not exist: ${outputFile.absolutePath}")
return false return false
} }
// Verify we can read the IV from the encrypted file
FileInputStream(outputFile).use { input -> FileInputStream(outputFile).use { input ->
val iv = ByteArray(16) val iv = ByteArray(16)
val bytesRead = input.read(iv) val bytesRead = input.read(iv)
if (bytesRead != 16) { if (bytesRead != 16) {
Log.e(TAG, "Failed to verify IV in encrypted file: expected 16 bytes but got $bytesRead")
return false return false
} }
} }
true true
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error encrypting file: ${e.message}", e)
// Clean up the output file if it exists
if (outputFile.exists()) { if (outputFile.exists()) {
outputFile.delete() outputFile.delete()
} }
@@ -105,60 +109,30 @@ object SecurityUtils {
try { try {
val encryptedFile = File(meta.filePath) val encryptedFile = File(meta.filePath)
if (!encryptedFile.exists()) { if (!encryptedFile.exists()) {
Log.e(TAG, "Encrypted file does not exist: ${meta.filePath}")
return null return null
} }
// Create a unique temp file name using the original file name
val tempDir = File(context.cacheDir, "preview_temp") val tempDir = File(context.cacheDir, "preview_temp")
if (!tempDir.exists()) tempDir.mkdirs() if (!tempDir.exists()) tempDir.mkdirs()
// Use the original extension from metadata
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}") val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
// Clean up any existing temp files
tempDir.listFiles()?.forEach { it.delete() } tempDir.listFiles()?.forEach { it.delete() }
// Attempt to decrypt the file
val success = decryptFile(context, encryptedFile, tempFile) val success = decryptFile(context, encryptedFile, tempFile)
if (success && tempFile.exists() && tempFile.length() > 0) { if (success && tempFile.exists() && tempFile.length() > 0) {
Log.d(TAG, "Successfully created preview file: ${tempFile.absolutePath}")
return tempFile return tempFile
} else { } else {
Log.e(TAG, "Failed to create preview file or file is empty")
if (tempFile.exists()) tempFile.delete() if (tempFile.exists()) tempFile.delete()
return null return null
} }
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error creating preview file: ${e.message}", e)
return null return null
} }
} }
fun getDecryptedFileUri(context: Context, encryptedFile: File): Uri? {
return try {
// Create temp file in cache dir with same extension
val extension = getFileExtension(encryptedFile)
val tempFile = File.createTempFile("decrypted_", extension, context.cacheDir)
if (decryptFile(context, encryptedFile, tempFile)) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
tempFile
)
uri
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get decrypted file URI: ${e.message}", e)
null
}
}
fun getUriForPreviewFile(context: Context, file: File): Uri? { fun getUriForPreviewFile(context: Context, file: File): Uri? {
return try { return try {
FileProvider.getUriForFile( FileProvider.getUriForFile(
@@ -166,8 +140,7 @@ object SecurityUtils {
"${context.packageName}.provider", // Must match AndroidManifest "${context.packageName}.provider", // Must match AndroidManifest
file file
) )
} catch (e: Exception) { } catch (_: Exception) {
Log.e("PreviewUtils", "Error getting URI", e)
null null
} }
} }
@@ -177,32 +150,25 @@ object SecurityUtils {
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean { fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try { return try {
if (!inputFile.exists()) { if (!inputFile.exists()) {
Log.e(TAG, "Input file does not exist: ${inputFile.absolutePath}")
return false return false
} }
if (inputFile.length() == 0L) { if (inputFile.length() == 0L) {
Log.e(TAG, "Input file is empty: ${inputFile.absolutePath}")
return false return false
} }
val secretKey = getSecretKey(context) val secretKey = getSecretKey(context)
val cipher = Cipher.getInstance(TRANSFORMATION) val cipher = Cipher.getInstance(TRANSFORMATION)
// First verify we can read the IV
FileInputStream(inputFile).use { input -> FileInputStream(inputFile).use { input ->
val iv = ByteArray(16) val iv = ByteArray(16)
val bytesRead = input.read(iv) val bytesRead = input.read(iv)
if (bytesRead != 16) { if (bytesRead != 16) {
Log.e(TAG, "Failed to read IV: expected 16 bytes but got $bytesRead")
return false return false
} }
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
// Create a new input stream for the actual decryption
FileInputStream(inputFile).use { decInput -> FileInputStream(inputFile).use { decInput ->
// Skip the IV
decInput.skip(16) decInput.skip(16)
FileOutputStream(outputFile).use { output -> FileOutputStream(outputFile).use { output ->
@@ -213,16 +179,12 @@ object SecurityUtils {
} }
} }
// Verify the decrypted file exists and has content
if (!outputFile.exists() || outputFile.length() == 0L) { if (!outputFile.exists() || outputFile.length() == 0L) {
Log.e(TAG, "Decrypted file is empty or does not exist: ${outputFile.absolutePath}")
return false return false
} }
true true
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error decrypting file: ${e.message}", e)
// Clean up the output file if it exists
if (outputFile.exists()) { if (outputFile.exists()) {
outputFile.delete() outputFile.delete()
} }
@@ -250,4 +212,30 @@ object SecurityUtils {
} }
return File(file.parent, newName) return File(file.parent, newName)
} }
fun setCustomKey(context: Context, key: String): Boolean {
return try {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
keyStore.edit {
putString("custom_key", key)
putBoolean("use_custom_key", true)
}
true
} catch (_: Exception) {
false
}
}
fun clearCustomKey(context: Context) {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
keyStore.edit {
remove("custom_key")
putBoolean("use_custom_key", false)
}
}
fun isUsingCustomKey(context: Context): Boolean {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
return keyStore.getBoolean("use_custom_key", false)
}
} }

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="#CC000000" android:centerColor="#00ffffff" android:endColor="#00ffffff" android:angle="90"/>
</shape>

View File

@@ -265,6 +265,14 @@
android:text="@string/encrypt_file_when_hiding" android:text="@string/encrypt_file_when_hiding"
android:textAppearance="?attr/textAppearanceBodyLarge" /> android:textAppearance="?attr/textAppearanceBodyLarge" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/customKeyStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/set_custom_encryption_key"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Enter encryption key"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keyInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Confirm encryption key"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/confirmKeyInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Note: Make sure to remember your key. If you lose it, you won't be able to decrypt your files."
android:textSize="12sp"
android:textColor="?android:textColorSecondary" />
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<CheckBox
android:id="@+id/checkboxImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Image"
android:padding="8dp" />
<CheckBox
android:id="@+id/checkboxVideo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Video"
android:padding="8dp" />
<CheckBox
android:id="@+id/checkboxAudio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Audio"
android:padding="8dp" />
</LinearLayout>

View File

@@ -21,6 +21,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:cardCornerRadius="10dp" app:cardCornerRadius="10dp"
android:layout_margin="5dp" android:layout_margin="5dp"
android:backgroundTint="#404040"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -36,6 +37,11 @@
android:layout_gravity="center" android:layout_gravity="center"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/add_image" /> android:src="@drawable/add_image" />
<View
android:id="@+id/shade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bottom_shade"/>
<LinearLayout <LinearLayout
android:id="@+id/selectedLayer" android:id="@+id/selectedLayer"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -50,6 +56,20 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/fileNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingHorizontal="13dp"
android:paddingVertical="6dp"
android:textColor="#fff"
android:textSize="10sp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@+id/cardView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<ImageView <ImageView
android:id="@+id/videoPlay" android:id="@+id/videoPlay"
@@ -79,14 +99,4 @@
</FrameLayout> </FrameLayout>
<TextView
android:id="@+id/fileNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="5dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout> </LinearLayout>

View File

@@ -157,6 +157,22 @@
<string name="decrypt_files">Decrypt Files</string> <string name="decrypt_files">Decrypt Files</string>
<string name="encrypt">Encrypt</string> <string name="encrypt">Encrypt</string>
<string name="decrypt">Decrypt</string> <string name="decrypt">Decrypt</string>
<string name="encryption_disclaimer">Warning: This will encrypt the selected files. The original files will be deleted after successful encryption. Make sure to decrypt all the files if you are uninstalling the application otherwise all your encrypted files will be lost. Do you want to continue?</string> <string name="encryption_disclaimer">Warning: This will encrypt the selected files. Please use a custom encryption key from Settings, you can only decrypt the encrypted files with the same key so if you use default key and uninstall the app without decrypting the file you cannot decrypt the file later!. Do you want to continue?</string>
<string name="decryption_disclaimer">Warning: This will decrypt the selected files. The encrypted files will be deleted after successful decryption. Make sure to do this. Do you want to continue?</string> <string name="decryption_disclaimer">Warning: This will decrypt the selected files. The encrypted files will be deleted after successful decryption. Make sure to do this. Do you want to continue?</string>
<string name="custom_encryption_key_cleared">Custom encryption key cleared</string>
<string name="failed_to_set_custom_key">Failed to set custom key</string>
<string name="custom_key_set_successfully">Custom key set successfully</string>
<string name="keys_do_not_match">Keys do not match</string>
<string name="key_cannot_be_empty">Key cannot be empty</string>
<string name="set_custom_encryption_key">Set Custom Encryption Key</string>
<string name="set">Set</string>
<string name="delete_key">Delete Key</string>
<string name="enter_folder_name_to_create">Enter Folder Name To Create</string>
<string name="please_select_exactly_one_folder_to_edit">Please select exactly one folder to edit</string>
<string name="invalid_folder_name">Invalid folder name</string>
<string name="folder_with_this_name_already_exists">Folder with this name already exists</string>
<string name="failed_to_rename_folder">Failed to rename folder</string>
<string name="select_file_type">Select File Type!</string>
<string name="please_select_the_type_of_file_to_decrypt">Please select the type of file to decrypt</string>
<string name="file_no_longer_exists">File no longer exists</string>
</resources> </resources>