Added - file encryption using room db.

This commit is contained in:
Binondi
2025-06-05 21:22:26 +05:30
parent e90e3c438f
commit eea0f56379
23 changed files with 1136 additions and 483 deletions

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="AppInsightsSettings"> <component name="AppInsightsSettings">
<option name="selectedTabId" value="Firebase Crashlytics" />
<option name="tabSettings"> <option name="tabSettings">
<map> <map>
<entry key="Firebase Crashlytics"> <entry key="Firebase Crashlytics">

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.24" /> <option name="version" value="2.0.0" />
</component> </component>
</project> </project>

View File

@@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
} }
android { android {
@@ -12,7 +13,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 5 versionCode = 5
versionName = "1.3.1" versionName = "1.4.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -80,4 +81,9 @@ dependencies {
implementation(libs.androidx.viewpager) implementation(libs.androidx.viewpager)
implementation(libs.zoomage) implementation(libs.zoomage)
implementation(libs.lottie) implementation(libs.lottie)
// Room dependencies
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
kapt(libs.androidx.room.compiler)
} }

View File

@@ -59,7 +59,7 @@
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="devs.org.calculator.fileprovider" android:authorities="devs.org.calculator.provider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data

View File

@@ -188,7 +188,6 @@ class HiddenActivity : AppCompatActivity() {
} }
private fun setupFlagSecure() { private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) { if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,

View File

@@ -15,6 +15,9 @@ import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil import devs.org.calculator.utils.PrefsUtil
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import android.util.Log
class PreviewActivity : AppCompatActivity() { class PreviewActivity : AppCompatActivity() {
@@ -27,7 +30,14 @@ class PreviewActivity : AppCompatActivity() {
private lateinit var adapter: ImagePreviewAdapter private lateinit var adapter: ImagePreviewAdapter
private lateinit var fileManager: FileManager private lateinit var fileManager: FileManager
private val dialogUtil = DialogUtil(this) private val dialogUtil = DialogUtil(this)
private val prefs:PrefsUtil by lazy { PrefsUtil(this) } private val prefs: PrefsUtil by lazy { PrefsUtil(this) }
private val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
}
companion object {
private const val TAG = "PreviewActivity"
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -141,8 +151,6 @@ class PreviewActivity : AppCompatActivity() {
} }
} }
private fun setupClickListeners() { private fun setupClickListeners() {
binding.back.setOnClickListener { binding.back.setOnClickListener {
finish() finish()
@@ -180,10 +188,18 @@ class PreviewActivity : AppCompatActivity() {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
// First delete from database
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
hiddenFile?.let {
hiddenFileRepository.deleteHiddenFile(it)
Log.d(TAG, "Deleted file metadata from database: ${it.filePath}")
}
// Then delete the actual file
fileManager.deletePhotoFromExternalStorage(fileUri) fileManager.deletePhotoFromExternalStorage(fileUri)
removeFileFromList(currentPosition) removeFileFromList(currentPosition)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(TAG, "Error deleting file: ${e.message}", e)
} }
} }
} }
@@ -212,10 +228,19 @@ class PreviewActivity : AppCompatActivity() {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
fileManager.copyFileToNormalDir(fileUri) // First copy the file to normal directory
removeFileFromList(currentPosition) 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) { } catch (e: Exception) {
e.printStackTrace() Log.e(TAG, "Error unhiding file: ${e.message}", e)
} }
} }
} }

View File

@@ -54,6 +54,7 @@ class SettingsActivity : AppCompatActivity() {
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true) binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true) binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
updateThemeModeVisibility() updateThemeModeVisibility()
} }
@@ -79,6 +80,9 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
} }
binding.encryptionSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.setBoolean("encryption", isChecked)
}
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked -> binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->

View File

