30 Commits

Author SHA1 Message Date
Binondi Borthakur
82982aa221 Added Android Freeware 2025-07-12 10:44:51 +05:30
Binondi Borthakur
6f151554f0 Updated Version 2025-07-12 10:34:45 +05:30
Binondi
5ecd3ec7d1 Merge remote-tracking branch 'origin/master' 2025-07-11 14:00:13 +05:30
Binondi
946953d4eb Fixed - App Crashing in lower android versions, added edge to edge for devices higher then android 15 2025-07-11 13:59:47 +05:30
Binondi Borthakur
18172f5191 Update README.md 2025-06-09 00:41:38 +05:30
Binondi Borthakur
603d96b672 Added - payment qr 2025-06-09 00:35:57 +05:30
Binondi Borthakur
78ea4596ea Update README.md 2025-06-09 00:34:48 +05:30
Binondi Borthakur
a70a569b57 Added - Payment QR 2025-06-09 00:31:00 +05:30
Binondi Borthakur
df0db1a479 Made Some. Changes 2025-06-09 00:30:18 +05:30
Binondi Borthakur
9cdc6eb1a4 Added - Payment QR 2025-06-09 00:21:45 +05:30
Binondi Borthakur
913af83b90 Updated Some Details 2025-06-09 00:11:45 +05:30
Binondi
7ceb599d9f Fixed - File Hiding 2025-06-08 09:08:13 +05:30
Binondi
dd1767e55d Fixed - Realtime Calculation Update 2025-06-07 21:37:35 +05:30
Binondi
28ccdda1bf Fixed - Realtime Calculation Update 2025-06-07 21:34:26 +05:30
Binondi
27a538f7c6 Fixed - Initial file update problem 2025-06-07 21:33:38 +05:30
Binondi
22c2a64450 Fixed - File loading bugs, optimized refresh using DiffUtil more efficiently 2025-06-07 20:42:01 +05:30
Binondi
3af3f81f3c Fixed - File loading bugs, optimized refresh using DiffUtil more efficiently 2025-06-07 19:50:25 +05:30
Binondi
f2e206f208 Merge remote-tracking branch 'origin/master' 2025-06-07 19:27:21 +05:30
Binondi
2de1b28afe Fixed - File loading bugs, optimized refresh using DiffUtil more efficiently 2025-06-07 19:24:32 +05:30
Binondi Borthakur
ccf291d2e2 Added - Banner 2025-06-07 10:47:09 +05:30
Binondi Borthakur
0968d5c19b Fixed - Banner Problem 2025-06-06 23:27:45 +05:30
Binondi Borthakur
d64904fbe7 Added - banner 2025-06-06 23:26:56 +05:30
Binondi Borthakur
2da2c944a3 Added - The Real Downloads Count 2025-06-06 22:56:50 +05:30
Binondi Borthakur
13e1fca28f Made Some Changes for Version 1.4.0 2025-06-06 22:32:34 +05:30
Binondi
5c5e0e4be8 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
#	app/src/main/assets/Screenshot_9.jpg
2025-06-06 21:24:11 +05:30
Binondi
aad939463c Updated Assets, Added Download Button, Added Banner 2025-06-06 21:16:26 +05:30
Binondi Borthakur
e4e2983acd Added - Information for Version 1.4.0 2025-06-06 20:37:39 +05:30
Binondi Borthakur
8702491f85 Added - file encryption, custom key creation. 2025-06-06 19:01:52 +05:30
Binondi Borthakur
5263f89cd3 Added - file encryption, custom key creation. 2025-06-06 18:49:10 +05:30
Binondi Borthakur
0787d6dd5b Update README.md 2025-06-06 18:45:43 +05:30
40 changed files with 939 additions and 921 deletions

View File

