Compare commits
30 Commits
1.4.0
...
82982aa221
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82982aa221 | ||
|
|
6f151554f0 | ||
|
|
5ecd3ec7d1 | ||
|
|
946953d4eb | ||
|
|
18172f5191 | ||
|
|
603d96b672 | ||
|
|
78ea4596ea | ||
|
|
a70a569b57 | ||
|
|
df0db1a479 | ||
|
|
9cdc6eb1a4 | ||
|
|
913af83b90 | ||
|
|
7ceb599d9f | ||
|
|
dd1767e55d | ||
|
|
28ccdda1bf | ||
|
|
27a538f7c6 | ||
|
|
22c2a64450 | ||
|
|
3af3f81f3c | ||
|
|
f2e206f208 | ||
|
|
2de1b28afe | ||
|
|
ccf291d2e2 | ||
|
|
0968d5c19b | ||
|
|
d64904fbe7 | ||
|
|
2da2c944a3 | ||
|
|
13e1fca28f | ||
|
|
5c5e0e4be8 | ||
|
|
aad939463c | ||
|
|
e4e2983acd | ||
|
|
8702491f85 | ||
|
|
5263f89cd3 | ||
|
|
0787d6dd5b |
51
README.md
@@ -1,23 +1,31 @@
|
|||||||
|