@@ -7,6 +7,7 @@ import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.animation.Animation import android.view.animation.Animation
@@ -20,19 +21,21 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.adapters.FileAdapter import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderSelectionAdapter import devs.org.calculator.adapters.FolderSelectionAdapter
import devs.org.calculator.callbacks.FileProcessCallback import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.databinding.ActivityViewFolderBinding import devs.org.calculator.databinding.ActivityViewFolderBinding
import devs.org.calculator.databinding.ProccessingDialogBinding import devs.org.calculator.databinding.ProccessingDialogBinding
import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileManager.Companion.ENCRYPTED_EXTENSION
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
import devs.org.calculator.utils.FolderManager import devs.org.calculator.utils.FolderManager
import devs.org.calculator.utils.PrefsUtil import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@@ -84,8 +87,8 @@ class ViewFolderActivity : AppCompatActivity() {
fileManager = FileManager(this, this) fileManager = FileManager(this, this)
folderManager = FolderManager() folderManager = FolderManager()
dialogUtil = DialogUtil(this) dialogUtil = DialogUtil(this)
}
}
private fun setupActivityResultLaunchers() { private fun setupActivityResultLaunchers() {
pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -163,6 +166,35 @@ class ViewFolderActivity : AppCompatActivity() {
object : FileProcessCallback { object : FileProcessCallback {
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) { override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
mainHandler.post { mainHandler.post {
// Add files to Room database
copiedFiles.forEach { file ->
val fileType = fileManager.getFileType(file)
var finalFile = file
val extension = ".${file.extension}"
val isEncrypted = prefs.getBoolean("encryption",false)
if (isEncrypted) {
val encryptedFile = SecurityUtils.changeFileExtension(file, ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(this@ViewFolderActivity, file, encryptedFile)) {
finalFile = encryptedFile
file.delete()
}
}
lifecycleScope.launch {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = finalFile.absolutePath,
fileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = isEncrypted,
encryptedFileName = finalFile.name
)
)
}
}
mainHandler.postDelayed({ mainHandler.postDelayed({
dismissCustomDialog() dismissCustomDialog()
}, 1000) }, 1000)
@@ -298,24 +330,54 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileOptionsMenu(selectedFiles: List<File>) { private fun showFileOptionsMenu(selectedFiles: List<File>) {
if (selectedFiles.isEmpty()) return if (selectedFiles.isEmpty()) return
val options = arrayOf( lifecycleScope.launch {
getString(R.string.un_hide), // Check if any files are encrypted
getString(R.string.delete), var hasEncryptedFiles = false
getString(R.string.copy_to_another_folder), var hasDecryptedFiles = false
getString(R.string.move_to_another_folder)
) for (file in selectedFiles) {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
MaterialAlertDialogBuilder(this) if (hiddenFile?.isEncrypted == true) {
.setTitle(getString(R.string.file_options)) hasEncryptedFiles = true
.setItems(options) { _, which -> } else {
when (which) { hasDecryptedFiles = true
0 -> unhideSelectedFiles(selectedFiles)
1 -> deleteSelectedFiles(selectedFiles)
2 -> copyToAnotherFolder(selectedFiles)
3 -> moveToAnotherFolder(selectedFiles)
} }
} }
.show()
val options = mutableListOf(
getString(R.string.un_hide),
getString(R.string.delete),
getString(R.string.copy_to_another_folder),
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) {
options.add(getString(R.string.decrypt_file))
}
MaterialAlertDialogBuilder(this@ViewFolderActivity)
.setTitle(getString(R.string.file_options))
.setItems(options.toTypedArray()) { _, which ->
when (which) {
0 -> unhideSelectedFiles(selectedFiles)
1 -> deleteSelectedFiles(selectedFiles)
2 -> copyToAnotherFolder(selectedFiles)
3 -> moveToAnotherFolder(selectedFiles)
else -> {
val option = options[which]
when (option) {
getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles()
getString(R.string.decrypt_file) -> fileAdapter?.decryptSelectedFiles()
}
}
}
}
.show()
}
} }
private fun moveToAnotherFolder(selectedFiles: List<File>) { private fun moveToAnotherFolder(selectedFiles: List<File>) {
@@ -334,6 +396,7 @@ class ViewFolderActivity : AppCompatActivity() {
object : DialogUtil.DialogCallback { object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
performFileUnhiding(selectedFiles) performFileUnhiding(selectedFiles)
} }
override fun onNegativeButtonClicked() {} override fun onNegativeButtonClicked() {}
@@ -367,20 +430,25 @@ class ViewFolderActivity : AppCompatActivity() {
private fun refreshCurrentFolder() { private fun refreshCurrentFolder() {
currentFolder?.let { folder -> currentFolder?.let { folder ->
val files = folderManager.getFilesInFolder(folder) lifecycleScope.launch {
if (files.isNotEmpty()) { val files = folderManager.getFilesInFolder(folder)
binding.recyclerView.visibility = View.VISIBLE mainHandler.post {
binding.noItems.visibility = View.GONE if (files.isNotEmpty()) {
fileAdapter?.submitList(files.toMutableList()) binding.recyclerView.visibility = View.VISIBLE
fileAdapter?.let { adapter -> binding.noItems.visibility = View.GONE
if (adapter.isInSelectionMode()) { // Submit new list directly
showFileSelectionIcons() fileAdapter?.submitList(files.toMutableList())
fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) {
showFileSelectionIcons()
} else {
showFileViewIcons()
}
}
} else { } else {
showFileViewIcons() showEmptyState()
} }
} }
} else {
showEmptyState()
} }
} }
} }
@@ -478,58 +546,34 @@ class ViewFolderActivity : AppCompatActivity() {
isFabOpen = false isFabOpen = false
} }
private fun performFileUnhiding(selectedFiles: List<File>) { private fun performFileDeletion(selectedFiles: List<File>) {
lifecycleScope.launch { lifecycleScope.launch {
var allUnhidden = true var allDeleted = true
selectedFiles.forEach { file -> selectedFiles.forEach { file ->
try { try {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file) val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
if (fileUri != null) { fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
val result = fileManager.copyFileToNormalDir(fileUri) }
if (result == null) { if (!file.delete()) {
allUnhidden = false allDeleted = false
}
} else {
allUnhidden = false
} }
} catch (e: Exception) { } catch (e: Exception) {
allUnhidden = false Log.e("ViewFolderActivity", "Error deleting file: ${e.message}")
allDeleted = false
} }
} }
mainHandler.post { val message = if (allDeleted) {
val message = if (allUnhidden) { getString(R.string.files_deleted_successfully)
getString(R.string.files_unhidden_successfully) } else {
} else { getString(R.string.some_items_could_not_be_deleted)
getString(R.string.some_files_could_not_be_unhidden)
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
}
}
private fun performFileDeletion(selectedFiles: List<File>) { Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
var allDeleted = true fileAdapter?.exitSelectionMode()
selectedFiles.forEach { file -> refreshCurrentFolder()
if (!file.delete()) {
allDeleted = false
}
} }
val message = if (allDeleted) {
getString(R.string.files_deleted_successfully)
} else {
getString(R.string.some_items_could_not_be_deleted)
}
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
private fun copyToAnotherFolder(selectedFiles: List<File>) { private fun copyToAnotherFolder(selectedFiles: List<File>) {
@@ -539,38 +583,67 @@ class ViewFolderActivity : AppCompatActivity() {
} }
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) { private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
var allCopied = true lifecycleScope.launch {
selectedFiles.forEach { file -> var allCopied = true
try { selectedFiles.forEach { file ->
val newFile = File(destinationFolder, file.name) try {
file.copyTo(newFile, overwrite = true) val newFile = File(destinationFolder, file.name)
} catch (e: Exception) { file.copyTo(newFile, overwrite = true)
allCopied = false val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = newFile.absolutePath,
fileName = it.fileName,
fileType = it.fileType,
originalExtension = it.originalExtension,
isEncrypted = it.isEncrypted,
encryptedFileName = it.encryptedFileName
)
)
}
} catch (e: Exception) {
Log.e("ViewFolderActivity", "Error copying file: ${e.message}")
allCopied = false
}
} }
}
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied) val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode() fileAdapter?.exitSelectionMode()
refreshCurrentFolder() refreshCurrentFolder()
}
} }
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) { private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
var allMoved = true lifecycleScope.launch {
selectedFiles.forEach { file -> var allMoved = true
try { selectedFiles.forEach { file ->
val newFile = File(destinationFolder, file.name) try {
file.copyTo(newFile, overwrite = true) val newFile = File(destinationFolder, file.name)
file.delete() file.copyTo(newFile, overwrite = true)
} catch (e: Exception) { val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
allMoved = false hiddenFile?.let {
} fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
} filePath = file.absolutePath,
newFilePath = newFile.absolutePath,
encryptedFileName = it.encryptedFileName,
isEncrypted = it.isEncrypted
)
}
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved) file.delete()
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } catch (e: Exception) {
fileAdapter?.exitSelectionMode() Log.e("ViewFolderActivity", "Error moving file: ${e.message}")
refreshCurrentFolder() allMoved = false
}
}
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
}
} }
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) { private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
@@ -596,4 +669,80 @@ class ViewFolderActivity : AppCompatActivity() {
bottomSheetDialog.show() bottomSheetDialog.show()
} }
private fun performFileUnhiding(selectedFiles: List<File>) {
lifecycleScope.launch {
var allUnhidden = true
selectedFiles.forEach { file ->
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) {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, decryptedFile)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
decryptedFile.delete()
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
allUnhidden = false
}
} else {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
} else {
allUnhidden = false
}
} else {
allUnhidden = false
}
}
} catch (e: Exception) {
Log.e("ViewFolderActivity", "Error unhiding file: ${e.message}")
allUnhidden = false
}
}
mainHandler.post {
val message = if (allUnhidden) {
getString(R.string.files_unhidden_successfully)
} else {
getString(R.string.some_files_could_not_be_unhidden)
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
}
}
}
} }