@@ -1,23 +1,31 @@
![App banner](media/banner.png)
<div align="center">
<img src="app/src/main/assets/logo.png" alt="Calculator Hide File App Logo" width="200" />
# 📂 Calculator Hide File App for Android 📂
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
<img alt="Latest release" src="https://img.shields.io/badge/Releases-v1.0-blue?logo=github&style=for-the-badge">
<img alt="Latest release" src="https://img.shields.io/badge/Releases-v1.4.2-blue?logo=github&style=for-the-badge">
</a>
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
<img alt="Downloads" src="https://img.shields.io/badge/Downloads-1.3k-blue?logo=github&style=for-the-badge">
<img alt="Downloads" src="https://img.shields.io/badge/downloads-1.1k-blue?logo=github&style=for-the-badge">
</a>
<a href="LICENSE">
<img alt="Apache License 2.0" src="https://img.shields.io/badge/License-Apache_2.0-blue?logo=github&style=for-the-badge">
<img alt="Apache License 2.0" src="https://img.shields.io/badge/License-Apache_2.0-blue?logo=github&style=for-the-badge">
</a>
</div>
---
<br>
</div>
<div align="center">
<h4>Download</h4>
<a>[<img src="https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/binondi/Calculator-Hide-Files/releases) </a><a href="https://apt.izzysoft.de/fdroid/index/apk/devs.org.calculator"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" height="80"></a> <a href="https://www.androidfreeware.net/download-apk-devs-org-calculator.html"><img src="https://www.androidfreeware.net/images/androidfreeware-badge.png" height="80"></a>
</div>
## 😍 Why Choose This App?
The **Calculator Hide File App** is an **open-source** application, allowing you to inspect the code yourself. This ensures **complete transparency** and guarantees that your **privacy remains uncompromised**. 🔒✅
@@ -33,37 +41,37 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
> - Hide images, videos, documents & other files securely.
> - Works like a **real calculator** with hidden storage mode.
> - No one will suspect its a file vault!
> - **Encrypt** files with your **custom key** to secure your hidden files.
---
## 🚀 Features
**Dual Functionality** A working **calculator** & a **file vault** in one app.
**Secret Passcode** Unlock hidden files by entering a secret code.
**Secure File Manager** Hide/unhide files easily.
**Secure File Manager** Hide/Unhide files easily.
**Fast & Lightweight** Smooth performance on all Android devices.
**No Root Required** Works without rooting your phone.
**Security** Encrypt & Decrypt files.
---
## 🖼️ Screenshots
<div align="center">
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="32%">
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hidle File App - Passcode Protection" width="32%">
<img src="media/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
<img src="media/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="32%">
<img src="media/Screenshot_3.jpg" alt="Calculator Hide File App - Passcode Protection" width="32%">
</div>
<div align="center">
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_5.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_6.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_5.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_6.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
</div>
<div align="center">
<img src="app/src/main/assets/Screenshot_7.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_8.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_9.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_7.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_8.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="media/Screenshot_9.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
</div>
---
@@ -106,7 +114,7 @@ git clone https://github.com/Binondi/Calculator-Hide-Files.git
## 🛠️ Technologies Used
- **Programming Language**: Kotlin
- **UI Framework**: XML (For UI)
- **UI Framework**: XML
- **File Storage**: Secure internal storage & MediaStore API
---
@@ -125,7 +133,12 @@ If you find this app useful, please consider supporting the development. 🙏
[![Sponsor on GitHub](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/Binondi)
[![Donate via PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/BinondiBorthakur56)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/binondi)
- **UPI ID** 📱
``
binondi@naviaxis
``
---
## 🔧 Contributing

View File

@@ -11,9 +11,10 @@ android {
defaultConfig {
applicationId = "devs.org.calculator"
minSdk = 26
//noinspection OldTargetApi
targetSdk = 34
versionCode = 5
versionName = "1.4.0"
versionCode = 7
versionName = "1.4.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -81,9 +82,11 @@ dependencies {
implementation(libs.androidx.viewpager)
implementation(libs.zoomage)
implementation(libs.lottie)
implementation(libs.androidx.swiperefreshlayout)
// Room dependencies
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
//noinspection KaptUsageInsteadOfKsp
kapt(libs.androidx.room.compiler)
}

View File

@@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 4,
"versionName": "1.3",
"versionCode": 6,
"versionName": "1.4.1",
"outputFile": "app-release.apk"
}
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -11,7 +11,10 @@ import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
@@ -47,7 +50,12 @@ class HiddenActivity : AppCompatActivity() {
fileManager = FileManager(this, this)
folderManager = FolderManager()
dialogUtil = DialogUtil(this)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupInitialUIState()
setupClickListeners()

View File

@@ -24,6 +24,10 @@ import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder
import java.util.regex.Pattern
import androidx.core.content.edit
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.utils.StoragePermissionUtil
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
private lateinit var binding: ActivityMainBinding
@@ -36,6 +40,8 @@ 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 lateinit var storagePermissionUtil: StoragePermissionUtil
private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,15 +49,30 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, 0, systemBars.right, systemBars.bottom)
insets
}
permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
storagePermissionUtil.handlePermissionResult(permissions)
}
// Initialize StoragePermissionUtil
storagePermissionUtil = StoragePermissionUtil(this)
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(result)
}
if (sp.getBoolean("isFirst", true)){
binding.display.text = getString(R.string.enter_123456)
}
if(!Environment.isExternalStorageManager()) {
if (!storagePermissionUtil.hasStoragePermission()) {
dialogUtil.showMaterialDialog(
getString(R.string.storage_permission),
getString(R.string.to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission) +
@@ -61,7 +82,10 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
getString(R.string.later),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
fileManager.askPermission(this@MainActivity)
storagePermissionUtil.requestStoragePermission(permissionLauncher) {
Toast.makeText(this@MainActivity, getString(R.string.permission_granted), Toast.LENGTH_SHORT).show()
}
}
override fun onNegativeButtonClicked() {
@@ -75,9 +99,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
getString(R.string.you_can_grant_permission_later_from_settings),
Toast.LENGTH_LONG).show()
}
}
)
})
}
setupNumberButton(binding.btn0, "0")
setupNumberButton(binding.btn00, "00")
setupNumberButton(binding.btn1, "1")
@@ -150,7 +176,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
}
private fun clearDisplay() {
currentExpression = ""
currentExpression = "0"
binding.total.text = ""
lastWasOperator = false
lastWasPercent = false
@@ -335,13 +361,17 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
}
try {
if (currentExpression.isEmpty() ||
(isOperator(currentExpression.last().toString()) && currentExpression.last() != '%')) {
if (currentExpression.isEmpty()) {
binding.total.text = ""
return
}
var processedExpression = currentExpression.replace("×", "*")
if (isOperator(processedExpression.last().toString())) {
processedExpression = processedExpression.substring(0, processedExpression.length - 1)
}
if (processedExpression.contains("%")) {
processedExpression = preprocessExpression(processedExpression)
}

View File

@@ -6,7 +6,10 @@ import android.os.Handler
import android.os.Looper
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import devs.org.calculator.R
@@ -42,7 +45,12 @@ class PreviewActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityPreviewBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
fileManager = FileManager(this, this)
currentPosition = intent.getIntExtra("position", 0)

View File

@@ -6,9 +6,12 @@ import android.view.View
import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
@@ -29,6 +32,12 @@ class SettingsActivity : AppCompatActivity() {
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
prefs = PrefsUtil(this)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
DEV_GITHUB_URL = getString(R.string.github_profile)
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
setupUI()

View File

@@ -5,7 +5,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
@@ -26,7 +29,12 @@ class SetupPasswordActivity : AppCompatActivity() {
binding = ActivitySetupPasswordBinding.inflate(layoutInflater)
binding2 = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
prefsUtil = PrefsUtil(this)
hasPassword = prefsUtil.hasPassword()

View File

@@ -7,7 +7,6 @@ import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.animation.Animation
@@ -23,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderSelectionAdapter
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.HiddenFileEntity
@@ -41,6 +39,11 @@ import java.io.File
import android.widget.CheckBox
import android.widget.CompoundButton
import android.app.AlertDialog
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import devs.org.calculator.adapters.FileAdapter
class ViewFolderActivity : AppCompatActivity() {
@@ -71,6 +74,12 @@ class ViewFolderActivity : AppCompatActivity() {
binding = ActivityViewFolderBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupAnimations()
initialize()
setupClickListeners()
@@ -142,16 +151,19 @@ class ViewFolderActivity : AppCompatActivity() {
customDialog?.dismiss()
customDialog = null
updateFilesToAdapter()
refreshCurrentFolder()
}, remainingTime)
} else {
customDialog?.dismiss()
customDialog = null
refreshCurrentFolder()
updateFilesToAdapter()
}
}
private fun updateFilesToAdapter() {
openFolder(currentFolder!!)
val files = folderManager.getFilesInFolder(currentFolder!!)
fileAdapter?.submitList(files)
}
@@ -199,6 +211,16 @@ class ViewFolderActivity : AppCompatActivity() {
mainHandler.postDelayed({
dismissCustomDialog()
val files = folderManager.getFilesInFolder(targetFolder)
if (files.isNotEmpty()) {
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
if (fileAdapter == null) {
showFileList(files, targetFolder)
} else {
fileAdapter?.submitList(files.toMutableList())
}
}
}, 1000)
}
}
@@ -223,6 +245,7 @@ class ViewFolderActivity : AppCompatActivity() {
).show()
dismissCustomDialog()
}
e.printStackTrace()
}
}
}
@@ -230,9 +253,19 @@ class ViewFolderActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
refreshCurrentFolder()
setupFlagSecure()
}
private fun openFolder(folder: File) {
private fun setupFlagSecure() {
if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
fun openFolder(folder: File) {
if (!folder.exists()) {
folder.mkdirs()
File(folder, ".nomedia").createNewFile()
@@ -246,11 +279,12 @@ class ViewFolderActivity : AppCompatActivity() {
} else {
showEmptyState()
}
binding.swipeLayout.isRefreshing = false
}
private fun showEmptyState() {
binding.noItems.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
binding.swipeLayout.visibility = View.GONE
}
private fun showFileList(files: List<File>, folder: File) {
@@ -261,17 +295,20 @@ class ViewFolderActivity : AppCompatActivity() {
onFolderLongClick = { isSelected ->
handleFileSelectionModeChange(isSelected)
}).apply {
setFileOperationCallback(object : FileAdapter.FileOperationCallback {
setFilesOperationCallback(object : FileAdapter.FilesOperationCallback {
override fun onFileDeleted(file: File) {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onFileRenamed(oldFile: File, newFile: File) {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onRefreshNeeded() {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
@@ -287,7 +324,7 @@ class ViewFolderActivity : AppCompatActivity() {
}
binding.recyclerView.adapter = fileAdapter
binding.recyclerView.visibility = View.VISIBLE
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
binding.menuButton.setOnClickListener {
@@ -455,21 +492,18 @@ class ViewFolderActivity : AppCompatActivity() {
lifecycleScope.launch {
var successCount = 0
var failCount = 0
val decryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
filePath = file.absolutePath,
@@ -479,27 +513,23 @@ class ViewFolderActivity : AppCompatActivity() {
)
}
if (file.delete()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
val extension = when (fileType) {
FileManager.FileType.IMAGE -> ".jpg"
FileManager.FileType.VIDEO -> ".mp4"
@@ -509,11 +539,8 @@ class ViewFolderActivity : AppCompatActivity() {
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = decryptedFile.absolutePath,
@@ -525,37 +552,32 @@ class ViewFolderActivity : AppCompatActivity() {
)
)
if (file.delete()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
e.printStackTrace()
}
}
mainHandler.post {
fileAdapter?.exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
@@ -567,7 +589,10 @@ class ViewFolderActivity : AppCompatActivity() {
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
refreshCurrentFolder()
if (successCount > 0) {
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
}
@@ -627,10 +652,19 @@ class ViewFolderActivity : AppCompatActivity() {
val files = folderManager.getFilesInFolder(folder)
mainHandler.post {
if (files.isNotEmpty()) {
binding.recyclerView.visibility = View.VISIBLE
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
fileAdapter?.submitList(files.toMutableList())
val currentFiles = fileAdapter?.currentList ?: emptyList()
val hasChanges = files.size != currentFiles.size ||
files.any { newFile ->
currentFiles.none { it.absolutePath == newFile.absolutePath }
}
if (hasChanges) {
fileAdapter?.submitList(files.toMutableList())
}
fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) {
showFileSelectionIcons()
@@ -642,8 +676,7 @@ class ViewFolderActivity : AppCompatActivity() {
showEmptyState()
}
}
} catch (e: Exception) {
} catch (_: Exception) {
mainHandler.post {
showEmptyState()
}
@@ -659,6 +692,9 @@ class ViewFolderActivity : AppCompatActivity() {
binding.back.setOnClickListener {
finish()
}
binding.swipeLayout.setOnRefreshListener {
openFolder(currentFolder!!)
}
binding.addImage.setOnClickListener { openFilePicker("image/*") }
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
@@ -748,6 +784,7 @@ class ViewFolderActivity : AppCompatActivity() {
private fun performFileUnhiding(selectedFiles: List<File>) {
lifecycleScope.launch {
var allUnhidden = true
val unhiddenFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
@@ -767,6 +804,7 @@ class ViewFolderActivity : AppCompatActivity() {
}
file.delete()
decryptedFile.delete()
unhiddenFiles.add(file)
} else {
decryptedFile.delete()
allUnhidden = false
@@ -794,6 +832,7 @@ class ViewFolderActivity : AppCompatActivity() {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
unhiddenFiles.add(file)
} else {
allUnhidden = false
}
@@ -802,8 +841,8 @@ class ViewFolderActivity : AppCompatActivity() {
}
}
} catch (e: Exception) {
allUnhidden = false
e.printStackTrace()
}
}
@@ -815,8 +854,8 @@ class ViewFolderActivity : AppCompatActivity() {
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
@@ -824,17 +863,19 @@ class ViewFolderActivity : AppCompatActivity() {
private fun performFileDeletion(selectedFiles: List<File>) {
lifecycleScope.launch {
var allDeleted = true
val deletedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
if (!file.delete()) {
if (file.delete()) {
deletedFiles.add(file)
} else {
allDeleted = false
}
} catch (e: Exception) {
} catch (_: Exception) {
allDeleted = false
}
}
@@ -847,8 +888,8 @@ class ViewFolderActivity : AppCompatActivity() {
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
@@ -862,6 +903,7 @@ class ViewFolderActivity : AppCompatActivity() {
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
lifecycleScope.launch {
var allCopied = true
val copiedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
@@ -880,17 +922,18 @@ class ViewFolderActivity : AppCompatActivity() {
)
)
}
copiedFiles.add(file)
} catch (e: Exception) {
allCopied = false
e.printStackTrace()
}
}
mainHandler.post {
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
@@ -898,6 +941,7 @@ class ViewFolderActivity : AppCompatActivity() {
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
lifecycleScope.launch {
var allMoved = true
val movedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
@@ -913,22 +957,27 @@ class ViewFolderActivity : AppCompatActivity() {
)
}
file.delete()
if (file.delete()) {
movedFiles.add(file)
} else {
allMoved = false
}
} catch (e: Exception) {
allMoved = false
e.printStackTrace()
}
}
mainHandler.post {
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
@SuppressLint("InflateParams")
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
val folders = folderManager.getFoldersInDirectory(hiddenDir)
.filter { it != currentFolder }

View File

@@ -9,9 +9,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.lifecycle.LifecycleOwner
@@ -26,7 +23,9 @@ import devs.org.calculator.activities.PreviewActivity
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ListItemFileBinding
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FolderManager
import devs.org.calculator.utils.SecurityUtils
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
@@ -40,22 +39,18 @@ class FileAdapter(
private val lifecycleOwner: LifecycleOwner,
private val currentFolder: File,
private val showFileName: Boolean,
private val onFolderLongClick: (Boolean) -> Unit
) : ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback()) {
private val onFolderLongClick: (Boolean) -> Unit,
) : ListAdapter<File, FileAdapter.FilesViewHolder>(FileDiffCallback()) {
private var filesOperationCallback: WeakReference<FilesOperationCallback>? = null
private val selectedItems = mutableSetOf<Int>()
private var isSelectionMode = false
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
private val fileExecutor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper())
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
interface FileOperationCallback {
interface FilesOperationCallback {
fun onFileDeleted(file: File)
fun onFileRenamed(oldFile: File, newFile: File)
fun onRefreshNeeded()
@@ -63,197 +58,77 @@ class FileAdapter(
fun onSelectionCountChanged(selectedCount: Int)
}
fun setFileOperationCallback(callback: FileOperationCallback?) {
fileOperationCallback = callback?.let { WeakReference(it) }
fun setFilesOperationCallback(callback: FilesOperationCallback?) {
filesOperationCallback = callback?.let { WeakReference(it) }
}
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
val selectedLayer: View = view.findViewById(R.id.selectedLayer)
val shade: View = view.findViewById(R.id.shade)
val selected: ImageView = view.findViewById(R.id.selected)
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): FilesViewHolder {
val binding =
ListItemFileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return FilesViewHolder(binding)
}
override fun onBindViewHolder(
holder: FilesViewHolder,
position: Int,
) {
holder.bind(getItem(position))
}
inner class FilesViewHolder(private val binding: ListItemFileBinding) :
RecyclerView.ViewHolder(binding.root) {
@SuppressLint("FileEndsWithExt")
fun bind(file: File) {
val position = adapterPosition
lifecycleOwner.lifecycleScope.launch {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
else {
FileManager(context, lifecycleOwner).getFileType(file)
}
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
val currentFileData =
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
val currentFileType = currentFileData?.fileType ?: FileManager(
context,
lifecycleOwner
).getFileType(file)
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
setupClickListeners(file, fileType)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
val position = adapterPosition
val isCurrentFileEncrypted = currentFileData?.isEncrypted ?: file.endsWith(
SecurityUtils.ENCRYPTED_EXTENSION
)
setupClickListeners(file, currentFileType)
setupDisplay(
file,
currentFileType,
isCurrentFileEncrypted,
currentFileData
)
binding.fileNameTextView.text = if (isCurrentFileEncrypted) currentFileData?.fileName else file.name
binding.fileNameTextView.visibility =
if (showFileName) View.VISIBLE else View.GONE
binding.shade.visibility = if (showFileName) View.VISIBLE else View.GONE
if (position != RecyclerView.NO_POSITION) {
val isSelected = selectedItems.contains(position)
updateSelectionUI(isSelected)
}
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
binding.encrypted.visibility =
if (isCurrentFileEncrypted) View.VISIBLE else View.GONE
} catch (e: Exception) {
Log.e("FileAdapter", "Error in bind: ${e.message}")
}
}
}
fun bind(file: File, payloads: List<Any>) {
if (payloads.isEmpty()) {
bind(file)
return
}
val changes = payloads.firstOrNull() as? List<String>
changes?.forEach { change ->
when (change) {
"NAME_CHANGED" -> {
fileNameTextView.text = file.name
}
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
}
"SELECTION_CHANGED" -> {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val isSelected = selectedItems.contains(position)
updateSelectionUI(isSelected)
notifySelectionModeChange()
}
}
}
}
}
private fun updateSelectionUI(isSelected: Boolean) {
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
selected.visibility = if (isSelected) View.VISIBLE else View.GONE
}
private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
fileNameTextView.text = metadata?.fileName ?: file.name
when (fileType) {
FileManager.FileType.IMAGE -> {
playIcon.visibility = View.GONE
if (isEncrypted) {
try {
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
val uri = getUriForPreviewFile(context, decryptedFile)
if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.encrypted)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.ic_image)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
}
}
FileManager.FileType.VIDEO -> {
playIcon.visibility = View.VISIBLE
if (isEncrypted) {
try {
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
val uri = getUriForPreviewFile(context, decryptedFile)
if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.encrypted)
.into(imageView)
imageView.setPadding(0, 0, 0, 0)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.ic_video)
.into(imageView)
}
}
FileManager.FileType.AUDIO -> {
playIcon.visibility = View.GONE
if (isEncrypted) {
imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_audio)
}
imageView.setPadding(50, 50, 50, 50)
}
else -> {
playIcon.visibility = View.GONE
if (isEncrypted) {
imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_document)
}
imageView.setPadding(50, 50, 50, 50)
}
}
}
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
itemView.setOnClickListener {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return@setOnClickListener
if (isSelectionMode) {
toggleSelection(position)
} else {
openFile(file, fileType)
}
}
itemView.setOnLongClickListener {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
if (!isSelectionMode) {
enterSelectionMode()
toggleSelection(position)
}
true
}
}
private fun openFile(file: File, fileType: FileManager.FileType) {
if (!file.exists()) {
Toast.makeText(context,
@@ -305,7 +180,7 @@ class FileAdapter(
} else {
openInPreview(fileType)
}
} catch (e: Exception) {
} catch (_: Exception) {
mainHandler.post {
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
}
@@ -317,6 +192,39 @@ class FileAdapter(
}
}
private fun showDecryptionTypeDialog(file: File) {
val options = arrayOf("Image", "Video", "Audio")
MaterialAlertDialogBuilder(context)
.setTitle("Select File Type")
.setMessage("Please select the type of file to decrypt")
.setItems(options) { _, which ->
val selectedType = when (which) {
0 -> FileManager.FileType.IMAGE
1 -> FileManager.FileType.VIDEO
2 -> FileManager.FileType.AUDIO
else -> FileManager.FileType.DOCUMENT
}
performDecryptionWithType(file, selectedType)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun openInPreview(fileType: FileManager.FileType) {
val fileTypeString = when (fileType) {
FileManager.FileType.IMAGE -> context.getString(R.string.image)
FileManager.FileType.VIDEO -> context.getString(R.string.video)
else -> "unknown"
}
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
}
context.startActivity(intent)
}
private fun openAudioFile(file: File) {
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
try {
@@ -332,7 +240,7 @@ class FileAdapter(
putExtra("position", adapterPosition)
}
context.startActivity(intent)
} catch (e: Exception) {
} catch (_: Exception) {
Toast.makeText(
context,
context.getString(R.string.no_audio_player_found),
@@ -354,7 +262,7 @@ class FileAdapter(
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(intent)
} catch (e: Exception) {
} catch (_: Exception) {
Toast.makeText(
context,
@@ -364,84 +272,126 @@ class FileAdapter(
}
}
private fun openInPreview(fileType: FileManager.FileType) {
val fileTypeString = when (fileType) {
FileManager.FileType.IMAGE -> context.getString(R.string.image)
FileManager.FileType.VIDEO -> context.getString(R.string.video)
else -> "unknown"
private fun setupDisplay(
file: File,
type: FileManager.FileType,
isCurrentFileEncrypted: Boolean,
metadata: HiddenFileEntity?,
) {
when (type) {
FileManager.FileType.IMAGE -> {
binding.videoPlay.visibility = View.GONE
binding.fileIconImageView.setPadding(0, 0, 0, 0)
if (isCurrentFileEncrypted) {
try {
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
val uri = getUriForPreviewFile(context, decryptedFile)
if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false)
.error(R.drawable.encrypted)
.into(binding.fileIconImageView)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
Log.e("FileAdapter", "Error displaying encrypted image: ${e.message}")
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.into(binding.fileIconImageView)
}
}
FileManager.FileType.VIDEO -> {
binding.fileIconImageView.setPadding(0, 0, 0, 0)
binding.videoPlay.visibility = View.VISIBLE
if (isCurrentFileEncrypted) {
try {
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
val uri = getUriForPreviewFile(context, decryptedFile)
if (uri != null) {
Glide.with(context)
.load(uri)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.encrypted)
.into(binding.fileIconImageView)
binding.fileIconImageView.setPadding(0, 0, 0, 0)
} else {
showEncryptedIcon()
}
} else {
showEncryptedIcon()
}
} catch (e: Exception) {
Log.e("FileAdapter", "Error displaying encrypted video: ${e.message}")
showEncryptedIcon()
}
} else {
Glide.with(context)
.load(file)
.into(binding.fileIconImageView)
}
}
FileManager.FileType.AUDIO -> {
binding.videoPlay.visibility = View.GONE
binding.fileIconImageView.setPadding(25, 25, 25, 25)
binding.fileIconImageView.setImageResource(R.drawable.ic_audio)
}
else -> {
binding.videoPlay.visibility = View.GONE
binding.fileIconImageView.setPadding(25, 25, 25, 25)
binding.fileIconImageView.setImageResource(R.drawable.ic_document)
}
}
}
private fun showEncryptedIcon() {
binding.fileIconImageView.setImageResource(R.drawable.encrypted)
}
private fun updateSelectionUI(isSelected: Boolean) {
binding.selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
binding.selected.visibility = if (isSelected) View.VISIBLE else View.GONE
}
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
itemView.setOnClickListener {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return@setOnClickListener
if (isSelectionMode) {
toggleSelection(position)
} else {
openFile(file,fileType)
}
}
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
}
context.startActivity(intent)
}
itemView.setOnLongClickListener {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
@SuppressLint("MissingInflatedId")
private fun renameFile(file: File) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
inputEditText.setText(file.name)
inputEditText.selectAll()
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.rename_file))
.setView(dialogView)
.setPositiveButton(context.getString(R.string.rename)) { dialog, _ ->
val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != file.name) {
if (isValidFileName(newName)) {
renameFileAsync(file, newName)
} else {
Toast.makeText(context, "Invalid file name", Toast.LENGTH_SHORT).show()
}
}
dialog.dismiss()
}
.setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.create()
.show()
}
private fun isValidFileName(fileName: String): Boolean {
val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
return fileName.isNotBlank() &&
fileName.none { it in forbiddenChars } &&
!fileName.startsWith(".") &&
fileName.length <= 255
}
private fun renameFileAsync(file: File, newName: String) {
fileExecutor.execute {
val parentDir = file.parentFile
if (parentDir != null) {
val newFile = File(parentDir, newName)
if (newFile.exists()) {
mainHandler.post {
Toast.makeText(context, "File with this name already exists", Toast.LENGTH_SHORT).show()
}
return@execute
}
val success = try {
file.renameTo(newFile)
} catch (e: Exception) {
false
}
mainHandler.post {
if (success) {
fileOperationCallback?.get()?.onFileRenamed(file, newFile)
Toast.makeText(context, "File renamed", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to rename file", Toast.LENGTH_SHORT).show()
}
}
if (!isSelectionMode) {
enterSelectionMode()
toggleSelection(position)
}
true
}
}
@@ -459,29 +409,6 @@ class FileAdapter(
notifyItemChanged(position)
}
private fun showEncryptedIcon() {
imageView.setImageResource(R.drawable.encrypted)
imageView.setPadding(50, 50, 50, 50)
}
private fun showDecryptionTypeDialog(file: File) {
val options = arrayOf("Image", "Video", "Audio")
MaterialAlertDialogBuilder(context)
.setTitle("Select File Type")
.setMessage("Please select the type of file to decrypt")
.setItems(options) { _, which ->
val selectedType = when (which) {
0 -> FileManager.FileType.IMAGE
1 -> FileManager.FileType.VIDEO
2 -> FileManager.FileType.AUDIO
else -> FileManager.FileType.DOCUMENT
}
performDecryptionWithType(file, selectedType)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
lifecycleOwner.lifecycleScope.launch {
try {
@@ -536,7 +463,7 @@ class FileAdapter(
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
} catch (_: Exception) {
mainHandler.post {
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
}
@@ -545,39 +472,20 @@ class FileAdapter(
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_file, parent, false)
return FileViewHolder(view)
}
fun isInSelectionMode(): Boolean = isSelectionMode
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
if (position < itemCount) {
val file = getItem(position)
holder.bind(file)
}
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
fun onBackPressed(): Boolean {
return if (isSelectionMode) {
exitSelectionMode()
true
} else {
if (position < itemCount) {
val file = getItem(position)
holder.bind(file, payloads)
}
false
}
}
override fun submitList(list: List<File>?) {
val currentList = currentList.toMutableList()
if (list == null) {
currentList.clear()
super.submitList(null)
} else {
val newList = list.filter { it.name != ".nomedia" }.toMutableList()
super.submitList(newList)
}
private fun onSelectionCountChanged(count: Int) {
filesOperationCallback?.get()?.onSelectionCountChanged(count)
}
fun enterSelectionMode() {
@@ -591,92 +499,35 @@ class FileAdapter(
fun exitSelectionMode() {
if (isSelectionMode) {
isSelectionMode = false
selectedItems.forEach { position ->
notifyItemChanged(position)
}
selectedItems.clear()
notifySelectionModeChange()
notifyDataSetChanged()
}
}
fun clearSelection() {
if (selectedItems.isNotEmpty()) {
val previouslySelected = selectedItems.toSet()
selectedItems.clear()
fileOperationCallback?.get()?.onSelectionCountChanged(0)
previouslySelected.forEach { position ->
if (position < itemCount) {
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
}
}
}
fun selectAll() {
if (!isSelectionMode) {
enterSelectionMode()
}
val previouslySelected = selectedItems.toSet()
selectedItems.clear()
for (i in 0 until itemCount) {
selectedItems.add(i)
}
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
updateSelectionItems(selectedItems.toSet(), previouslySelected)
}
private fun updateSelectionItems(newSelections: Set<Int>, oldSelections: Set<Int>) {
val changedItems = (oldSelections - newSelections) + (newSelections - oldSelections)
changedItems.forEach { position ->
if (position < itemCount) {
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
}
}
private fun notifySelectionModeChange() {
fileOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
filesOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
onFolderLongClick(isSelectionMode)
}
fun getSelectedItems(): List<File> {
return selectedItems.mapNotNull { position ->
if (position < itemCount) getItem(position) else null
}
}
fun getSelectedCount(): Int = selectedItems.size
fun isInSelectionMode(): Boolean = isSelectionMode
fun onBackPressed(): Boolean {
return if (isSelectionMode) {
exitSelectionMode()
true
} else {
false
}
}
fun cleanup() {
try {
if (!fileExecutor.isShutdown) {
fileExecutor.shutdown()
}
} catch (e: Exception) {
} catch (_: Exception) {
}
fileOperationCallback?.clear()
fileOperationCallback = null
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
cleanup()
filesOperationCallback?.clear()
filesOperationCallback = null
}
private fun onSelectionCountChanged(count: Int) {
fileOperationCallback?.get()?.onSelectionCountChanged(count)
fun getSelectedItems(): List<File> {
return selectedItems.mapNotNull { position ->
if (position < itemCount) getItem(position) else null
}
}
fun encryptSelectedFiles() {
@@ -687,85 +538,16 @@ class FileAdapter(
.setTitle(context.getString(R.string.encrypt_files))
.setMessage(context.getString(R.string.encryption_disclaimer))
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
performEncryption(selectedFiles)
FileManager(context, lifecycleOwner).performEncryption(
selectedFiles
) {
updateItemsAfterEncryption(it)
}
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performEncryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) continue
val originalExtension = ".${file.extension.lowercase()}"
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
if (encryptedFile.exists()) {
if (hiddenFile == null){
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = encryptedFile.absolutePath,
isEncrypted = true,
encryptedFileName = encryptedFile.name,
fileType = fileType,
fileName = file.name,
originalExtension = originalExtension
)
)
}else{
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = hiddenFile.filePath,
newFilePath = encryptedFile.absolutePath,
encryptedFileName = encryptedFile.name,
isEncrypted = true
)
}
}
if (file.delete()) {
updatedFiles.add(encryptedFile)
successCount++
} else {
failCount++
}
} else {
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
}
fun decryptSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
@@ -774,73 +556,40 @@ class FileAdapter(
.setTitle(context.getString(R.string.decrypt_files))
.setMessage(context.getString(R.string.decryption_disclaimer))
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
performDecryption(selectedFiles)
FileManager(context, lifecycleOwner).performDecryption(selectedFiles) {
updateItemsAfterDecryption(it)
}
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performDecryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
fun updateItemsAfterEncryption(encryptedFiles: Map<File, File>) {
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted != true) continue
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
updatedFiles.add(decryptedFile)
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} catch (e: Exception) {
failCount++
}
}
val currentList = FolderManager().getFilesInFolder(currentFolder)
val updatedList = currentList.map { file ->
encryptedFiles[file] ?: file
}.toMutableList()
selectedItems.clear()
exitSelectionMode()
submitList(updatedList)
mainHandler.postDelayed({
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
filesOperationCallback?.get()?.onRefreshNeeded()
},20)
}
fun updateItemsAfterDecryption(decryptedFiles: Map<File, File>) {
val currentList = FolderManager().getFilesInFolder(currentFolder)
val updatedList = currentList.map { file ->
decryptedFiles[file] ?: file
}.toMutableList()
selectedItems.clear()
exitSelectionMode()
submitList(updatedList)
mainHandler.postDelayed({
filesOperationCallback?.get()?.onRefreshNeeded()
},20)
}
}

View File

@@ -37,6 +37,10 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
changes.add("EXISTENCE_CHANGED")
}
if (oldItem.absolutePath != newItem.absolutePath) {
changes.add("FILE_CHANGED")
}
return changes.ifEmpty { null }
}
}

View File

@@ -5,6 +5,7 @@ import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -71,109 +72,124 @@ class ImagePreviewAdapter(
private var currentPosition = 0
private var tempDecryptedFile: File? = null
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
fun bind(file: File, position: Int, decryptedFileType: FileManager.FileType) {
currentPosition = position
releaseMediaPlayer()
resetAudioUI()
cleanupTempFile()
try {
lifecycleOwner.lifecycleScope.launch {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile != null){
if (hiddenFile != null) {
val isEncrypted = hiddenFile.isEncrypted
val fileType = hiddenFile.fileType
if (isEncrypted) {
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
displayFile(tempDecryptedFile, fileType,true)
tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile!!.exists() && tempDecryptedFile!!.length() > 0) {
displayFile(tempDecryptedFile!!, fileType, true)
} else {
Log.e("ImagePreviewAdapter", "Failed to get decrypted preview file for: ${file.absolutePath}")
showEncryptedError()
}
} else {
displayFile(file, decryptedFileType,false)
displayFile(file, decryptedFileType, false)
}
}else{
displayFile(file, decryptedFileType,false)
} else {
displayFile(file, decryptedFileType, false)
}
}
} catch (_: Exception) {
displayFile(file, decryptedFileType,false)
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error in bind: ${e.message}")
displayFile(file, decryptedFileType, false)
}
}
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
val uri = getUriForPreviewFile(context, file)
when (fileType) {
FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE
private fun displayFile(file: File, fileType: FileManager.FileType, isEncrypted: Boolean = false) {
try {
val uri = if (isEncrypted) {
getUriForPreviewFile(context, file)
} else {
Uri.fromFile(file)
}
val videoUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.videoView.setVideoURI(videoUri)
binding.videoView.start()
if (uri == null) {
Log.e("ImagePreviewAdapter", "Failed to get URI for file: ${file.absolutePath}")
showEncryptedError()
return
}
val mediaController = MediaController(context)
mediaController.setAnchorView(binding.videoView)
binding.videoView.setMediaController(mediaController)
when (fileType) {
FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE
mediaController.setPrevNextListeners(
{
binding.videoView.setVideoURI(uri)
binding.videoView.start()
val mediaController = MediaController(context)
mediaController.setAnchorView(binding.videoView)
binding.videoView.setMediaController(mediaController)
mediaController.setPrevNextListeners(
{
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
},
{
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
playVideoAtPosition(prevPosition)
}
)
binding.videoView.setOnCompletionListener {
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
},
{
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
playVideoAtPosition(prevPosition)
}
)
}
FileManager.FileType.IMAGE -> {
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
Glide.with(context)
.load(uri)
.error(R.drawable.encrypted)
.into(binding.imageView)
}
FileManager.FileType.AUDIO -> {
val audioFile: File? = if (isEncrypted) {
getFileFromUri(context, uri)
} else {
file
}
if (audioFile == null) {
Log.e("ImagePreviewAdapter", "Failed to get audio file from URI")
showEncryptedError()
return
}
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name
binding.videoView.setOnCompletionListener {
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
setupAudioPlayer(audioFile)
setupPlaybackControls()
}
else -> {
binding.imageView.visibility = View.VISIBLE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.GONE
Glide.with(context)
.load(uri)
.error(R.drawable.encrypted)
.into(binding.imageView)
}
}
FileManager.FileType.IMAGE -> {
val imageUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
Glide.with(context)
.load(imageUri)
.into(binding.imageView)
}
FileManager.FileType.AUDIO -> {
val audioFile: File? = if (isEncrypted) {
getFileFromUri(context, uri!!)
} else {
file
}
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name
setupAudioPlayer(audioFile!!)
setupPlaybackControls()
}
else -> {
binding.imageView.visibility = View.VISIBLE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.GONE
}
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error displaying file: ${e.message}")
showEncryptedError()
}
}
@@ -199,11 +215,12 @@ class ImagePreviewAdapter(
}
private fun cleanupTempFile() {
tempDecryptedFile?.let { file ->
if (file.exists()) {
tempDecryptedFile?.let {
if (it.exists()) {
try {
file.delete()
} catch (_: Exception) {
it.delete()
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error cleaning up temp file: ${e.message}")
}
}
tempDecryptedFile = null

View File

@@ -1,5 +1,6 @@
package devs.org.calculator.utils
import android.Manifest
import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.ActivityNotFoundException
@@ -8,6 +9,8 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
@@ -15,29 +18,23 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.database.HiddenFileRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import android.Manifest
import androidx.core.content.FileProvider
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) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
val intent = Intent()
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
@@ -67,15 +64,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
return dir
}
fun getFilesInHiddenDir(type: FileType): List<File> {
val hiddenDir = getHiddenDirectory()
val typeDir = File(hiddenDir, type.dirName)
if (!typeDir.exists()) {
typeDir.mkdirs()
File(typeDir, ".nomedia").createNewFile()
}
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
}
fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List<File> {
val typeDir = File(folder)
if (!typeDir.exists()) {
@@ -117,8 +105,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
throw Exception("File copy failed")
}
// Encrypt file if encryption is enabled
// Media scan the new file to hide it
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(targetDir)
@@ -173,69 +159,13 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
}
}
fun unHideFile(file: File, onSuccess: (() -> Unit)? = null, onError: ((String) -> Unit)? = null) {
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
try {
// Create target directory (Downloads)
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
targetDir.mkdirs()
// Create target file with same name or timestamp
val targetFile = File(targetDir, file.name)
// If file with same name exists, add timestamp
val finalTargetFile = if (targetFile.exists()) {
val nameWithoutExt = file.nameWithoutExtension
val extension = file.extension
File(targetDir, "${nameWithoutExt}_${System.currentTimeMillis()}.${extension}")
} else {
targetFile
}
// Check if file is encrypted
if (file.extension == ENCRYPTED_EXTENSION) {
// Decrypt file
val decryptedFile = SecurityUtils.changeFileExtension(file, SecurityUtils.getFileExtension(file))
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
decryptedFile.copyTo(finalTargetFile, overwrite = false)
decryptedFile.delete()
} else {
throw Exception("Failed to decrypt file")
}
} else {
// Copy file content
file.copyTo(finalTargetFile, overwrite = false)
}
// Verify copy success
if (!finalTargetFile.exists() || finalTargetFile.length() == 0L) {
throw Exception("File copy failed")
}
// Media scan the new file
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
context.sendBroadcast(mediaScanIntent)
withContext(Dispatchers.Main) {
onSuccess?.invoke()
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
onError?.invoke(e.message ?: "Unknown error occurred")
}
}
}
}
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
withContext(Dispatchers.IO) {
try {
// First try to delete using DocumentFile
val documentFile = DocumentFile.fromSingleUri(context, photoUri)
if (documentFile?.exists() == true && documentFile.canWrite()) {
val deleted = documentFile.delete()
documentFile.delete()
withContext(Dispatchers.Main) {
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
}
@@ -400,5 +330,141 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
ALL("all")
}
fun performEncryption(selectedFiles: List<File>,onEncryptionEnded :(MutableMap<File, File>)-> Unit) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val encryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) continue
val originalExtension = ".${file.extension.lowercase()}"
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
val encryptedFile = SecurityUtils.changeFileExtension(file, devs.org.calculator.utils.FileManager.ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
if (encryptedFile.exists()) {
if (hiddenFile == null){
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = encryptedFile.absolutePath,
isEncrypted = true,
encryptedFileName = encryptedFile.name,
fileType = fileType,
fileName = file.name,
originalExtension = originalExtension
)
)
}else{
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = hiddenFile.filePath,
newFilePath = encryptedFile.absolutePath,
encryptedFileName = encryptedFile.name,
isEncrypted = true
)
}
}
if (file.delete()) {
encryptedFiles[file] = encryptedFile
successCount++
} else {
failCount++
}
} else {
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
Handler(Looper.getMainLooper()).post{
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Files encrypted successfully", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Some files could not be encrypted", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
else -> {
Toast.makeText(context, "Failed to encrypt files", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
}
}
}
}
fun performDecryption(selectedFiles: List<File>,onDecryptionEnded :(MutableMap<File, File>) -> Unit) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val decryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
}
} catch (e: Exception) {
failCount++
}
}
Handler(Looper.getMainLooper()).post{
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Files decrypted successfully", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Some files could not be decrypted", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
else -> {
Toast.makeText(context, "Failed to decrypt files", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
}
}
}
}
}

View File

@@ -17,11 +17,13 @@ import android.content.SharedPreferences
import androidx.core.content.FileProvider
import devs.org.calculator.database.HiddenFileEntity
import androidx.core.content.edit
import android.util.Log
object SecurityUtils {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val KEY_SIZE = 256
val ENCRYPTED_EXTENSION = ".enc"
private fun getSecretKey(context: Context): SecretKey {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
@@ -109,44 +111,59 @@ object SecurityUtils {
try {
val encryptedFile = File(meta.filePath)
if (!encryptedFile.exists()) {
Log.e("SecurityUtils", "Encrypted file does not exist: ${meta.filePath}")
return null
}
val tempDir = File(context.cacheDir, "preview_temp")
if (!tempDir.exists()) tempDir.mkdirs()
if (!tempDir.exists()) {
if (!tempDir.mkdirs()) {
Log.e("SecurityUtils", "Failed to create temp directory")
return null
}
}
// Clean up old preview files
tempDir.listFiles()?.forEach {
if (it.lastModified() < System.currentTimeMillis() - 5 * 60 * 1000) { // 5 minutes
it.delete()
}
}
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
tempDir.listFiles()?.forEach { it.delete() }
val success = decryptFile(context, encryptedFile, tempFile)
if (success && tempFile.exists() && tempFile.length() > 0) {
return tempFile
} else {
Log.e("SecurityUtils", "Failed to decrypt preview file: ${meta.filePath}")
if (tempFile.exists()) tempFile.delete()
return null
}
} catch (_: Exception) {
} catch (e: Exception) {
Log.e("SecurityUtils", "Error in getDecryptedPreviewFile: ${e.message}")
return null
}
}
fun getUriForPreviewFile(context: Context, file: File): Uri? {
return try {
if (!file.exists() || file.length() == 0L) {
Log.e("SecurityUtils", "Preview file does not exist or is empty: ${file.absolutePath}")
return null
}
FileProvider.getUriForFile(
context,
"${context.packageName}.provider", // Must match AndroidManifest
"${context.packageName}.provider",
file
)
} catch (_: Exception) {
} catch (e: Exception) {
Log.e("SecurityUtils", "Error getting URI for preview file: ${e.message}")
null
}
}
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try {
if (!inputFile.exists()) {

View File

@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
@@ -16,17 +17,12 @@ import androidx.core.net.toUri
class StoragePermissionUtil(private val activity: AppCompatActivity) {
private val requestPermissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (permissions.all { it.value }) {
onPermissionGranted?.invoke()
}
}
private var onPermissionGranted: (() -> Unit)? = null
fun requestStoragePermission(onGranted: () -> Unit) {
fun requestStoragePermission(
launcher: ActivityResultLauncher<Array<String>>,
onGranted: () -> Unit
) {
onPermissionGranted = onGranted
when {
@@ -45,7 +41,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
requestPermissionLauncher.launch(permissions)
launcher.launch(permissions)
}
}
}
@@ -66,4 +62,10 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
writePermission == PermissionChecker.PERMISSION_GRANTED
}
}
fun handlePermissionResult(permissions: Map<String, Boolean>) {
if (permissions.all { it.value }) {
onPermissionGranted?.invoke()
}
}
}

View File

@@ -5,74 +5,86 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".activities.SetupPasswordActivity">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/change_password"
android:textSize="24sp"
android:textStyle="bold"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilOldPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/enter_old_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/change_password"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etOldPassword"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilOldPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:singleLine="true"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="32dp"
android:hint="@string/enter_old_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilNewPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/enter_new_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etOldPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etNewPassword"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilNewPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:singleLine="true"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="16dp"
android:hint="@string/enter_new_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnChangePassword"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="32dp"
android:padding="12dp"
android:text="@string/change_password"
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etNewPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnResetPassword"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/forgot_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnChangePassword" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnChangePassword"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="32dp"
android:padding="12dp"
android:text="@string/change_password"
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnResetPassword"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/forgot_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnChangePassword" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -48,7 +48,7 @@
android:gravity="end|bottom"
android:autoSizeTextType="uniform"
android:padding="10dp"
android:text=""
android:text="0"
android:textSize="70sp"
tools:ignore="Suspicious0dp" />
</LinearLayout>

View File

@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:id="@+id/main"
android:layout_height="match_parent"
android:orientation="vertical">

View File

@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/main"
android:layout_height="match_parent"
tools:context=".activities.SettingsActivity">

View File

@@ -5,104 +5,116 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".activities.SetupPasswordActivity">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/setup_password"
android:textSize="24sp"
android:textStyle="bold"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:padding="18dp"
android:gravity="center_horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/enter_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/setup_password"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:singleLine="true"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="32dp"
android:hint="@string/enter_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilConfirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/confirm_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilPassword">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etConfirmPassword"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilConfirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:singleLine="true"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="16dp"
android:hint="@string/confirm_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilPassword">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSecurityQuestion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/security_question_for_password_reset"
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etConfirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="number"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSecurityQuestion"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSecurityQuestion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="16dp"
android:hint="@string/security_question_for_password_reset"
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSecurityAnswer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/security_answer"
app:layout_constraintTop_toBottomOf="@id/tilSecurityQuestion">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSecurityQuestion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSecurityAnswer"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSecurityAnswer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginTop="16dp"
android:hint="@string/security_answer"
app:layout_constraintTop_toBottomOf="@id/tilSecurityQuestion">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSavePassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:padding="12dp"
android:text="@string/save_password"
app:layout_constraintTop_toBottomOf="@id/tilSecurityAnswer" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSecurityAnswer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnResetPassword"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/forgot_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnSavePassword" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSavePassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:padding="12dp"
android:text="@string/save_password"
app:layout_constraintTop_toBottomOf="@id/tilSecurityAnswer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnResetPassword"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/forgot_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnSavePassword" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -45,20 +45,27 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_more"
android:visibility="gone"
style="@style/Widget.Material3.Button.IconButton"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/swipeLayout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolBar">
app:layout_constraintTop_toBottomOf="@+id/toolBar"
android:layout_height="0dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp">
</androidx.recyclerview.widget.RecyclerView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.recyclerview.widget.RecyclerView>
<LinearLayout
android:id="@+id/noItems"

View File

@@ -4,6 +4,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<androidx.cardview.widget.CardView

View File

@@ -35,8 +35,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/add_image" />
android:scaleType="centerCrop"/>
<View
android:id="@+id/shade"
android:layout_width="match_parent"

View File

@@ -1,5 +1,6 @@
<resources>
<string name="app_name">Calculator</string>
<string name="app_name" translatable="false">Calculator</string>
<string name="version" translatable="false">Version 1.4.1</string>
<string name="invalid_message">Invalid Value Entered</string>
<string name="add_image">Add Image</string>
<string name="add_audio">Add Audio</string>
@@ -121,7 +122,6 @@
<string name="app_settings">App Settings</string>
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
<string name="security_settings">Security Settings</string>
<string name="version">Version 1.4.0</string>
<string name="app_details">App Details</string>
<string name="setup_password">Setup Password</string>
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.9.3"
agp = "8.11.1"
documentfile = "1.1.0"
exp4j = "0.4.8"
glide = "4.16.0"
@@ -8,7 +8,7 @@ coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
appcompat = "1.7.1"
lottie = "6.2.0"
material = "1.12.0"
activity = "1.10.1"
@@ -17,6 +17,7 @@ materialColorUtilities = "1.3.0"
gridlayout = "1.1.0"
photoview = "2.3.0"
roomRuntime = "2.7.1"
swiperefreshlayout = "1.2.0-beta01"
viewpager = "1.1.0"
zoomage = "1.3.1"
@@ -26,6 +27,7 @@ androidx-documentfile = { module = "androidx.documentfile:documentfile", version
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-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }

View File

@@ -1,6 +1,6 @@
#Sun Nov 03 19:53:13 IST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 183 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 406 KiB

BIN
media/Screenshot_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

View File

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 469 KiB

View File

Before

Width:  |  Height:  |  Size: 557 KiB

After

Width:  |  Height:  |  Size: 557 KiB

BIN
media/Screenshot_9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
media/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

BIN
media/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
media/payment_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB