Added - file encryption, custom key creation.
This commit is contained in:
@@ -13,7 +13,6 @@ import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.adapters.FolderAdapter
|
||||
@@ -138,7 +137,7 @@ class HiddenActivity : AppCompatActivity() {
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
|
||||
showEmptyState()
|
||||
}
|
||||
@@ -158,25 +157,25 @@ class HiddenActivity : AppCompatActivity() {
|
||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Enter Folder Name To Create")
|
||||
.setTitle(getString(R.string.enter_folder_name_to_create))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("Create") { dialog, _ ->
|
||||
.setPositiveButton(getString(R.string.create)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty()) {
|
||||
try {
|
||||
folderManager.createFolder(hiddenDir, newName)
|
||||
refreshCurrentView()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
this@HiddenActivity,
|
||||
"Failed to create folder",
|
||||
getString(R.string.failed_to_create_folder),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.show()
|
||||
@@ -214,7 +213,7 @@ class HiddenActivity : AppCompatActivity() {
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
@@ -434,7 +433,8 @@ class HiddenActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -449,20 +449,21 @@ class HiddenActivity : AppCompatActivity() {
|
||||
inputEditText.selectAll()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Rename Folder")
|
||||
.setTitle(getString(R.string.rename_folder))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("Rename") { dialog, _ ->
|
||||
.setPositiveButton(getString(R.string.rename)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty() && newName != folder.name) {
|
||||
if (isValidFolderName(newName)) {
|
||||
renameFolder(folder, newName)
|
||||
} 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()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.show()
|
||||
@@ -481,7 +482,8 @@ class HiddenActivity : AppCompatActivity() {
|
||||
if (parentDir != null) {
|
||||
val newFolder = File(parentDir, newName)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -492,7 +494,7 @@ class HiddenActivity : AppCompatActivity() {
|
||||
|
||||
refreshCurrentView()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import devs.org.calculator.utils.PrefsUtil
|
||||
import net.objecthunter.exp4j.ExpressionBuilder
|
||||
import java.util.regex.Pattern
|
||||
import androidx.core.content.edit
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
@@ -37,7 +36,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
||||
private val dialogUtil = DialogUtil(this)
|
||||
private val fileManager = FileManager(this, this)
|
||||
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
|
||||
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -276,7 +274,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
||||
private fun evaluateExpression(expression: String): Double {
|
||||
return try {
|
||||
ExpressionBuilder(expression).build().evaluate()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
expression.toDouble()
|
||||
}
|
||||
}
|
||||
@@ -359,7 +357,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
||||
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
|
||||
binding.total.text = getString(R.string.now_enter_button)
|
||||
}else binding.total.text = formattedResult
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
binding.total.text = ""
|
||||
}
|
||||
}
|
||||
@@ -372,7 +370,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
||||
val lastChar = currentExpression.last()
|
||||
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
|
||||
|
||||
// Update flags based on what was removed
|
||||
if (lastChar == '%') {
|
||||
lastWasPercent = false
|
||||
} else if (isOperator(lastChar.toString())) {
|
||||
|
||||
@@ -6,18 +6,16 @@ import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import devs.org.calculator.R
|
||||
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.utils.DialogUtil
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import android.util.Log
|
||||
|
||||
class PreviewActivity : AppCompatActivity() {
|
||||
|
||||
@@ -35,10 +33,6 @@ class PreviewActivity : AppCompatActivity() {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PreviewActivity"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPreviewBinding.inflate(layoutInflater)
|
||||
@@ -73,7 +67,6 @@ class PreviewActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setupFlagSecure() {
|
||||
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
@@ -188,18 +181,15 @@ class PreviewActivity : AppCompatActivity() {
|
||||
override fun onPositiveButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// First delete from database
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
|
||||
hiddenFile?.let {
|
||||
hiddenFileRepository.deleteHiddenFile(it)
|
||||
Log.d(TAG, "Deleted file metadata from database: ${it.filePath}")
|
||||
}
|
||||
|
||||
// Then delete the actual file
|
||||
fileManager.deletePhotoFromExternalStorage(fileUri)
|
||||
removeFileFromList(currentPosition)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error deleting file: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,19 +218,17 @@ class PreviewActivity : AppCompatActivity() {
|
||||
override fun onPositiveButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// First copy the file to normal directory
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
|
||||
hiddenFile?.let {
|
||||
hiddenFileRepository.deleteHiddenFile(it)
|
||||
Log.d(TAG, "Deleted file metadata from database: ${it.filePath}")
|
||||
}
|
||||
|
||||
removeFileFromList(currentPosition)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error unhiding file: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
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.databinding.ActivitySettingsBinding
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
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 GITHUB_URL = ""
|
||||
|
||||
@@ -25,6 +28,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
prefs = PrefsUtil(this)
|
||||
DEV_GITHUB_URL = getString(R.string.github_profile)
|
||||
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
|
||||
setupUI()
|
||||
@@ -52,6 +56,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
else -> binding.systemThemeRadio.isChecked = true
|
||||
}
|
||||
|
||||
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
|
||||
binding.customKeyStatus.isChecked = isUsingCustomKey
|
||||
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
|
||||
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
||||
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
|
||||
@@ -113,6 +119,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||
binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setBoolean("showFileName", isChecked)
|
||||
}
|
||||
|
||||
binding.customKeyStatus.setOnClickListener {
|
||||
showCustomKeyDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateThemeModeVisibility() {
|
||||
@@ -154,4 +164,57 @@ class SettingsActivity : AppCompatActivity() {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ import devs.org.calculator.utils.PrefsUtil
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.app.AlertDialog
|
||||
|
||||
class ViewFolderActivity : AppCompatActivity() {
|
||||
|
||||
@@ -166,7 +169,6 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
object : FileProcessCallback {
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
mainHandler.post {
|
||||
// Add files to Room database
|
||||
copiedFiles.forEach { file ->
|
||||
val fileType = fileManager.getFileType(file)
|
||||
var finalFile = file
|
||||
@@ -331,14 +333,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
if (selectedFiles.isEmpty()) return
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Check if any files are encrypted
|
||||
var hasEncryptedFiles = false
|
||||
var hasDecryptedFiles = false
|
||||
var hasEncFilesWithoutMetadata = false
|
||||
|
||||
for (file in selectedFiles) {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
|
||||
if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
hasEncryptedFiles = true
|
||||
} else {
|
||||
hasEncFilesWithoutMetadata = true
|
||||
}
|
||||
} else {
|
||||
hasDecryptedFiles = true
|
||||
}
|
||||
@@ -351,14 +358,14 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
getString(R.string.move_to_another_folder)
|
||||
)
|
||||
|
||||
// Add encryption/decryption options based on file status
|
||||
if (hasDecryptedFiles) {
|
||||
options.add(getString(R.string.encrypt_file))
|
||||
}
|
||||
if (hasEncryptedFiles) {
|
||||
if (hasEncryptedFiles || hasEncFilesWithoutMetadata) {
|
||||
options.add(getString(R.string.decrypt_file))
|
||||
}
|
||||
|
||||
|
||||
MaterialAlertDialogBuilder(this@ViewFolderActivity)
|
||||
.setTitle(getString(R.string.file_options))
|
||||
.setItems(options.toTypedArray()) { _, which ->
|
||||
@@ -371,7 +378,20 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
val option = options[which]
|
||||
when (option) {
|
||||
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>) {
|
||||
showFolderSelectionDialog { destinationFolder ->
|
||||
moveFilesToFolder(selectedFiles, destinationFolder)
|
||||
@@ -437,7 +629,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
if (files.isNotEmpty()) {
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.noItems.visibility = View.GONE
|
||||
// Submit new list directly
|
||||
|
||||
fileAdapter?.submitList(files.toMutableList())
|
||||
fileAdapter?.let { adapter ->
|
||||
if (adapter.isInSelectionMode()) {
|
||||
@@ -451,7 +643,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewFolderActivity", "Error refreshing folder: ${e.message}")
|
||||
|
||||
mainHandler.post {
|
||||
showEmptyState()
|
||||
}
|
||||
@@ -610,7 +802,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewFolderActivity", "Error unhiding file: ${e.message}")
|
||||
|
||||
allUnhidden = false
|
||||
}
|
||||
}
|
||||
@@ -642,7 +834,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
allDeleted = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewFolderActivity", "Error deleting file: ${e.message}")
|
||||
|
||||
allDeleted = false
|
||||
}
|
||||
}
|
||||
@@ -689,7 +881,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewFolderActivity", "Error copying file: ${e.message}")
|
||||
|
||||
allCopied = false
|
||||
}
|
||||
}
|
||||
@@ -723,7 +915,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
||||
|
||||
file.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.e("ViewFolderActivity", "Error moving file: ${e.message}")
|
||||
|
||||
allMoved = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,6 @@ class FileAdapter(
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FileAdapter"
|
||||
}
|
||||
|
||||
interface FileOperationCallback {
|
||||
fun onFileDeleted(file: File)
|
||||
fun onFileRenamed(oldFile: File, newFile: File)
|
||||
@@ -76,6 +72,7 @@ class FileAdapter(
|
||||
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)
|
||||
|
||||
@@ -91,6 +88,7 @@ class FileAdapter(
|
||||
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
|
||||
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
@@ -99,11 +97,11 @@ class FileAdapter(
|
||||
}
|
||||
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error binding file: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun bind(file: File, payloads: List<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
bind(file)
|
||||
@@ -163,7 +161,6 @@ class FileAdapter(
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error loading encrypted image preview: ${e.message}")
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
@@ -200,7 +197,6 @@ class FileAdapter(
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error loading encrypted video preview: ${e.message}")
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
@@ -260,7 +256,8 @@ class FileAdapter(
|
||||
|
||||
private fun openFile(file: File, fileType: FileManager.FileType) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -270,12 +267,13 @@ class FileAdapter(
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
if (hiddenFile?.isEncrypted == true || file.extension == FileManager.ENCRYPTED_EXTENSION) {
|
||||
if (file.extension == FileManager.ENCRYPTED_EXTENSION && hiddenFile == null) {
|
||||
showDecryptionTypeDialog(file)
|
||||
} else {
|
||||
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
||||
Log.d(TAG, "Attempting to decrypt file for preview: ${file.absolutePath}")
|
||||
|
||||
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
||||
Log.d(TAG, "Successfully decrypted file for preview: ${tempFile.absolutePath}")
|
||||
if (tempFile.exists() && tempFile.length() > 0) {
|
||||
mainHandler.post {
|
||||
val fileTypeString = when (fileType) {
|
||||
@@ -294,22 +292,20 @@ class FileAdapter(
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Decrypted preview file is empty or doesn't exist")
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Failed to prepare 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 {
|
||||
openInPreview(fileType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error preparing file for preview: ${e.message}", e)
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -337,7 +333,6 @@ class FileAdapter(
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to open audio file: ${e.message}")
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_audio_player_found),
|
||||
@@ -360,7 +355,7 @@ class FileAdapter(
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to open document file: ${e.message}")
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_suitable_app_found_to_open_this_document),
|
||||
@@ -435,7 +430,6 @@ class FileAdapter(
|
||||
val success = try {
|
||||
file.renameTo(newFile)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to rename file: ${e.message}")
|
||||
false
|
||||
}
|
||||
|
||||
@@ -469,6 +463,86 @@ class FileAdapter(
|
||||
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 {
|
||||
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 {
|
||||
@@ -591,7 +665,6 @@ class FileAdapter(
|
||||
fileExecutor.shutdown()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error shutting down executor: ${e.message}")
|
||||
}
|
||||
|
||||
fileOperationCallback?.clear()
|
||||
@@ -669,7 +742,6 @@ class FileAdapter(
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error encrypting file: ${e.message}")
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
@@ -748,7 +820,6 @@ class FileAdapter(
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error decrypting file: ${e.message}")
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,19 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.MediaController
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import devs.org.calculator.databinding.ViewpagerItemsBinding
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class ImagePreviewAdapter(
|
||||
private val context: Context,
|
||||
@@ -38,10 +36,6 @@ class ImagePreviewAdapter(
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ImagePreviewAdapter"
|
||||
}
|
||||
|
||||
var images: List<File>
|
||||
get() = differ.currentList
|
||||
set(value) = differ.submitList(value)
|
||||
@@ -53,10 +47,10 @@ class ImagePreviewAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
val imageUrl = images[position]
|
||||
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
|
||||
stopAndResetCurrentAudio()
|
||||
|
||||
holder.bind(imageUrl, position)
|
||||
holder.bind(imageUrl, position,fileType)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = images.size
|
||||
@@ -77,36 +71,42 @@ class ImagePreviewAdapter(
|
||||
private var currentPosition = 0
|
||||
private var tempDecryptedFile: File? = null
|
||||
|
||||
fun bind(file: File, position: Int) {
|
||||
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
|
||||
currentPosition = position
|
||||
|
||||
releaseMediaPlayer()
|
||||
resetAudioUI()
|
||||
cleanupTempFile()
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
val isEncrypted = hiddenFile?.isEncrypted == true
|
||||
val fileType = hiddenFile!!.fileType
|
||||
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)
|
||||
displayFile(tempDecryptedFile, fileType,true)
|
||||
} else {
|
||||
showEncryptedError()
|
||||
}
|
||||
} else {
|
||||
displayFile(file, fileType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error binding file: ${e.message}", e)
|
||||
showEncryptedError()
|
||||
}
|
||||
displayFile(file, decryptedFileType,false)
|
||||
}
|
||||
}else{
|
||||
displayFile(file, decryptedFileType,false)
|
||||
}
|
||||
|
||||
private fun displayFile(file: File, fileType: FileManager.FileType) {
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
|
||||
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 -> {
|
||||
@@ -114,7 +114,11 @@ class ImagePreviewAdapter(
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.VISIBLE
|
||||
|
||||
val videoUri = uri
|
||||
val videoUri = if (isEncrypted){
|
||||
uri
|
||||
}else{
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
binding.videoView.setVideoURI(videoUri)
|
||||
binding.videoView.start()
|
||||
|
||||
@@ -139,21 +143,30 @@ class ImagePreviewAdapter(
|
||||
}
|
||||
}
|
||||
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(uri)
|
||||
.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(file)
|
||||
setupAudioPlayer(audioFile!!)
|
||||
setupPlaybackControls()
|
||||
}
|
||||
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() {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
@@ -176,9 +203,7 @@ class ImagePreviewAdapter(
|
||||
if (file.exists()) {
|
||||
try {
|
||||
file.delete()
|
||||
Log.d(TAG, "Cleaned up temporary decrypted file: ${file.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error cleaning up temporary file: ${e.message}", e)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
tempDecryptedFile = null
|
||||
@@ -188,7 +213,7 @@ class ImagePreviewAdapter(
|
||||
private fun resetAudioUI() {
|
||||
binding.playPause.setImageResource(R.drawable.play)
|
||||
binding.audioSeekBar.value = 0f
|
||||
binding.audioSeekBar.valueTo = 100f // Default value
|
||||
binding.audioSeekBar.valueTo = 100f
|
||||
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
const val ENCRYPTED_EXTENSION = ".enc"
|
||||
}
|
||||
|
||||
|
||||
fun getHiddenDirectory(): File {
|
||||
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||
if (!dir.exists()) {
|
||||
@@ -58,7 +59,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
if (!created) {
|
||||
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
|
||||
}
|
||||
// Create .nomedia file to hide from media scanners
|
||||
val nomediaFile = File(dir, ".nomedia")
|
||||
if (!nomediaFile.exists()) {
|
||||
nomediaFile.createNewFile()
|
||||
@@ -399,4 +399,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
DOCUMENT(DOCS_DIR),
|
||||
ALL("all")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package devs.org.calculator.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
@@ -17,27 +16,38 @@ import javax.crypto.spec.SecretKeySpec
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.FileProvider
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import androidx.core.content.edit
|
||||
|
||||
object SecurityUtils {
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
private const val KEY_SIZE = 256
|
||||
private const val TAG = "SecurityUtils"
|
||||
|
||||
private fun getSecretKey(context: Context): SecretKey {
|
||||
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) {
|
||||
try {
|
||||
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
|
||||
SecretKeySpec(decodedKey, ALGORITHM)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error decoding stored key, generating new key", e)
|
||||
} catch (_: Exception) {
|
||||
generateAndStoreNewKey(keyStore)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "No stored key found, generating new key")
|
||||
generateAndStoreNewKey(keyStore)
|
||||
}
|
||||
}
|
||||
@@ -47,14 +57,13 @@ object SecurityUtils {
|
||||
keyGenerator.init(KEY_SIZE, SecureRandom())
|
||||
val key = keyGenerator.generateKey()
|
||||
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
|
||||
}
|
||||
|
||||
fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||
return try {
|
||||
if (!inputFile.exists()) {
|
||||
Log.e(TAG, "Input file does not exist: ${inputFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -66,7 +75,6 @@ object SecurityUtils {
|
||||
|
||||
FileInputStream(inputFile).use { input ->
|
||||
FileOutputStream(outputFile).use { output ->
|
||||
// Write IV at the beginning of the file
|
||||
output.write(iv)
|
||||
CipherOutputStream(output, cipher).use { cipherOutput ->
|
||||
input.copyTo(cipherOutput)
|
||||
@@ -74,26 +82,22 @@ object SecurityUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the encrypted file exists and has content
|
||||
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||
Log.e(TAG, "Encrypted file is empty or does not exist: ${outputFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify we can read the IV from the encrypted file
|
||||
FileInputStream(outputFile).use { input ->
|
||||
val iv = ByteArray(16)
|
||||
val bytesRead = input.read(iv)
|
||||
if (bytesRead != 16) {
|
||||
Log.e(TAG, "Failed to verify IV in encrypted file: expected 16 bytes but got $bytesRead")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error encrypting file: ${e.message}", e)
|
||||
// Clean up the output file if it exists
|
||||
} catch (_: Exception) {
|
||||
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete()
|
||||
}
|
||||
@@ -105,60 +109,30 @@ object SecurityUtils {
|
||||
try {
|
||||
val encryptedFile = File(meta.filePath)
|
||||
if (!encryptedFile.exists()) {
|
||||
Log.e(TAG, "Encrypted file does not exist: ${meta.filePath}")
|
||||
return null
|
||||
}
|
||||
|
||||
// Create a unique temp file name using the original file name
|
||||
val tempDir = File(context.cacheDir, "preview_temp")
|
||||
if (!tempDir.exists()) tempDir.mkdirs()
|
||||
|
||||
// Use the original extension from metadata
|
||||
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
|
||||
|
||||
// Clean up any existing temp files
|
||||
tempDir.listFiles()?.forEach { it.delete() }
|
||||
|
||||
// Attempt to decrypt the file
|
||||
val success = decryptFile(context, encryptedFile, tempFile)
|
||||
|
||||
if (success && tempFile.exists() && tempFile.length() > 0) {
|
||||
Log.d(TAG, "Successfully created preview file: ${tempFile.absolutePath}")
|
||||
return tempFile
|
||||
} else {
|
||||
Log.e(TAG, "Failed to create preview file or file is empty")
|
||||
if (tempFile.exists()) tempFile.delete()
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error creating preview file: ${e.message}", e)
|
||||
} catch (_: Exception) {
|
||||
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? {
|
||||
return try {
|
||||
FileProvider.getUriForFile(
|
||||
@@ -166,8 +140,7 @@ object SecurityUtils {
|
||||
"${context.packageName}.provider", // Must match AndroidManifest
|
||||
file
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("PreviewUtils", "Error getting URI", e)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -177,32 +150,25 @@ object SecurityUtils {
|
||||
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||
return try {
|
||||
if (!inputFile.exists()) {
|
||||
Log.e(TAG, "Input file does not exist: ${inputFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
if (inputFile.length() == 0L) {
|
||||
Log.e(TAG, "Input file is empty: ${inputFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
val secretKey = getSecretKey(context)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
|
||||
// First verify we can read the IV
|
||||
FileInputStream(inputFile).use { input ->
|
||||
val iv = ByteArray(16)
|
||||
val bytesRead = input.read(iv)
|
||||
if (bytesRead != 16) {
|
||||
Log.e(TAG, "Failed to read IV: expected 16 bytes but got $bytesRead")
|
||||
return false
|
||||
}
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||
|
||||
// Create a new input stream for the actual decryption
|
||||
FileInputStream(inputFile).use { decInput ->
|
||||
// Skip the IV
|
||||
decInput.skip(16)
|
||||
|
||||
FileOutputStream(outputFile).use { output ->
|
||||
@@ -213,16 +179,12 @@ object SecurityUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the decrypted file exists and has content
|
||||
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||
Log.e(TAG, "Decrypted file is empty or does not exist: ${outputFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error decrypting file: ${e.message}", e)
|
||||
// Clean up the output file if it exists
|
||||
} catch (_: Exception) {
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete()
|
||||
}
|
||||
@@ -250,4 +212,30 @@ object SecurityUtils {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/drawable/bottom_shade.xml
Normal file
5
app/src/main/res/drawable/bottom_shade.xml
Normal 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>
|
||||
@@ -265,6 +265,14 @@
|
||||
android:text="@string/encrypt_file_when_hiding"
|
||||
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>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
47
app/src/main/res/layout/dialog_custom_key.xml
Normal file
47
app/src/main/res/layout/dialog_custom_key.xml
Normal 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>
|
||||
29
app/src/main/res/layout/dialog_file_type_selection.xml
Normal file
29
app/src/main/res/layout/dialog_file_type_selection.xml
Normal 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>
|
||||
@@ -21,6 +21,7 @@
|
||||
android:layout_height="0dp"
|
||||
app:cardCornerRadius="10dp"
|
||||
android:layout_margin="5dp"
|
||||
android:backgroundTint="#404040"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -36,6 +37,11 @@
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerCrop"
|
||||
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
|
||||
android:id="@+id/selectedLayer"
|
||||
android:layout_width="match_parent"
|
||||
@@ -50,6 +56,20 @@
|
||||
|
||||
</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>
|
||||
<ImageView
|
||||
android:id="@+id/videoPlay"
|
||||
@@ -79,14 +99,4 @@
|
||||
|
||||
</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>
|
||||
@@ -157,6 +157,22 @@
|
||||
<string name="decrypt_files">Decrypt Files</string>
|
||||
<string name="encrypt">Encrypt</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="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>
|
||||
Reference in New Issue
Block a user