View File

@@ -15,6 +15,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -22,7 +23,14 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.activities.PreviewActivity import devs.org.calculator.activities.PreviewActivity
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.SecurityUtils
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -43,6 +51,10 @@ class FileAdapter(
private val fileExecutor = Executors.newSingleThreadExecutor() private val fileExecutor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
companion object { companion object {
private const val TAG = "FileAdapter" private const val TAG = "FileAdapter"
} }
@@ -65,17 +77,30 @@ class FileAdapter(
val playIcon: ImageView = view.findViewById(R.id.videoPlay) val playIcon: ImageView = view.findViewById(R.id.videoPlay)
val selectedLayer: View = view.findViewById(R.id.selectedLayer) val selectedLayer: View = view.findViewById(R.id.selectedLayer)
val selected: ImageView = view.findViewById(R.id.selected) val selected: ImageView = view.findViewById(R.id.selected)
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
fun bind(file: File) { fun bind(file: File) {
val fileType = FileManager(context, lifecycleOwner).getFileType(file) lifecycleOwner.lifecycleScope.launch {
setupFileDisplay(file, fileType) try {
setupClickListeners(file, fileType) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
else {
FileManager(context, lifecycleOwner).getFileType(file)
}
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
setupClickListeners(file, fileType)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
val position = adapterPosition val position = adapterPosition
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
val isSelected = selectedItems.contains(position) val isSelected = selectedItems.contains(position)
updateSelectionUI(isSelected) updateSelectionUI(isSelected)
}
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
} catch (e: Exception) {
Log.e(TAG, "Error binding file: ${e.message}")
}
} }
} }
@@ -111,37 +136,100 @@ class FileAdapter(
selected.visibility = if (isSelected) View.VISIBLE else View.GONE selected.visibility = if (isSelected) View.VISIBLE else View.GONE
} }
private fun setupFileDisplay(file: File, fileType: FileManager.FileType) { private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
fileNameTextView.text = file.name fileNameTextView.text = metadata?.fileName ?: file.name
when (fileType) { when (fileType) {
FileManager.FileType.IMAGE -> { FileManager.FileType.IMAGE -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
Glide.with(context) if (isEncrypted) {
.load(file) try {
.centerCrop() val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
.error(R.drawable.ic_document) val uri = getUriForPreviewFile(context, decryptedFile)
.into(imageView) if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.encrypted)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
Log.e(TAG, "Error loading encrypted image preview: ${e.message}")
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.ic_image)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
}
} }
FileManager.FileType.VIDEO -> { FileManager.FileType.VIDEO -> {
playIcon.visibility = View.VISIBLE playIcon.visibility = View.VISIBLE
Glide.with(context) if (isEncrypted) {
.load(file) try {
.centerCrop() val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
.error(R.drawable.ic_document) val uri = getUriForPreviewFile(context, decryptedFile)
.into(imageView) if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.encrypted)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
Log.e(TAG, "Error loading encrypted video preview: ${e.message}")
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.ic_video)
.into(imageView)
}
} }
FileManager.FileType.AUDIO -> { FileManager.FileType.AUDIO -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
imageView.setImageResource(R.drawable.ic_audio) if (isEncrypted) {
imageView.setPadding(50,50,50,50) imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_audio)
}
imageView.setPadding(50, 50, 50, 50)
} }
else -> { else -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
imageView.setImageResource(R.drawable.ic_document) if (isEncrypted) {
imageView.setPadding(50,50,50,50) imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_document)
}
imageView.setPadding(50, 50, 50, 50)
} }
} }
} }
@@ -178,7 +266,56 @@ class FileAdapter(
when (fileType) { when (fileType) {
FileManager.FileType.AUDIO -> openAudioFile(file) FileManager.FileType.AUDIO -> openAudioFile(file)
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> openInPreview(fileType) FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
lifecycleOwner.lifecycleScope.launch {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
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) {
FileManager.FileType.IMAGE -> context.getString(R.string.image)
FileManager.FileType.VIDEO -> context.getString(R.string.video)
else -> "unknown"
}
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
putExtra("isEncrypted", true)
putExtra("tempFile", tempFile.absolutePath)
}
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()
}
}
}
}
FileManager.FileType.DOCUMENT -> openDocumentFile(file) FileManager.FileType.DOCUMENT -> openDocumentFile(file)
else -> openDocumentFile(file) else -> openDocumentFile(file)
} }
@@ -247,101 +384,6 @@ class FileAdapter(
context.startActivity(intent) context.startActivity(intent)
} }
private fun showFileOptionsDialog(file: File) {
val options = if (isSelectionMode) {
arrayOf(
context.getString(R.string.un_hide),
context.getString(R.string.delete),
context.getString(R.string.copy_to_another_folder),
context.getString(R.string.move_to_another_folder)
)
} else {
arrayOf(
context.getString(R.string.un_hide),
context.getString(R.string.select_multiple),
context.getString(R.string.rename),
context.getString(R.string.delete),
context.getString(R.string.share)
)
}
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.file_options))
.setItems(options) { dialog, which ->
if (isSelectionMode) {
when (which) {
0 -> unHideFile(file)
1 -> deleteFile(file)
2 -> copyToAnotherFolder(file)
3 -> moveToAnotherFolder(file)
}
} else {
when (which) {
0 -> unHideFile(file)
1 -> enableSelectMultipleFiles()
2 -> renameFile(file)
3 -> deleteFile(file)
4 -> shareFile(file)
}
}
dialog.dismiss()
}
.create()
.show()
}
private fun enableSelectMultipleFiles() {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return
enterSelectionMode()
selectedItems.add(position)
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
private fun unHideFile(file: File) {
FileManager(context, lifecycleOwner).unHideFile(
file = file,
onSuccess = {
fileOperationCallback?.get()?.onFileDeleted(file)
},
onError = { errorMessage ->
Toast.makeText(context, "Failed to unhide: $errorMessage", Toast.LENGTH_SHORT).show()
}
)
}
private fun deleteFile(file: File) {
MaterialAlertDialogBuilder(context)
.setTitle("Delete File")
.setMessage("Are you sure you want to delete ${file.name}?")
.setPositiveButton("Delete") { _, _ ->
deleteFileAsync(file)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun deleteFileAsync(file: File) {
fileExecutor.execute {
val success = try {
file.delete()
} catch (e: Exception) {
Log.e(TAG, "Failed to delete file: ${e.message}")
false
}
mainHandler.post {
if (success) {
fileOperationCallback?.get()?.onFileDeleted(file)
Toast.makeText(context, "File deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show()
}
}
}
}
@SuppressLint("MissingInflatedId") @SuppressLint("MissingInflatedId")
private fun renameFile(file: File) { private fun renameFile(file: File) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null) val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
@@ -409,34 +451,6 @@ class FileAdapter(
} }
} }
private fun shareFile(file: File) {
try {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = context.contentResolver.getType(uri) ?: "*/*"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
)
} catch (e: Exception) {
Log.e(TAG, "Failed to share file: ${e.message}")
Toast.makeText(context, "Failed to share file", Toast.LENGTH_SHORT).show()
}
}
private fun copyToAnotherFolder(file: File) {
fileOperationCallback?.get()?.onRefreshNeeded()
}
private fun moveToAnotherFolder(file: File) {
fileOperationCallback?.get()?.onRefreshNeeded()
}
private fun toggleSelection(position: Int) { private fun toggleSelection(position: Int) {
if (selectedItems.contains(position)) { if (selectedItems.contains(position)) {
@@ -450,6 +464,11 @@ class FileAdapter(
onSelectionCountChanged(selectedItems.size) onSelectionCountChanged(selectedItems.size)
notifyItemChanged(position) notifyItemChanged(position)
} }
private fun showEncryptedIcon() {
imageView.setImageResource(R.drawable.encrypted)
imageView.setPadding(50, 50, 50, 50)
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
@@ -482,7 +501,7 @@ class FileAdapter(
currentList.clear() currentList.clear()
super.submitList(null) super.submitList(null)
} else { } else {
val newList = list.toMutableList() val newList = list.filter { it.name != ".nomedia" }.toMutableList()
super.submitList(newList) super.submitList(newList)
} }
} }
@@ -521,19 +540,12 @@ class FileAdapter(
if (!isSelectionMode) { if (!isSelectionMode) {
enterSelectionMode() enterSelectionMode()
} }
val previouslySelected = selectedItems.toSet() val previouslySelected = selectedItems.toSet()
selectedItems.clear() selectedItems.clear()
// Add all positions to selection
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
selectedItems.add(i) selectedItems.add(i)
} }
// Notify callback about selection change
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size) fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
// Update UI for changed items efficiently
updateSelectionItems(selectedItems.toSet(), previouslySelected) updateSelectionItems(selectedItems.toSet(), previouslySelected)
} }
@@ -564,122 +576,6 @@ class FileAdapter(
fun isInSelectionMode(): Boolean = isSelectionMode fun isInSelectionMode(): Boolean = isSelectionMode
fun deleteSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
MaterialAlertDialogBuilder(context)
.setTitle("Delete Files")
.setMessage("Are you sure you want to delete ${selectedFiles.size} file(s)?")
.setPositiveButton("Delete") { _, _ ->
deleteFilesAsync(selectedFiles)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun deleteFilesAsync(selectedFiles: List<File>) {
fileExecutor.execute {
var deletedCount = 0
var failedCount = 0
val failedFiles = mutableListOf<String>()
selectedFiles.forEach { file ->
try {
if (file.delete()) {
deletedCount++
mainHandler.post {
fileOperationCallback?.get()?.onFileDeleted(file)
}
} else {
failedCount++
failedFiles.add(file.name)
}
} catch (e: Exception) {
failedCount++
failedFiles.add(file.name)
Log.e(TAG, "Failed to delete ${file.name}: ${e.message}")
}
}
mainHandler.post {
exitSelectionMode()
when {
deletedCount > 0 && failedCount == 0 -> {
Toast.makeText(context, "Deleted $deletedCount file(s)", Toast.LENGTH_SHORT).show()
}
deletedCount > 0 && failedCount > 0 -> {
Toast.makeText(context,
"Deleted $deletedCount file(s), failed to delete $failedCount",
Toast.LENGTH_LONG).show()
}
failedCount > 0 -> {
Toast.makeText(context,
"Failed to delete $failedCount file(s)",
Toast.LENGTH_SHORT).show()
}
}
}
}
}
fun shareSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
try {
if (selectedFiles.size == 1) {
val file = selectedFiles.first()
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = context.contentResolver.getType(uri) ?: "*/*"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
)
} else {
val uris = selectedFiles.mapNotNull { file ->
try {
FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
} catch (e: Exception) {
Log.e(TAG, "Failed to get URI for file ${file.name}: ${e.message}")
null
}
}
if (uris.isNotEmpty()) {
val shareIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "*/*"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, "Share ${selectedFiles.size} files")
)
} else {
Toast.makeText(context, "No files could be shared", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to share files: ${e.message}")
Toast.makeText(context, "Failed to share files", Toast.LENGTH_SHORT).show()
}
exitSelectionMode()
}
fun onBackPressed(): Boolean { fun onBackPressed(): Boolean {
return if (isSelectionMode) { return if (isSelectionMode) {
exitSelectionMode() exitSelectionMode()
@@ -689,17 +585,6 @@ class FileAdapter(
} }
} }
fun refreshSelectionStates() {
if (isSelectionMode) {
selectedItems.forEach { position ->
if (position < itemCount) {
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
}
notifySelectionModeChange()
}
}
fun cleanup() { fun cleanup() {
try { try {
if (!fileExecutor.isShutdown) { if (!fileExecutor.isShutdown) {
@@ -720,4 +605,171 @@ class FileAdapter(
private fun onSelectionCountChanged(count: Int) { private fun onSelectionCountChanged(count: Int) {
fileOperationCallback?.get()?.onSelectionCountChanged(count) fileOperationCallback?.get()?.onSelectionCountChanged(count)
} }
fun encryptSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.encrypt_files))
.setMessage(context.getString(R.string.encryption_disclaimer))
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
performEncryption(selectedFiles)
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performEncryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) continue
val originalExtension = ".${file.extension.lowercase()}"
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
if (encryptedFile.exists()) {
if (hiddenFile == null){
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = encryptedFile.absolutePath,
isEncrypted = true,
encryptedFileName = encryptedFile.name,
fileType = fileType,
fileName = file.name,
originalExtension = originalExtension
)
)
}else{
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = hiddenFile.filePath,
newFilePath = encryptedFile.absolutePath,
encryptedFileName = encryptedFile.name,
isEncrypted = true
)
}
}
if (file.delete()) {
updatedFiles.add(encryptedFile)
successCount++
} else {
failCount++
}
} else {
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
Log.e(TAG, "Error encrypting file: ${e.message}")
failCount++
}
}
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
}
fun decryptSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.decrypt_files))
.setMessage(context.getString(R.string.decryption_disclaimer))
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
performDecryption(selectedFiles)
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performDecryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted != true) continue
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
updatedFiles.add(decryptedFile)
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} catch (e: Exception) {
Log.e(TAG, "Error decrypting file: ${e.message}")
failCount++
}
}
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
}
} }

View File

@@ -17,6 +17,14 @@ import devs.org.calculator.databinding.ViewpagerItemsBinding
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import java.io.File import java.io.File
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.utils.SecurityUtils
import android.util.Log
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch
class ImagePreviewAdapter( class ImagePreviewAdapter(
private val context: Context, private val context: Context,
@@ -26,6 +34,13 @@ class ImagePreviewAdapter(
private val differ = AsyncListDiffer(this, FileDiffCallback()) private val differ = AsyncListDiffer(this, FileDiffCallback())
private var currentPlayingPosition = -1 private var currentPlayingPosition = -1
private var currentViewHolder: ImageViewHolder? = null private var currentViewHolder: ImageViewHolder? = null
private val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
companion object {
private const val TAG = "ImagePreviewAdapter"
}
var images: List<File> var images: List<File>
get() = differ.currentList get() = differ.currentList
@@ -38,11 +53,10 @@ class ImagePreviewAdapter(
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = images[position] val imageUrl = images[position]
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
stopAndResetCurrentAudio() stopAndResetCurrentAudio()
holder.bind(imageUrl, fileType, position) holder.bind(imageUrl, position)
} }
override fun getItemCount(): Int = images.size override fun getItemCount(): Int = images.size
@@ -61,20 +75,46 @@ class ImagePreviewAdapter(
private var isMediaPlayerPrepared = false private var isMediaPlayerPrepared = false
private var isPlaying = false private var isPlaying = false
private var currentPosition = 0 private var currentPosition = 0
private var tempDecryptedFile: File? = null
fun bind(file: File, fileType: FileManager.FileType, position: Int) { fun bind(file: File, position: Int) {
currentPosition = position currentPosition = position
releaseMediaPlayer() releaseMediaPlayer()
resetAudioUI() resetAudioUI()
cleanupTempFile()
lifecycleOwner.lifecycleScope.launch {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
val isEncrypted = hiddenFile?.isEncrypted == true
val fileType = hiddenFile!!.fileType
if (isEncrypted) {
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
displayFile(tempDecryptedFile, fileType)
} else {
showEncryptedError()
}
} else {
displayFile(file, fileType)
}
} catch (e: Exception) {
Log.e(TAG, "Error binding file: ${e.message}", e)
showEncryptedError()
}
}
}
private fun displayFile(file: File, fileType: FileManager.FileType) {
val uri = getUriForPreviewFile(context, file)
when (fileType) { when (fileType) {
FileManager.FileType.VIDEO -> { FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE binding.videoView.visibility = View.VISIBLE
val videoUri = Uri.fromFile(file) val videoUri = uri
binding.videoView.setVideoURI(videoUri) binding.videoView.setVideoURI(videoUri)
binding.videoView.start() binding.videoView.start()
@@ -99,11 +139,12 @@ class ImagePreviewAdapter(
} }
} }
FileManager.FileType.IMAGE -> { FileManager.FileType.IMAGE -> {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
Glide.with(context) Glide.with(context)
.load(file) .load(uri)
.into(binding.imageView) .into(binding.imageView)
} }
FileManager.FileType.AUDIO -> { FileManager.FileType.AUDIO -> {
@@ -123,6 +164,27 @@ class ImagePreviewAdapter(
} }
} }
private fun showEncryptedError() {
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.imageView.setImageResource(R.drawable.encrypted)
}
private fun cleanupTempFile() {
tempDecryptedFile?.let { file ->
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)
}
}
tempDecryptedFile = null
}
}
private fun resetAudioUI() { private fun resetAudioUI() {
binding.playPause.setImageResource(R.drawable.play) binding.playPause.setImageResource(R.drawable.play)
binding.audioSeekBar.value = 0f binding.audioSeekBar.value = 0f

View File

@@ -0,0 +1,28 @@
package devs.org.calculator.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [HiddenFileEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun hiddenFileDao(): HiddenFileDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"calculator_database"
).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,33 @@
package devs.org.calculator.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface HiddenFileDao {
@Query("SELECT * FROM hidden_files")
fun getAllHiddenFiles(): Flow<List<HiddenFileEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHiddenFile(hiddenFile: HiddenFileEntity)
@Delete
suspend fun deleteHiddenFile(hiddenFile: HiddenFileEntity)
@Query("SELECT * FROM hidden_files WHERE filePath = :filePath")
suspend fun getHiddenFileByPath(filePath: String): HiddenFileEntity?
@Query("SELECT * FROM hidden_files WHERE fileName = :fileName")
suspend fun getHiddenFileByOriginalName(fileName: String): HiddenFileEntity?
@Query("UPDATE hidden_files SET isEncrypted = :isEncrypted, filePath = :newFilePath, encryptedFileName = :encryptedFileName WHERE filePath = :filePath")
suspend fun updateEncryptionStatus(
filePath: String,
newFilePath: String,
encryptedFileName: String?,
isEncrypted: Boolean
)
@Update
suspend fun updateHiddenFile(hiddenFile: HiddenFileEntity)
}

View File

@@ -0,0 +1,17 @@
package devs.org.calculator.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import devs.org.calculator.utils.FileManager
@Entity(tableName = "hidden_files")
data class HiddenFileEntity(
@PrimaryKey
val filePath: String, //absolute path of the file
val fileName: String, // Original filename with extension
val encryptedFileName: String, // Encrypted filename
val fileType: FileManager.FileType, //type of the file
val originalExtension: String, // original file extension
val isEncrypted: Boolean, // is the file encrypted or not
var dateAdded: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,27 @@
package devs.org.calculator.database
import kotlinx.coroutines.flow.Flow
class HiddenFileRepository(private val hiddenFileDao: HiddenFileDao) {
fun getAllHiddenFiles(): Flow<List<HiddenFileEntity>> {
return hiddenFileDao.getAllHiddenFiles()
}
suspend fun insertHiddenFile(hiddenFile: HiddenFileEntity) {
hiddenFileDao.insertHiddenFile(hiddenFile)
}
suspend fun deleteHiddenFile(hiddenFile: HiddenFileEntity) {
hiddenFileDao.deleteHiddenFile(hiddenFile)
}
suspend fun getHiddenFileByPath(filePath: String): HiddenFileEntity? {
return hiddenFileDao.getHiddenFileByPath(filePath)
}
suspend fun updateEncryptionStatus(filePath: String, newFilePath: String,encryptedFileName: String, isEncrypted: Boolean) {
hiddenFileDao.updateEncryptionStatus(filePath,newFilePath, encryptedFileName = encryptedFileName, isEncrypted)
}
}

View File

@@ -27,10 +27,20 @@ import java.io.File
import android.Manifest import android.Manifest
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.utils.PrefsUtil
import android.util.Log
import devs.org.calculator.database.HiddenFileEntity
import java.io.FileOutputStream
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) { class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest> private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
val intent = Intent() val intent = Intent()
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
companion object { companion object {
const val HIDDEN_DIR = ".CalculatorHide" const val HIDDEN_DIR = ".CalculatorHide"
@@ -38,6 +48,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
const val VIDEOS_DIR = "videos" const val VIDEOS_DIR = "videos"
const val AUDIO_DIR = "audio" const val AUDIO_DIR = "audio"
const val DOCS_DIR = "documents" const val DOCS_DIR = "documents"
const val ENCRYPTED_EXTENSION = ".enc"
} }
fun getHiddenDirectory(): File { fun getHiddenDirectory(): File {
@@ -92,7 +103,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
val extension = MimeTypeMap.getSingleton() val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(mimeType) ?: "" .getExtensionFromMimeType(mimeType) ?: ""
val fileName = "${System.currentTimeMillis()}.${extension}" val fileName = "${System.currentTimeMillis()}.${extension}"
val targetFile = File(targetDir, fileName) var targetFile = File(targetDir, fileName)
// Copy file using DocumentFile // Copy file using DocumentFile
contentResolver.openInputStream(uri)?.use { input -> contentResolver.openInputStream(uri)?.use { input ->
@@ -106,6 +117,8 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
throw Exception("File copy failed") throw Exception("File copy failed")
} }
// Encrypt file if encryption is enabled
// Media scan the new file to hide it // Media scan the new file to hide it
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(targetDir) mediaScanIntent.data = Uri.fromFile(targetDir)
@@ -179,41 +192,39 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
targetFile targetFile
} }
// Copy file content // Check if file is encrypted
file.copyTo(finalTargetFile, overwrite = false) if (file.extension == ENCRYPTED_EXTENSION) {
// Decrypt file
// Verify copy success val decryptedFile = SecurityUtils.changeFileExtension(file, SecurityUtils.getFileExtension(file))
if (finalTargetFile.exists() && finalTargetFile.length() > 0) { if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
// Delete original hidden file decryptedFile.copyTo(finalTargetFile, overwrite = false)
if (file.delete()) { decryptedFile.delete()
// Trigger media scan for the new file
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
context.sendBroadcast(mediaScanIntent)
withContext(Dispatchers.Main) {
Toast.makeText(context, context.getString(R.string.file_unhidden_successfully), Toast.LENGTH_SHORT).show()
onSuccess?.invoke() // Call success callback
}
} else { } else {
withContext(Dispatchers.Main) { throw Exception("Failed to decrypt file")
Toast.makeText(context, "File copied but failed to remove from hidden folder", Toast.LENGTH_SHORT).show()
onError?.invoke("Failed to remove from hidden folder")
}
} }
} else { } else {
withContext(Dispatchers.Main) { // Copy file content
Toast.makeText(context, "Failed to copy file", Toast.LENGTH_SHORT).show() file.copyTo(finalTargetFile, overwrite = false)
onError?.invoke("Failed to copy file")
}
} }
} catch (e: Exception) { // Verify copy success
withContext(Dispatchers.Main) { if (!finalTargetFile.exists() || finalTargetFile.length() == 0L) {
Toast.makeText(context, "Error unhiding file: ${e.message}", Toast.LENGTH_LONG).show() throw Exception("File copy failed")
onError?.invoke(e.message ?: "Unknown error")
} }
// Media scan the new file
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
context.sendBroadcast(mediaScanIntent)
withContext(Dispatchers.Main) {
onSuccess?.invoke()
}
} catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) {
onError?.invoke(e.message ?: "Unknown error occurred")
}
} }
} }
} }

View File

@@ -48,6 +48,10 @@ class PrefsUtil(context: Context) {
return stored == hashPassword(input) return stored == hashPassword(input)
} }
fun getPassword(): String{
return prefs.getString("password", "") ?: ""
}
fun saveSecurityQA(question: String, answer: String) { fun saveSecurityQA(question: String, answer: String) {
prefs.edit { prefs.edit {
putString("security_question", question) putString("security_question", question)

View File

@@ -2,63 +2,252 @@ package devs.org.calculator.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import android.content.SharedPreferences
import androidx.core.content.FileProvider
import devs.org.calculator.database.HiddenFileEntity
class SecurityUtils { object SecurityUtils {
companion object { private const val ALGORITHM = "AES"
private const val ALGORITHM = "AES" private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val HIDDEN_FOLDER = "Calculator_Data" private const val KEY_SIZE = 256
private const val TAG = "SecurityUtils"
fun validatePassword(input: String, storedHash: String): Boolean {
return input.hashCode().toString() == storedHash
}
fun encryptFile(context: Context, sourceUri: Uri, password: String): File { private fun getSecretKey(context: Context): SecretKey {
val inputStream = context.contentResolver.openInputStream(sourceUri) val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
val secretKey = generateKey(password) val encodedKey = keyStore.getString("secret_key", null)
val cipher = Cipher.getInstance(ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val hiddenDir = File(context.getExternalFilesDir(null), HIDDEN_FOLDER) return if (encodedKey != null) {
if (!hiddenDir.exists()) hiddenDir.mkdirs() try {
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
val encryptedFile = File(hiddenDir, "${System.currentTimeMillis()}_encrypted") SecretKeySpec(decodedKey, ALGORITHM)
val outputStream = FileOutputStream(encryptedFile) } catch (e: Exception) {
Log.e(TAG, "Error decoding stored key, generating new key", e)
inputStream?.use { input -> generateAndStoreNewKey(keyStore)
val buffer = ByteArray(1024)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
val encrypted = cipher.update(buffer, 0, read)
outputStream.write(encrypted)
}
val finalBlock = cipher.doFinal()
outputStream.write(finalBlock)
} }
outputStream.close() } else {
return encryptedFile Log.d(TAG, "No stored key found, generating new key")
} generateAndStoreNewKey(keyStore)
fun decryptFile(file: File, password: String): ByteArray {
val secretKey = generateKey(password)
val cipher = Cipher.getInstance(ALGORITHM)
cipher.init(Cipher.DECRYPT_MODE, secretKey)
val inputStream = FileInputStream(file)
val bytes = inputStream.readBytes()
inputStream.close()
return cipher.doFinal(bytes)
}
private fun generateKey(password: String): SecretKey {
val keyBytes = password.toByteArray().copyOf(16)
return SecretKeySpec(keyBytes, ALGORITHM)
} }
} }
private fun generateAndStoreNewKey(keyStore: SharedPreferences): SecretKey {
val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
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()
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
}
val secretKey = getSecretKey(context)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
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)
}
}
}
// 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
if (outputFile.exists()) {
outputFile.delete()
}
false
}
}
fun getDecryptedPreviewFile(context: Context, meta: HiddenFileEntity): File? {
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)
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(
context,
"${context.packageName}.provider", // Must match AndroidManifest
file
)
} catch (e: Exception) {
Log.e("PreviewUtils", "Error getting URI", e)
null
}
}
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 ->
CipherInputStream(decInput, cipher).use { cipherInput ->
cipherInput.copyTo(output)
}
}
}
}
// 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
if (outputFile.exists()) {
outputFile.delete()
}
false
}
}
fun getFileExtension(file: File): String {
val name = file.name
val lastDotIndex = name.lastIndexOf('.')
return if (lastDotIndex > 0) {
name.substring(lastDotIndex)
} else {
""
}
}
fun changeFileExtension(file: File, newExtension: String): File {
val name = file.name
val lastDotIndex = name.lastIndexOf('.')
val newName = if (lastDotIndex > 0) {
name.substring(0, lastDotIndex) + newExtension
} else {
name + newExtension
}
return File(file.parent, newName)
}
} }

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="50dp" android:viewportHeight="73" android:viewportWidth="73" android:width="50dp">
<path android:fillColor="#00FFFFFF" android:fillType="nonZero" android:pathData="M15,1L58,1A14,14 0,0 1,72 15L72,58A14,14 0,0 1,58 72L15,72A14,14 0,0 1,1 58L1,15A14,14 0,0 1,15 1z" android:strokeColor="#0004673E" android:strokeWidth="2"/>
<path android:fillColor="#00DD80" android:fillType="nonZero" android:pathData="M54.191,43.754C52.953,47.108 51.081,50.025 48.626,52.423C45.832,55.151 42.173,57.319 37.751,58.866C37.605,58.917 37.454,58.958 37.302,58.989C37.101,59.028 36.896,59.05 36.694,59.053L36.654,59.053C36.438,59.053 36.221,59.031 36.005,58.989C35.853,58.958 35.704,58.917 35.56,58.867C31.132,57.323 27.469,55.156 24.671,52.427C22.215,50.03 20.344,47.115 19.108,43.76C16.86,37.66 16.988,30.941 17.091,25.542L17.093,25.459C17.113,25.013 17.127,24.544 17.134,24.027C17.172,21.488 19.191,19.387 21.73,19.246C27.025,18.95 31.121,17.223 34.621,13.812L34.652,13.784C35.233,13.251 35.965,12.989 36.694,13C37.396,13.009 38.096,13.271 38.657,13.784L38.687,13.812C42.187,17.223 46.283,18.95 51.578,19.246C54.118,19.387 56.137,21.488 56.174,24.027C56.182,24.548 56.195,25.016 56.216,25.459L56.217,25.494C56.319,30.904 56.446,37.636 54.191,43.754Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#03D078" android:fillType="nonZero" android:pathData="M54.191,43.754C52.953,47.108 51.081,50.025 48.626,52.423C45.832,55.151 42.173,57.319 37.751,58.866C37.605,58.917 37.454,58.958 37.302,58.989C37.101,59.028 36.896,59.05 36.694,59.053L36.694,13C37.396,13.009 38.096,13.271 38.657,13.784L38.687,13.812C42.187,17.223 46.283,18.95 51.578,19.246C54.118,19.387 56.137,21.488 56.174,24.027C56.182,24.548 56.195,25.016 56.216,25.459L56.217,25.494C56.319,30.904 56.446,37.636 54.191,43.754Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#00FFFFFF" android:fillType="nonZero" android:pathData="M48.131,36.026C48.131,42.341 43.003,47.482 36.694,47.504L36.653,47.504C30.325,47.504 25.176,42.355 25.176,36.026C25.176,29.698 30.325,24.549 36.653,24.549L36.694,24.549C43.003,24.571 48.131,29.712 48.131,36.026Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#00E1EBF0" android:fillType="nonZero" android:pathData="M48.131,36.026C48.131,42.341 43.003,47.482 36.694,47.504L36.694,24.549C43.003,24.571 48.131,29.712 48.131,36.026Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#FFFFFF" android:fillType="nonZero" android:pathData="M41.863,34.374L36.694,39.543L35.577,40.66C35.313,40.924 34.967,41.056 34.621,41.056C34.275,41.056 33.929,40.924 33.665,40.66L31.264,38.258C30.736,37.73 30.736,36.875 31.264,36.347C31.791,35.819 32.646,35.819 33.174,36.347L34.621,37.794L39.952,32.463C40.48,31.935 41.336,31.935 41.863,32.463C42.391,32.991 42.391,33.847 41.863,34.374Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#F8F6F6" android:fillType="nonZero" android:pathData="M41.863,34.374L36.694,39.543L36.694,35.721L39.952,32.463C40.48,31.935 41.336,31.935 41.863,32.463C42.391,32.991 42.391,33.847 41.863,34.374Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

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

