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.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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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