|
||||||
|
|
||||||
<div align="center">
|
<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 📂
|
# 📂 Calculator Hide File App for Android 📂
|
||||||
|
|
||||||
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
|
<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>
|
||||||
|
|
||||||
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
|
<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>
|
||||||
|
|
||||||
<a href="LICENSE">
|
<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>
|
</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?
|
## 😍 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**. 🔒✅
|
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.
|
> - Hide images, videos, documents & other files securely.
|
||||||
> - Works like a **real calculator** with hidden storage mode.
|
> - Works like a **real calculator** with hidden storage mode.
|
||||||
> - No one will suspect it’s a file vault!
|
> - No one will suspect it’s a file vault!
|
||||||
|
> - **Encrypt** files with your **custom key** to secure your hidden files.
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
✅ **Dual Functionality** – A working **calculator** & a **file vault** in one app.
|
✅ **Dual Functionality** – A working **calculator** & a **file vault** in one app.
|
||||||
✅ **Secret Passcode** – Unlock hidden files by entering a secret code.
|
✅ **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.
|
✅ **Fast & Lightweight** – Smooth performance on all Android devices.
|
||||||
✅ **No Root Required** – Works without rooting your phone.
|
✅ **No Root Required** – Works without rooting your phone.
|
||||||
|
✅ **Security** – Encrypt & Decrypt files.
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🖼️ Screenshots
|
## 🖼️ Screenshots
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
|
<img src="media/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="media/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_3.jpg" alt="Calculator Hide File App - Passcode Protection" width="32%">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="app/src/main/assets/Screenshot_4.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="app/src/main/assets/Screenshot_5.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="app/src/main/assets/Screenshot_6.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>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="app/src/main/assets/Screenshot_7.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="app/src/main/assets/Screenshot_8.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="app/src/main/assets/Screenshot_9.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>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -106,7 +114,7 @@ git clone https://github.com/Binondi/Calculator-Hide-Files.git
|
|||||||
## 🛠️ Technologies Used
|
## 🛠️ Technologies Used
|
||||||
|
|
||||||
- **Programming Language**: Kotlin
|
- **Programming Language**: Kotlin
|
||||||
- **UI Framework**: XML (For UI)
|
- **UI Framework**: XML
|
||||||
- **File Storage**: Secure internal storage & MediaStore API
|
- **File Storage**: Secure internal storage & MediaStore API
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -125,7 +133,12 @@ If you find this app useful, please consider supporting the development. 🙏
|
|||||||
|
|
||||||
[](https://github.com/sponsors/Binondi)
|
[](https://github.com/sponsors/Binondi)
|
||||||
[](https://paypal.me/BinondiBorthakur56)
|
[](https://paypal.me/BinondiBorthakur56)
|
||||||
|
[](https://buymeacoffee.com/binondi)
|
||||||
|
|
||||||
|
- **UPI ID** 📱
|
||||||
|
``
|
||||||
|
binondi@naviaxis
|
||||||
|
``
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Contributing
|
## 🔧 Contributing
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "devs.org.calculator"
|
applicationId = "devs.org.calculator"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
|
//noinspection OldTargetApi
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 5
|
versionCode = 7
|
||||||
versionName = "1.4.0"
|
versionName = "1.4.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -81,9 +82,11 @@ dependencies {
|
|||||||
implementation(libs.androidx.viewpager)
|
implementation(libs.androidx.viewpager)
|
||||||
implementation(libs.zoomage)
|
implementation(libs.zoomage)
|
||||||
implementation(libs.lottie)
|
implementation(libs.lottie)
|
||||||
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
|
|
||||||
// Room dependencies
|
// Room dependencies
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
|
//noinspection KaptUsageInsteadOfKsp
|
||||||
kapt(libs.androidx.room.compiler)
|
kapt(libs.androidx.room.compiler)
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
"type": "SINGLE",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 4,
|
"versionCode": 6,
|
||||||
"versionName": "1.3",
|
"versionName": "1.4.1",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 237 KiB |
@@ -11,7 +11,10 @@ import android.view.WindowManager
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
@@ -47,7 +50,12 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
fileManager = FileManager(this, this)
|
fileManager = FileManager(this, this)
|
||||||
folderManager = FolderManager()
|
folderManager = FolderManager()
|
||||||
dialogUtil = DialogUtil(this)
|
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()
|
setupInitialUIState()
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import devs.org.calculator.utils.PrefsUtil
|
|||||||
import net.objecthunter.exp4j.ExpressionBuilder
|
import net.objecthunter.exp4j.ExpressionBuilder
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import 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 {
|
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -36,6 +40,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
private val dialogUtil = DialogUtil(this)
|
private val dialogUtil = DialogUtil(this)
|
||||||
private val fileManager = FileManager(this, this)
|
private val fileManager = FileManager(this, this)
|
||||||
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
|
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
|
||||||
|
private lateinit var storagePermissionUtil: StoragePermissionUtil
|
||||||
|
private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -43,15 +49,30 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
enableEdgeToEdge()
|
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 ->
|
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
handleActivityResult(result)
|
handleActivityResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sp.getBoolean("isFirst", true)){
|
if (sp.getBoolean("isFirst", true)){
|
||||||
binding.display.text = getString(R.string.enter_123456)
|
binding.display.text = getString(R.string.enter_123456)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Environment.isExternalStorageManager()) {
|
|
||||||
|
if (!storagePermissionUtil.hasStoragePermission()) {
|
||||||
dialogUtil.showMaterialDialog(
|
dialogUtil.showMaterialDialog(
|
||||||
getString(R.string.storage_permission),
|
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) +
|
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),
|
getString(R.string.later),
|
||||||
object : DialogUtil.DialogCallback {
|
object : DialogUtil.DialogCallback {
|
||||||
override fun onPositiveButtonClicked() {
|
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() {
|
override fun onNegativeButtonClicked() {
|
||||||
@@ -75,9 +99,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
getString(R.string.you_can_grant_permission_later_from_settings),
|
getString(R.string.you_can_grant_permission_later_from_settings),
|
||||||
Toast.LENGTH_LONG).show()
|
Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setupNumberButton(binding.btn0, "0")
|
setupNumberButton(binding.btn0, "0")
|
||||||
setupNumberButton(binding.btn00, "00")
|
setupNumberButton(binding.btn00, "00")
|
||||||
setupNumberButton(binding.btn1, "1")
|
setupNumberButton(binding.btn1, "1")
|
||||||
@@ -150,7 +176,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearDisplay() {
|
private fun clearDisplay() {
|
||||||
currentExpression = ""
|
currentExpression = "0"
|
||||||
binding.total.text = ""
|
binding.total.text = ""
|
||||||
lastWasOperator = false
|
lastWasOperator = false
|
||||||
lastWasPercent = false
|
lastWasPercent = false
|
||||||
@@ -335,13 +361,17 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentExpression.isEmpty() ||
|
if (currentExpression.isEmpty()) {
|
||||||
(isOperator(currentExpression.last().toString()) && currentExpression.last() != '%')) {
|
|
||||||
binding.total.text = ""
|
binding.total.text = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var processedExpression = currentExpression.replace("×", "*")
|
var processedExpression = currentExpression.replace("×", "*")
|
||||||
|
|
||||||
|
if (isOperator(processedExpression.last().toString())) {
|
||||||
|
processedExpression = processedExpression.substring(0, processedExpression.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
if (processedExpression.contains("%")) {
|
if (processedExpression.contains("%")) {
|
||||||
processedExpression = preprocessExpression(processedExpression)
|
processedExpression = preprocessExpression(processedExpression)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
@@ -42,7 +45,12 @@ class PreviewActivity : AppCompatActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityPreviewBinding.inflate(layoutInflater)
|
binding = ActivityPreviewBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
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)
|
fileManager = FileManager(this, this)
|
||||||
|
|
||||||
currentPosition = intent.getIntExtra("position", 0)
|
currentPosition = intent.getIntExtra("position", 0)
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -29,6 +32,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
prefs = PrefsUtil(this)
|
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)
|
DEV_GITHUB_URL = getString(R.string.github_profile)
|
||||||
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
|
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
|
||||||
setupUI()
|
setupUI()
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.color.DynamicColors
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
@@ -26,7 +29,12 @@ class SetupPasswordActivity : AppCompatActivity() {
|
|||||||
binding = ActivitySetupPasswordBinding.inflate(layoutInflater)
|
binding = ActivitySetupPasswordBinding.inflate(layoutInflater)
|
||||||
binding2 = ActivityChangePasswordBinding.inflate(layoutInflater)
|
binding2 = ActivityChangePasswordBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
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)
|
prefsUtil = PrefsUtil(this)
|
||||||
hasPassword = prefsUtil.hasPassword()
|
hasPassword = prefsUtil.hasPassword()
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
@@ -23,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
import devs.org.calculator.adapters.FileAdapter
|
|
||||||
import devs.org.calculator.adapters.FolderSelectionAdapter
|
import devs.org.calculator.adapters.FolderSelectionAdapter
|
||||||
import devs.org.calculator.callbacks.FileProcessCallback
|
import devs.org.calculator.callbacks.FileProcessCallback
|
||||||
import devs.org.calculator.database.HiddenFileEntity
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
@@ -41,6 +39,11 @@ import java.io.File
|
|||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.app.AlertDialog
|
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() {
|
class ViewFolderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -71,6 +74,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
binding = ActivityViewFolderBinding.inflate(layoutInflater)
|
binding = ActivityViewFolderBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
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()
|
setupAnimations()
|
||||||
initialize()
|
initialize()
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
@@ -142,16 +151,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
customDialog?.dismiss()
|
customDialog?.dismiss()
|
||||||
customDialog = null
|
customDialog = null
|
||||||
updateFilesToAdapter()
|
updateFilesToAdapter()
|
||||||
|
refreshCurrentFolder()
|
||||||
}, remainingTime)
|
}, remainingTime)
|
||||||
} else {
|
} else {
|
||||||
customDialog?.dismiss()
|
customDialog?.dismiss()
|
||||||
customDialog = null
|
customDialog = null
|
||||||
|
refreshCurrentFolder()
|
||||||
updateFilesToAdapter()
|
updateFilesToAdapter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilesToAdapter() {
|
private fun updateFilesToAdapter() {
|
||||||
openFolder(currentFolder!!)
|
val files = folderManager.getFilesInFolder(currentFolder!!)
|
||||||
|
fileAdapter?.submitList(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -199,6 +211,16 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
mainHandler.postDelayed({
|
mainHandler.postDelayed({
|
||||||
dismissCustomDialog()
|
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)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +245,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
).show()
|
).show()
|
||||||
dismissCustomDialog()
|
dismissCustomDialog()
|
||||||
}
|
}
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,9 +253,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
refreshCurrentFolder()
|
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()) {
|
if (!folder.exists()) {
|
||||||
folder.mkdirs()
|
folder.mkdirs()
|
||||||
File(folder, ".nomedia").createNewFile()
|
File(folder, ".nomedia").createNewFile()
|
||||||
@@ -246,11 +279,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
|
binding.swipeLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyState() {
|
private fun showEmptyState() {
|
||||||
binding.noItems.visibility = View.VISIBLE
|
binding.noItems.visibility = View.VISIBLE
|
||||||
binding.recyclerView.visibility = View.GONE
|
binding.swipeLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFileList(files: List<File>, folder: File) {
|
private fun showFileList(files: List<File>, folder: File) {
|
||||||
@@ -261,17 +295,20 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
onFolderLongClick = { isSelected ->
|
onFolderLongClick = { isSelected ->
|
||||||
handleFileSelectionModeChange(isSelected)
|
handleFileSelectionModeChange(isSelected)
|
||||||
}).apply {
|
}).apply {
|
||||||
setFileOperationCallback(object : FileAdapter.FileOperationCallback {
|
setFilesOperationCallback(object : FileAdapter.FilesOperationCallback {
|
||||||
override fun onFileDeleted(file: File) {
|
override fun onFileDeleted(file: File) {
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
updateFilesToAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFileRenamed(oldFile: File, newFile: File) {
|
override fun onFileRenamed(oldFile: File, newFile: File) {
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
updateFilesToAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefreshNeeded() {
|
override fun onRefreshNeeded() {
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
updateFilesToAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
|
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
|
||||||
@@ -287,7 +324,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerView.adapter = fileAdapter
|
binding.recyclerView.adapter = fileAdapter
|
||||||
binding.recyclerView.visibility = View.VISIBLE
|
binding.swipeLayout.visibility = View.VISIBLE
|
||||||
binding.noItems.visibility = View.GONE
|
binding.noItems.visibility = View.GONE
|
||||||
|
|
||||||
binding.menuButton.setOnClickListener {
|
binding.menuButton.setOnClickListener {
|
||||||
@@ -381,10 +418,10 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
getString(R.string.decrypt_file) -> {
|
getString(R.string.decrypt_file) -> {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val filesWithoutMetadata = selectedFiles.filter { file ->
|
val filesWithoutMetadata = selectedFiles.filter { file ->
|
||||||
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
||||||
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
|
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesWithoutMetadata.isNotEmpty()) {
|
if (filesWithoutMetadata.isNotEmpty()) {
|
||||||
showDecryptionTypeDialog(filesWithoutMetadata)
|
showDecryptionTypeDialog(filesWithoutMetadata)
|
||||||
} else {
|
} else {
|
||||||
@@ -455,21 +492,18 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var successCount = 0
|
var successCount = 0
|
||||||
var failCount = 0
|
var failCount = 0
|
||||||
|
val decryptedFiles = mutableMapOf<File, File>()
|
||||||
|
|
||||||
for (file in selectedFiles) {
|
for (file in selectedFiles) {
|
||||||
try {
|
try {
|
||||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
|
||||||
|
|
||||||
if (hiddenFile?.isEncrypted == true) {
|
if (hiddenFile?.isEncrypted == true) {
|
||||||
|
|
||||||
val originalExtension = hiddenFile.originalExtension
|
val originalExtension = hiddenFile.originalExtension
|
||||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||||
|
|
||||||
|
|
||||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
|
||||||
hiddenFile.let {
|
hiddenFile.let {
|
||||||
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||||
filePath = file.absolutePath,
|
filePath = file.absolutePath,
|
||||||
@@ -479,27 +513,23 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (file.delete()) {
|
if (file.delete()) {
|
||||||
|
decryptedFiles[file] = decryptedFile
|
||||||
successCount++
|
successCount++
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (decryptedFile.exists()) {
|
if (decryptedFile.exists()) {
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
}
|
}
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
|
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
|
||||||
|
|
||||||
val extension = when (fileType) {
|
val extension = when (fileType) {
|
||||||
FileManager.FileType.IMAGE -> ".jpg"
|
FileManager.FileType.IMAGE -> ".jpg"
|
||||||
FileManager.FileType.VIDEO -> ".mp4"
|
FileManager.FileType.VIDEO -> ".mp4"
|
||||||
@@ -509,11 +539,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||||
|
|
||||||
|
|
||||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
|
||||||
|
|
||||||
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||||
HiddenFileEntity(
|
HiddenFileEntity(
|
||||||
filePath = decryptedFile.absolutePath,
|
filePath = decryptedFile.absolutePath,
|
||||||
@@ -525,37 +552,32 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (file.delete()) {
|
if (file.delete()) {
|
||||||
|
decryptedFiles[file] = decryptedFile
|
||||||
successCount++
|
successCount++
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (decryptedFile.exists()) {
|
if (decryptedFile.exists()) {
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
}
|
}
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
failCount++
|
failCount++
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
when {
|
when {
|
||||||
successCount > 0 && failCount == 0 -> {
|
successCount > 0 && failCount == 0 -> {
|
||||||
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
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()
|
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)
|
val files = folderManager.getFilesInFolder(folder)
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
if (files.isNotEmpty()) {
|
if (files.isNotEmpty()) {
|
||||||
binding.recyclerView.visibility = View.VISIBLE
|
binding.swipeLayout.visibility = View.VISIBLE
|
||||||
binding.noItems.visibility = View.GONE
|
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 ->
|
fileAdapter?.let { adapter ->
|
||||||
if (adapter.isInSelectionMode()) {
|
if (adapter.isInSelectionMode()) {
|
||||||
showFileSelectionIcons()
|
showFileSelectionIcons()
|
||||||
@@ -642,8 +676,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
@@ -659,6 +692,9 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
binding.back.setOnClickListener {
|
binding.back.setOnClickListener {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
binding.swipeLayout.setOnRefreshListener {
|
||||||
|
openFolder(currentFolder!!)
|
||||||
|
}
|
||||||
|
|
||||||
binding.addImage.setOnClickListener { openFilePicker("image/*") }
|
binding.addImage.setOnClickListener { openFilePicker("image/*") }
|
||||||
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
|
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
|
||||||
@@ -748,6 +784,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private fun performFileUnhiding(selectedFiles: List<File>) {
|
private fun performFileUnhiding(selectedFiles: List<File>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var allUnhidden = true
|
var allUnhidden = true
|
||||||
|
val unhiddenFiles = mutableListOf<File>()
|
||||||
selectedFiles.forEach { file ->
|
selectedFiles.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
@@ -767,6 +804,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
file.delete()
|
file.delete()
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
|
unhiddenFiles.add(file)
|
||||||
} else {
|
} else {
|
||||||
decryptedFile.delete()
|
decryptedFile.delete()
|
||||||
allUnhidden = false
|
allUnhidden = false
|
||||||
@@ -794,6 +832,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||||
}
|
}
|
||||||
file.delete()
|
file.delete()
|
||||||
|
unhiddenFiles.add(file)
|
||||||
} else {
|
} else {
|
||||||
allUnhidden = false
|
allUnhidden = false
|
||||||
}
|
}
|
||||||
@@ -802,8 +841,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
allUnhidden = false
|
allUnhidden = false
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,8 +854,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,17 +863,19 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private fun performFileDeletion(selectedFiles: List<File>) {
|
private fun performFileDeletion(selectedFiles: List<File>) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var allDeleted = true
|
var allDeleted = true
|
||||||
|
val deletedFiles = mutableListOf<File>()
|
||||||
selectedFiles.forEach { file ->
|
selectedFiles.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
hiddenFile?.let {
|
hiddenFile?.let {
|
||||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||||
}
|
}
|
||||||
if (!file.delete()) {
|
if (file.delete()) {
|
||||||
|
deletedFiles.add(file)
|
||||||
|
} else {
|
||||||
allDeleted = false
|
allDeleted = false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|
||||||
allDeleted = false
|
allDeleted = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -847,8 +888,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,6 +903,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var allCopied = true
|
var allCopied = true
|
||||||
|
val copiedFiles = mutableListOf<File>()
|
||||||
selectedFiles.forEach { file ->
|
selectedFiles.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val newFile = File(destinationFolder, file.name)
|
val newFile = File(destinationFolder, file.name)
|
||||||
@@ -880,17 +922,18 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
copiedFiles.add(file)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
allCopied = false
|
allCopied = false
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
|
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
|
||||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -898,6 +941,7 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
var allMoved = true
|
var allMoved = true
|
||||||
|
val movedFiles = mutableListOf<File>()
|
||||||
selectedFiles.forEach { file ->
|
selectedFiles.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val newFile = File(destinationFolder, file.name)
|
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) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
allMoved = false
|
allMoved = false
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
|
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()
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
||||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||||
.filter { it != currentFolder }
|
.filter { it != currentFolder }
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import android.util.Log
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
@@ -26,7 +23,9 @@ import devs.org.calculator.activities.PreviewActivity
|
|||||||
import devs.org.calculator.database.AppDatabase
|
import devs.org.calculator.database.AppDatabase
|
||||||
import devs.org.calculator.database.HiddenFileEntity
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
import devs.org.calculator.database.HiddenFileRepository
|
import devs.org.calculator.database.HiddenFileRepository
|
||||||
|
import devs.org.calculator.databinding.ListItemFileBinding
|
||||||
import devs.org.calculator.utils.FileManager
|
import devs.org.calculator.utils.FileManager
|
||||||
|
import devs.org.calculator.utils.FolderManager
|
||||||
import devs.org.calculator.utils.SecurityUtils
|
import devs.org.calculator.utils.SecurityUtils
|
||||||
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||||
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||||
@@ -40,22 +39,18 @@ class FileAdapter(
|
|||||||
private val lifecycleOwner: LifecycleOwner,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val currentFolder: File,
|
private val currentFolder: File,
|
||||||
private val showFileName: Boolean,
|
private val showFileName: Boolean,
|
||||||
private val onFolderLongClick: (Boolean) -> Unit
|
private val onFolderLongClick: (Boolean) -> Unit,
|
||||||
) : ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback()) {
|
) : ListAdapter<File, FileAdapter.FilesViewHolder>(FileDiffCallback()) {
|
||||||
|
|
||||||
|
|
||||||
|
private var filesOperationCallback: WeakReference<FilesOperationCallback>? = null
|
||||||
private val selectedItems = mutableSetOf<Int>()
|
private val selectedItems = mutableSetOf<Int>()
|
||||||
private var isSelectionMode = false
|
private var isSelectionMode = false
|
||||||
|
|
||||||
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
|
|
||||||
|
|
||||||
private val fileExecutor = Executors.newSingleThreadExecutor()
|
private val fileExecutor = Executors.newSingleThreadExecutor()
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
|
||||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileOperationCallback {
|
interface FilesOperationCallback {
|
||||||
fun onFileDeleted(file: File)
|
fun onFileDeleted(file: File)
|
||||||
fun onFileRenamed(oldFile: File, newFile: File)
|
fun onFileRenamed(oldFile: File, newFile: File)
|
||||||
fun onRefreshNeeded()
|
fun onRefreshNeeded()
|
||||||
@@ -63,197 +58,77 @@ class FileAdapter(
|
|||||||
fun onSelectionCountChanged(selectedCount: Int)
|
fun onSelectionCountChanged(selectedCount: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFileOperationCallback(callback: FileOperationCallback?) {
|
fun setFilesOperationCallback(callback: FilesOperationCallback?) {
|
||||||
fileOperationCallback = callback?.let { WeakReference(it) }
|
filesOperationCallback = callback?.let { WeakReference(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
|
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
fun bind(file: File) {
|
||||||
|
val position = adapterPosition
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
|
val currentFileData =
|
||||||
else {
|
hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
FileManager(context, lifecycleOwner).getFileType(file)
|
val currentFileType = currentFileData?.fileType ?: FileManager(
|
||||||
}
|
context,
|
||||||
|
lifecycleOwner
|
||||||
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
|
).getFileType(file)
|
||||||
setupClickListeners(file, fileType)
|
|
||||||
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
|
|
||||||
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
|
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
|
||||||
|
|
||||||
val position = adapterPosition
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
val isSelected = selectedItems.contains(position)
|
val isSelected = selectedItems.contains(position)
|
||||||
updateSelectionUI(isSelected)
|
updateSelectionUI(isSelected)
|
||||||
}
|
}
|
||||||
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
|
binding.encrypted.visibility =
|
||||||
|
if (isCurrentFileEncrypted) View.VISIBLE else View.GONE
|
||||||
} catch (e: Exception) {
|
} 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) {
|
private fun openFile(file: File, fileType: FileManager.FileType) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Toast.makeText(context,
|
Toast.makeText(context,
|
||||||
@@ -272,7 +147,7 @@ class FileAdapter(
|
|||||||
showDecryptionTypeDialog(file)
|
showDecryptionTypeDialog(file)
|
||||||
} else {
|
} else {
|
||||||
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
||||||
|
|
||||||
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
||||||
if (tempFile.exists() && tempFile.length() > 0) {
|
if (tempFile.exists() && tempFile.length() > 0) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
@@ -305,7 +180,7 @@ class FileAdapter(
|
|||||||
} else {
|
} else {
|
||||||
openInPreview(fileType)
|
openInPreview(fileType)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -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) {
|
private fun openAudioFile(file: File) {
|
||||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||||
try {
|
try {
|
||||||
@@ -332,7 +240,7 @@ class FileAdapter(
|
|||||||
putExtra("position", adapterPosition)
|
putExtra("position", adapterPosition)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.no_audio_player_found),
|
context.getString(R.string.no_audio_player_found),
|
||||||
@@ -354,7 +262,7 @@ class FileAdapter(
|
|||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
@@ -364,84 +272,126 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInPreview(fileType: FileManager.FileType) {
|
private fun setupDisplay(
|
||||||
val fileTypeString = when (fileType) {
|
file: File,
|
||||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
type: FileManager.FileType,
|
||||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
isCurrentFileEncrypted: Boolean,
|
||||||
else -> "unknown"
|
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 {
|
itemView.setOnLongClickListener {
|
||||||
putExtra("type", fileTypeString)
|
val position = adapterPosition
|
||||||
putExtra("folder", currentFolder.toString())
|
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||||
putExtra("position", adapterPosition)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingInflatedId")
|
if (!isSelectionMode) {
|
||||||
private fun renameFile(file: File) {
|
enterSelectionMode()
|
||||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
toggleSelection(position)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,29 +409,6 @@ class FileAdapter(
|
|||||||
notifyItemChanged(position)
|
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) {
|
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -491,7 +418,7 @@ class FileAdapter(
|
|||||||
FileManager.FileType.AUDIO -> ".mp3"
|
FileManager.FileType.AUDIO -> ".mp3"
|
||||||
else -> ".txt"
|
else -> ".txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
@@ -536,7 +463,7 @@ class FileAdapter(
|
|||||||
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -545,39 +472,20 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
|
fun isInSelectionMode(): Boolean = isSelectionMode
|
||||||
val view = LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.list_item_file, parent, false)
|
|
||||||
return FileViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
|
fun onBackPressed(): Boolean {
|
||||||
if (position < itemCount) {
|
return if (isSelectionMode) {
|
||||||
val file = getItem(position)
|
exitSelectionMode()
|
||||||
holder.bind(file)
|
true
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int, payloads: MutableList<Any>) {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBindViewHolder(holder, position)
|
|
||||||
} else {
|
} else {
|
||||||
if (position < itemCount) {
|
false
|
||||||
val file = getItem(position)
|
|
||||||
holder.bind(file, payloads)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun submitList(list: List<File>?) {
|
|
||||||
val currentList = currentList.toMutableList()
|
private fun onSelectionCountChanged(count: Int) {
|
||||||
if (list == null) {
|
filesOperationCallback?.get()?.onSelectionCountChanged(count)
|
||||||
currentList.clear()
|
|
||||||
super.submitList(null)
|
|
||||||
} else {
|
|
||||||
val newList = list.filter { it.name != ".nomedia" }.toMutableList()
|
|
||||||
super.submitList(newList)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enterSelectionMode() {
|
fun enterSelectionMode() {
|
||||||
@@ -591,92 +499,35 @@ class FileAdapter(
|
|||||||
fun exitSelectionMode() {
|
fun exitSelectionMode() {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
isSelectionMode = false
|
isSelectionMode = false
|
||||||
|
selectedItems.forEach { position ->
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
selectedItems.clear()
|
selectedItems.clear()
|
||||||
notifySelectionModeChange()
|
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() {
|
private fun notifySelectionModeChange() {
|
||||||
fileOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
|
filesOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
|
||||||
onFolderLongClick(isSelectionMode)
|
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() {
|
fun cleanup() {
|
||||||
try {
|
try {
|
||||||
if (!fileExecutor.isShutdown) {
|
if (!fileExecutor.isShutdown) {
|
||||||
fileExecutor.shutdown()
|
fileExecutor.shutdown()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fileOperationCallback?.clear()
|
filesOperationCallback?.clear()
|
||||||
fileOperationCallback = null
|
filesOperationCallback = null
|
||||||
}
|
|
||||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
|
||||||
super.onDetachedFromRecyclerView(recyclerView)
|
|
||||||
cleanup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSelectionCountChanged(count: Int) {
|
fun getSelectedItems(): List<File> {
|
||||||
fileOperationCallback?.get()?.onSelectionCountChanged(count)
|
return selectedItems.mapNotNull { position ->
|
||||||
|
if (position < itemCount) getItem(position) else null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptSelectedFiles() {
|
fun encryptSelectedFiles() {
|
||||||
@@ -687,85 +538,16 @@ class FileAdapter(
|
|||||||
.setTitle(context.getString(R.string.encrypt_files))
|
.setTitle(context.getString(R.string.encrypt_files))
|
||||||
.setMessage(context.getString(R.string.encryption_disclaimer))
|
.setMessage(context.getString(R.string.encryption_disclaimer))
|
||||||
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
|
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
|
||||||
performEncryption(selectedFiles)
|
FileManager(context, lifecycleOwner).performEncryption(
|
||||||
|
selectedFiles
|
||||||
|
) {
|
||||||
|
updateItemsAfterEncryption(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||||
.show()
|
.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() {
|
fun decryptSelectedFiles() {
|
||||||
val selectedFiles = getSelectedItems()
|
val selectedFiles = getSelectedItems()
|
||||||
if (selectedFiles.isEmpty()) return
|
if (selectedFiles.isEmpty()) return
|
||||||
@@ -774,73 +556,40 @@ class FileAdapter(
|
|||||||
.setTitle(context.getString(R.string.decrypt_files))
|
.setTitle(context.getString(R.string.decrypt_files))
|
||||||
.setMessage(context.getString(R.string.decryption_disclaimer))
|
.setMessage(context.getString(R.string.decryption_disclaimer))
|
||||||
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
|
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
|
||||||
performDecryption(selectedFiles)
|
FileManager(context, lifecycleOwner).performDecryption(selectedFiles) {
|
||||||
|
updateItemsAfterDecryption(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performDecryption(selectedFiles: List<File>) {
|
fun updateItemsAfterEncryption(encryptedFiles: Map<File, File>) {
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
|
||||||
var successCount = 0
|
|
||||||
var failCount = 0
|
|
||||||
val updatedFiles = mutableListOf<File>()
|
|
||||||
|
|
||||||
for (file in selectedFiles) {
|
val currentList = FolderManager().getFilesInFolder(currentFolder)
|
||||||
try {
|
val updatedList = currentList.map { file ->
|
||||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
encryptedFiles[file] ?: file
|
||||||
if (hiddenFile?.isEncrypted != true) continue
|
}.toMutableList()
|
||||||
val originalExtension = hiddenFile.originalExtension
|
selectedItems.clear()
|
||||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
exitSelectionMode()
|
||||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
submitList(updatedList)
|
||||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
mainHandler.postDelayed({
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainHandler.post {
|
filesOperationCallback?.get()?.onRefreshNeeded()
|
||||||
exitSelectionMode()
|
},20)
|
||||||
when {
|
}
|
||||||
successCount > 0 && failCount == 0 -> {
|
|
||||||
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
fun updateItemsAfterDecryption(decryptedFiles: Map<File, File>) {
|
||||||
}
|
val currentList = FolderManager().getFilesInFolder(currentFolder)
|
||||||
successCount > 0 && failCount > 0 -> {
|
val updatedList = currentList.map { file ->
|
||||||
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
decryptedFiles[file] ?: file
|
||||||
}
|
}.toMutableList()
|
||||||
failCount > 0 -> {
|
selectedItems.clear()
|
||||||
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
exitSelectionMode()
|
||||||
}
|
submitList(updatedList)
|
||||||
}
|
mainHandler.postDelayed({
|
||||||
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
|
||||||
submitList(currentFiles)
|
filesOperationCallback?.get()?.onRefreshNeeded()
|
||||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
},20)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,10 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
|||||||
changes.add("EXISTENCE_CHANGED")
|
changes.add("EXISTENCE_CHANGED")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldItem.absolutePath != newItem.absolutePath) {
|
||||||
|
changes.add("FILE_CHANGED")
|
||||||
|
}
|
||||||
|
|
||||||
return changes.ifEmpty { null }
|
return changes.ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.media.MediaPlayer
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -71,109 +72,124 @@ class ImagePreviewAdapter(
|
|||||||
private var currentPosition = 0
|
private var currentPosition = 0
|
||||||
private var tempDecryptedFile: File? = null
|
private var tempDecryptedFile: File? = null
|
||||||
|
|
||||||
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
|
fun bind(file: File, position: Int, decryptedFileType: FileManager.FileType) {
|
||||||
currentPosition = position
|
currentPosition = position
|
||||||
|
|
||||||
releaseMediaPlayer()
|
releaseMediaPlayer()
|
||||||
resetAudioUI()
|
resetAudioUI()
|
||||||
cleanupTempFile()
|
cleanupTempFile()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
if (hiddenFile != null){
|
if (hiddenFile != null) {
|
||||||
val isEncrypted = hiddenFile.isEncrypted
|
val isEncrypted = hiddenFile.isEncrypted
|
||||||
val fileType = hiddenFile.fileType
|
val fileType = hiddenFile.fileType
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
||||||
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
|
if (tempDecryptedFile != null && tempDecryptedFile!!.exists() && tempDecryptedFile!!.length() > 0) {
|
||||||
displayFile(tempDecryptedFile, fileType,true)
|
displayFile(tempDecryptedFile!!, fileType, true)
|
||||||
} else {
|
} else {
|
||||||
|
Log.e("ImagePreviewAdapter", "Failed to get decrypted preview file for: ${file.absolutePath}")
|
||||||
showEncryptedError()
|
showEncryptedError()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
displayFile(file, decryptedFileType,false)
|
displayFile(file, decryptedFileType, false)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
displayFile(file, decryptedFileType,false)
|
displayFile(file, decryptedFileType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
} catch (_: Exception) {
|
Log.e("ImagePreviewAdapter", "Error in bind: ${e.message}")
|
||||||
|
displayFile(file, decryptedFileType, false)
|
||||||
displayFile(file, decryptedFileType,false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
|
private fun displayFile(file: File, fileType: FileManager.FileType, isEncrypted: Boolean = false) {
|
||||||
val uri = getUriForPreviewFile(context, file)
|
try {
|
||||||
when (fileType) {
|
val uri = if (isEncrypted) {
|
||||||
FileManager.FileType.VIDEO -> {
|
getUriForPreviewFile(context, file)
|
||||||
binding.imageView.visibility = View.GONE
|
} else {
|
||||||
binding.audioBg.visibility = View.GONE
|
Uri.fromFile(file)
|
||||||
binding.videoView.visibility = View.VISIBLE
|
}
|
||||||
|
|
||||||
val videoUri = if (isEncrypted){
|
if (uri == null) {
|
||||||
uri
|
Log.e("ImagePreviewAdapter", "Failed to get URI for file: ${file.absolutePath}")
|
||||||
}else{
|
showEncryptedError()
|
||||||
Uri.fromFile(file)
|
return
|
||||||
}
|
}
|
||||||
binding.videoView.setVideoURI(videoUri)
|
|
||||||
binding.videoView.start()
|
|
||||||
|
|
||||||
val mediaController = MediaController(context)
|
when (fileType) {
|
||||||
mediaController.setAnchorView(binding.videoView)
|
FileManager.FileType.VIDEO -> {
|
||||||
binding.videoView.setMediaController(mediaController)
|
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
|
val nextPosition = (adapterPosition + 1) % images.size
|
||||||
playVideoAtPosition(nextPosition)
|
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 {
|
setupAudioPlayer(audioFile)
|
||||||
val nextPosition = (adapterPosition + 1) % images.size
|
setupPlaybackControls()
|
||||||
playVideoAtPosition(nextPosition)
|
}
|
||||||
|
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 -> {
|
} catch (e: Exception) {
|
||||||
val imageUri = if (isEncrypted){
|
Log.e("ImagePreviewAdapter", "Error displaying file: ${e.message}")
|
||||||
uri
|
showEncryptedError()
|
||||||
}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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,11 +215,12 @@ class ImagePreviewAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanupTempFile() {
|
private fun cleanupTempFile() {
|
||||||
tempDecryptedFile?.let { file ->
|
tempDecryptedFile?.let {
|
||||||
if (file.exists()) {
|
if (it.exists()) {
|
||||||
try {
|
try {
|
||||||
file.delete()
|
it.delete()
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImagePreviewAdapter", "Error cleaning up temp file: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tempDecryptedFile = null
|
tempDecryptedFile = null
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package devs.org.calculator.utils
|
package devs.org.calculator.utils
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.RecoverableSecurityException
|
import android.app.RecoverableSecurityException
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@@ -8,6 +9,8 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
@@ -15,29 +18,23 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.IntentSenderRequest
|
import androidx.activity.result.IntentSenderRequest
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import devs.org.calculator.callbacks.FileProcessCallback
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
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) {
|
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
|
||||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
|
|
||||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||||
}
|
}
|
||||||
@@ -67,15 +64,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
return dir
|
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> {
|
fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List<File> {
|
||||||
val typeDir = File(folder)
|
val typeDir = File(folder)
|
||||||
if (!typeDir.exists()) {
|
if (!typeDir.exists()) {
|
||||||
@@ -117,8 +105,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
throw Exception("File copy failed")
|
throw Exception("File copy failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt file if encryption is enabled
|
|
||||||
|
|
||||||
// Media scan the new file to hide it
|
// Media scan the new file to hide it
|
||||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||||
mediaScanIntent.data = Uri.fromFile(targetDir)
|
mediaScanIntent.data = Uri.fromFile(targetDir)
|
||||||
@@ -172,62 +158,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -235,7 +165,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
// First try to delete using DocumentFile
|
// First try to delete using DocumentFile
|
||||||
val documentFile = DocumentFile.fromSingleUri(context, photoUri)
|
val documentFile = DocumentFile.fromSingleUri(context, photoUri)
|
||||||
if (documentFile?.exists() == true && documentFile.canWrite()) {
|
if (documentFile?.exists() == true && documentFile.canWrite()) {
|
||||||
val deleted = documentFile.delete()
|
documentFile.delete()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
|
// 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")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,13 @@ import android.content.SharedPreferences
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import devs.org.calculator.database.HiddenFileEntity
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
object SecurityUtils {
|
object SecurityUtils {
|
||||||
private const val ALGORITHM = "AES"
|
private const val ALGORITHM = "AES"
|
||||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||||
private const val KEY_SIZE = 256
|
private const val KEY_SIZE = 256
|
||||||
|
val ENCRYPTED_EXTENSION = ".enc"
|
||||||
|
|
||||||
private fun getSecretKey(context: Context): SecretKey {
|
private fun getSecretKey(context: Context): SecretKey {
|
||||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||||
@@ -109,44 +111,59 @@ object SecurityUtils {
|
|||||||
try {
|
try {
|
||||||
val encryptedFile = File(meta.filePath)
|
val encryptedFile = File(meta.filePath)
|
||||||
if (!encryptedFile.exists()) {
|
if (!encryptedFile.exists()) {
|
||||||
|
Log.e("SecurityUtils", "Encrypted file does not exist: ${meta.filePath}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val tempDir = File(context.cacheDir, "preview_temp")
|
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}")
|
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
|
||||||
|
|
||||||
tempDir.listFiles()?.forEach { it.delete() }
|
|
||||||
|
|
||||||
val success = decryptFile(context, encryptedFile, tempFile)
|
val success = decryptFile(context, encryptedFile, tempFile)
|
||||||
|
|
||||||
if (success && tempFile.exists() && tempFile.length() > 0) {
|
if (success && tempFile.exists() && tempFile.length() > 0) {
|
||||||
return tempFile
|
return tempFile
|
||||||
} else {
|
} else {
|
||||||
|
Log.e("SecurityUtils", "Failed to decrypt preview file: ${meta.filePath}")
|
||||||
if (tempFile.exists()) tempFile.delete()
|
if (tempFile.exists()) tempFile.delete()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("SecurityUtils", "Error in getDecryptedPreviewFile: ${e.message}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getUriForPreviewFile(context: Context, file: File): Uri? {
|
fun getUriForPreviewFile(context: Context, file: File): Uri? {
|
||||||
return try {
|
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(
|
FileProvider.getUriForFile(
|
||||||
context,
|
context,
|
||||||
"${context.packageName}.provider", // Must match AndroidManifest
|
"${context.packageName}.provider",
|
||||||
file
|
file
|
||||||
)
|
)
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("SecurityUtils", "Error getting URI for preview file: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||||
return try {
|
return try {
|
||||||
if (!inputFile.exists()) {
|
if (!inputFile.exists()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
@@ -15,20 +16,15 @@ import androidx.core.content.PermissionChecker
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
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
|
private var onPermissionGranted: (() -> Unit)? = null
|
||||||
|
|
||||||
fun requestStoragePermission(onGranted: () -> Unit) {
|
fun requestStoragePermission(
|
||||||
|
launcher: ActivityResultLauncher<Array<String>>,
|
||||||
|
onGranted: () -> Unit
|
||||||
|
) {
|
||||||
onPermissionGranted = onGranted
|
onPermissionGranted = onGranted
|
||||||
|
|
||||||
when {
|
when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
if (Environment.isExternalStorageManager()) {
|
if (Environment.isExternalStorageManager()) {
|
||||||
@@ -45,7 +41,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
|||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
Manifest.permission.WRITE_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
|
writePermission == PermissionChecker.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun handlePermissionResult(permissions: Map<String, Boolean>) {
|
||||||
|
if (permissions.all { it.value }) {
|
||||||
|
onPermissionGranted?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,74 +5,86 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".activities.SetupPasswordActivity">
|
tools:context=".activities.SetupPasswordActivity">
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<LinearLayout
|
||||||
android:id="@+id/tvTitle"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:gravity="center_horizontal"
|
||||||
android:text="@string/change_password"
|
android:orientation="vertical"
|
||||||
android:textSize="24sp"
|
android:padding="18dp"
|
||||||
android:textStyle="bold"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/tilOldPassword"
|
android:id="@+id/tvTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
android:text="@string/change_password"
|
||||||
android:hint="@string/enter_old_password"
|
android:textSize="24sp"
|
||||||
app:endIconMode="password_toggle"
|
android:textStyle="bold"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etOldPassword"
|
android:id="@+id/tilOldPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:imeOptions="actionNext"
|
android:layout_marginTop="32dp"
|
||||||
android:singleLine="true"
|
android:hint="@string/enter_old_password"
|
||||||
android:inputType="number" />
|
app:endIconMode="password_toggle"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/tilNewPassword"
|
android:id="@+id/etOldPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:imeOptions="actionNext"
|
||||||
android:hint="@string/enter_new_password"
|
android:inputType="number"
|
||||||
app:endIconMode="password_toggle"
|
android:singleLine="true" />
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etNewPassword"
|
android:id="@+id/tilNewPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:imeOptions="actionNext"
|
android:layout_marginTop="16dp"
|
||||||
android:singleLine="true"
|
android:hint="@string/enter_new_password"
|
||||||
android:inputType="number" />
|
app:endIconMode="password_toggle"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/btnChangePassword"
|
android:id="@+id/etNewPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="50dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
android:imeOptions="actionNext"
|
||||||
android:padding="12dp"
|
android:inputType="number"
|
||||||
android:text="@string/change_password"
|
android:singleLine="true" />
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnResetPassword"
|
android:id="@+id/btnChangePassword"
|
||||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="50dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginTop="32dp"
|
||||||
android:layout_marginTop="8dp"
|
android:padding="12dp"
|
||||||
android:text="@string/forgot_password"
|
android:text="@string/change_password"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/btnChangePassword" />
|
<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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
android:gravity="end|bottom"
|
android:gravity="end|bottom"
|
||||||
android:autoSizeTextType="uniform"
|
android:autoSizeTextType="uniform"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:text=""
|
android:text="0"
|
||||||
android:textSize="70sp"
|
android:textSize="70sp"
|
||||||
tools:ignore="Suspicious0dp" />
|
tools:ignore="Suspicious0dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/main"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/main"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".activities.SettingsActivity">
|
tools:context=".activities.SettingsActivity">
|
||||||
|
|
||||||
|
|||||||
@@ -5,104 +5,116 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".activities.SetupPasswordActivity">
|
tools:context=".activities.SetupPasswordActivity">
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<LinearLayout
|
||||||
android:id="@+id/tvTitle"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:orientation="vertical"
|
||||||
android:text="@string/setup_password"
|
android:padding="18dp"
|
||||||
android:textSize="24sp"
|
android:gravity="center_horizontal"
|
||||||
android:textStyle="bold"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="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
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/etPassword"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:imeOptions="actionNext"
|
android:layout_marginTop="32dp"
|
||||||
android:singleLine="true"
|
android:hint="@string/enter_password"
|
||||||
android:inputType="number" />
|
app:endIconMode="password_toggle"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/tilConfirmPassword"
|
android:id="@+id/etPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:imeOptions="actionNext"
|
||||||
android:hint="@string/confirm_password"
|
android:inputType="number"
|
||||||
app:endIconMode="password_toggle"
|
android:singleLine="true" />
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilPassword">
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etConfirmPassword"
|
android:id="@+id/tilConfirmPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:imeOptions="actionNext"
|
android:layout_marginTop="16dp"
|
||||||
android:singleLine="true"
|
android:hint="@string/confirm_password"
|
||||||
android:inputType="number" />
|
app:endIconMode="password_toggle"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
app:layout_constraintTop_toBottomOf="@id/tilPassword">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/tilSecurityQuestion"
|
android:id="@+id/etConfirmPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:imeOptions="actionNext"
|
||||||
android:hint="@string/security_question_for_password_reset"
|
android:inputType="number"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword">
|
android:singleLine="true" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etSecurityQuestion"
|
android:id="@+id/tilSecurityQuestion"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text" />
|
android:layout_marginTop="16dp"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
android:hint="@string/security_question_for_password_reset"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/tilSecurityAnswer"
|
android:id="@+id/etSecurityQuestion"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:inputType="text" />
|
||||||
android:hint="@string/security_answer"
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilSecurityQuestion">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/etSecurityAnswer"
|
android:id="@+id/tilSecurityAnswer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text" />
|
android:layout_marginTop="16dp"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
android:hint="@string/security_answer"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tilSecurityQuestion">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/btnSavePassword"
|
android:id="@+id/etSecurityAnswer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
android:inputType="text" />
|
||||||
android:padding="12dp"
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
android:text="@string/save_password"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/tilSecurityAnswer" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnResetPassword"
|
android:id="@+id/btnSavePassword"
|
||||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginTop="32dp"
|
||||||
android:layout_marginTop="8dp"
|
android:padding="12dp"
|
||||||
android:text="@string/forgot_password"
|
android:text="@string/save_password"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/tilSecurityAnswer" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/btnSavePassword" />
|
|
||||||
|
|
||||||
|
<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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -45,20 +45,27 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_more"
|
app:icon="@drawable/ic_more"
|
||||||
|
android:visibility="gone"
|
||||||
style="@style/Widget.Material3.Button.IconButton"/>
|
style="@style/Widget.Material3.Button.IconButton"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:id="@+id/swipeLayout"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="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
|
<LinearLayout
|
||||||
android:id="@+id/noItems"
|
android:id="@+id/noItems"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
|
|||||||
@@ -35,8 +35,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"/>
|
||||||
android:src="@drawable/add_image" />
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/shade"
|
android:id="@+id/shade"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<resources>
|
<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="invalid_message">Invalid Value Entered</string>
|
||||||
<string name="add_image">Add Image</string>
|
<string name="add_image">Add Image</string>
|
||||||
<string name="add_audio">Add Audio</string>
|
<string name="add_audio">Add Audio</string>
|
||||||
@@ -121,7 +122,6 @@
|
|||||||
<string name="app_settings">App Settings</string>
|
<string name="app_settings">App Settings</string>
|
||||||
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
|
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
|
||||||
<string name="security_settings">Security Settings</string>
|
<string name="security_settings">Security Settings</string>
|
||||||
<string name="version">Version 1.4.0</string>
|
|
||||||
<string name="app_details">App Details</string>
|
<string name="app_details">App Details</string>
|
||||||
<string name="setup_password">Setup Password</string>
|
<string name="setup_password">Setup Password</string>
|
||||||
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
|
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.3"
|
agp = "8.11.1"
|
||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
exp4j = "0.4.8"
|
exp4j = "0.4.8"
|
||||||
glide = "4.16.0"
|
glide = "4.16.0"
|
||||||
@@ -8,7 +8,7 @@ coreKtx = "1.16.0"
|
|||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.1"
|
||||||
lottie = "6.2.0"
|
lottie = "6.2.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
activity = "1.10.1"
|
activity = "1.10.1"
|
||||||
@@ -17,6 +17,7 @@ materialColorUtilities = "1.3.0"
|
|||||||
gridlayout = "1.1.0"
|
gridlayout = "1.1.0"
|
||||||
photoview = "2.3.0"
|
photoview = "2.3.0"
|
||||||
roomRuntime = "2.7.1"
|
roomRuntime = "2.7.1"
|
||||||
|
swiperefreshlayout = "1.2.0-beta01"
|
||||||
viewpager = "1.1.0"
|
viewpager = "1.1.0"
|
||||||
zoomage = "1.3.1"
|
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-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
||||||
androidx-room-ktx = { module = "androidx.room:room-ktx", 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-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" }
|
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
|
||||||
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
|
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
|
||||||
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Sun Nov 03 19:53:13 IST 2024
|
#Sun Nov 03 19:53:13 IST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
BIN
media/Screenshot_6.jpg
Normal file
|
After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 557 KiB After Width: | Height: | Size: 557 KiB |
BIN
media/Screenshot_9.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
media/banner.jpg
Normal file
|
After Width: | Height: | Size: 665 KiB |
BIN
media/banner.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
media/payment_qr.png
Normal file
|
After Width: | Height: | Size: 234 KiB |