View File

@@ -59,6 +59,13 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_play_circle" android:src="@drawable/ic_play_circle"
android:layout_gravity="center"/> android:layout_gravity="center"/>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
android:src="@drawable/encrypted"
android:layout_gravity="end"
android:id="@+id/encrypted"/>
<ImageView <ImageView
android:id="@+id/selected" android:id="@+id/selected"
android:layout_width="20dp" android:layout_width="20dp"
@@ -69,6 +76,7 @@
android:src="@drawable/selected" android:src="@drawable/selected"
android:background="@drawable/gradient_bg" android:background="@drawable/gradient_bg"
android:layout_gravity="end"/> android:layout_gravity="end"/>
</FrameLayout> </FrameLayout>

View File

@@ -121,7 +121,7 @@
<string name="app_settings">App Settings</string> <string name="app_settings">App Settings</string>
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string> <string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
<string name="security_settings">Security Settings</string> <string name="security_settings">Security Settings</string>
<string name="version">Version 1.3</string> <string name="version">Version 1.4.0</string>
<string name="app_details">App Details</string> <string name="app_details">App Details</string>
<string name="setup_password">Setup Password</string> <string name="setup_password">Setup Password</string>
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string> <string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
@@ -150,4 +150,13 @@
<string name="if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app">If you turn on/off this option, dynamic theme changes will be visible after you reopen the app.</string> <string name="if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app">If you turn on/off this option, dynamic theme changes will be visible after you reopen the app.</string>
<string name="attention">Attention!</string> <string name="attention">Attention!</string>
<string name="are_you_sure_to_delete_selected_files_permanently">Are you sure to delete selected files permanently ?</string> <string name="are_you_sure_to_delete_selected_files_permanently">Are you sure to delete selected files permanently ?</string>
<string name="encrypt_file_when_hiding">Encrypt File When Hiding</string>
<string name="encrypt_file">Encrypt File</string>
<string name="decrypt_file">Decrypt File</string>
<string name="encrypt_files">Encrypt Files</string>
<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 do this. Do you want to continue?</string>
<string name="decryption_disclaimer">Warning: This will decrypt the selected files. The encrypted files will be deleted after successful decryption. Make sure to do this. Do you want to continue?</string>
</resources> </resources>

View File

@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Allow access to external files -->
<external-path name="external_files" path="." /> <external-path name="external_files" path="." />
<!-- Allow access to hidden directories -->
<external-files-path name="hidden_files" path=".CalculatorHide/" /> <external-files-path name="hidden_files" path=".CalculatorHide/" />
<cache-path name="cache_files" path="." />
</paths> </paths>

View File

@@ -1,27 +1,31 @@
[versions] [versions]
agp = "8.9.1" agp = "8.9.3"
documentfile = "1.0.1" documentfile = "1.1.0"
exp4j = "0.4.8" exp4j = "0.4.8"
glide = "4.16.0" glide = "4.16.0"
kotlin = "1.9.24" kotlin = "2.0.0"
coreKtx = "1.15.0" coreKtx = "1.16.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
appcompat = "1.7.0" appcompat = "1.7.0"
lottie = "6.2.0" lottie = "6.2.0"
material = "1.12.0" material = "1.12.0"
activity = "1.9.3" activity = "1.10.1"
constraintlayout = "2.2.0" constraintlayout = "2.2.1"
materialColorUtilities = "1.3.0" materialColorUtilities = "1.3.0"
gridlayout = "1.0.0" gridlayout = "1.1.0"
photoview = "2.3.0" photoview = "2.3.0"
roomRuntime = "2.7.1"
viewpager = "1.1.0" viewpager = "1.1.0"
zoomage = "1.3.1" zoomage = "1.3.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" } androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" } exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }