26 Commits
1.3 ... 1.4.0

Author SHA1 Message Date
Binondi
cf192b6ab9 Added - file encryption, custom key creation. 2025-06-06 16:09:09 +05:30
Binondi
191368bdf8 Added - file encryption, custom key creation. 2025-06-06 15:16:21 +05:30
Binondi
5aafc7e411 Added - file encryption using room db. 2025-06-05 21:34:23 +05:30
Binondi
1f81cccd96 Added - file encryption using room db. 2025-06-05 21:27:28 +05:30
Binondi
eea0f56379 Added - file encryption using room db. 2025-06-05 21:22:26 +05:30
Binondi
e90e3c438f Added - relatable dialog text. 2025-06-04 12:22:43 +05:30
Binondi
480a90b64c Added - relatable dialog text. 2025-06-04 12:20:51 +05:30
Binondi
90a252f227 Added - relatable dialog text. 2025-06-04 12:12:40 +05:30
Binondi
08cdd747ca Added - relatable dialog text. 2025-06-04 12:10:33 +05:30
Binondi
2d10c2f941 Added - relatable dialog text. 2025-06-04 12:03:48 +05:30
Binondi
d840732305 Added - relatable dialog text. 2025-06-04 12:02:24 +05:30
Binondi
7da5885931 Fixed - icon color. 2025-06-04 11:51:01 +05:30
Binondi
082dbf17aa Added - padding in the document icon. 2025-06-04 11:50:29 +05:30
Binondi
093c419f34 Fixed - icon color 2025-06-04 11:49:25 +05:30
Binondi
8649ec9ac2 Merge remote-tracking branch 'origin/master' 2025-06-04 11:39:57 +05:30
Binondi
527f377ebd Fixed resource mipmap/ic_launcher_monochrome not found problem 2025-06-04 11:38:45 +05:30
Binondi Borthakur
e1627c426f Add files via upload 2025-06-03 20:44:28 +05:30
Binondi Borthakur
092219d487 Add files via upload 2025-06-03 20:43:34 +05:30
Binondi Borthakur
7803ed2589 Add files via upload 2025-06-03 20:40:43 +05:30
Binondi
e40b59c460 2025-06-03 20:35:46 +05:30
Binondi
c25ac613c2 Merge remote-tracking branch 'origin/master' 2025-06-03 20:31:13 +05:30
Binondi
bfc339c101 Changes For Folder Feature 2025-06-03 20:29:39 +05:30
Binondi Borthakur
657b674ff3 Update README.md 2025-06-03 16:38:25 +05:30
Binondi Borthakur
ea7c6ea18a Add files via upload 2025-06-03 16:32:56 +05:30
Binondi Borthakur
c97f38ae53 Add files via upload 2025-06-03 16:30:21 +05:30
Binondi Borthakur
0ba60f2bae Update README.md 2025-06-03 15:59:23 +05:30
59 changed files with 2481 additions and 1078 deletions

View File

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

2
.idea/kotlinc.xml generated
View File

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

View File

@@ -49,10 +49,21 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
## 🖼️ 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="24%"> <img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="24%"> <img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="32%">
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hide File App - Passcode Protection" width="24%"> <img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hidle File App - Passcode Protection" width="32%">
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="24%"> </div>
<div align="center">
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_5.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_6.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
</div>
<div align="center">
<img src="app/src/main/assets/Screenshot_7.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_8.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
<img src="app/src/main/assets/Screenshot_9.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
</div> </div>
--- ---
@@ -68,7 +79,9 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
3. **Manage Hidden Files** 3. **Manage Hidden Files**
- Add, remove, and restore hidden files. - Add, remove, and restore hidden files.
- Files stay protected even after closing the app. - Files stay protected even after closing the app.
- Create Hidden Folders.
- Move & Copy Hidden Files Between Hidden Folders.
--- ---
@@ -78,7 +91,8 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
### 🔹 Prerequisites ### 🔹 Prerequisites
- **Android 6.0 or higher** - **Android 6.0 or higher**
- **Storage permissions enabled** - **Storage permissions enabled**
- **Need Manage All Files Permissons For Android 11 and Higher Versions.**
### 🔹 Installation Steps ### 🔹 Installation Steps
```bash ```bash

View File

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

View File

@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "devs.org.calculator",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 4,
"versionName": "1.3",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}

View File

@@ -59,13 +59,17 @@
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="devs.org.calculator.fileprovider" android:authorities="devs.org.calculator.provider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<meta-data
android:name="android.app.icon"
android:resource="@mipmap/ic_launcher_monochrome" />
</application> </application>
</manifest> </manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View File

@@ -3,19 +3,14 @@ package devs.org.calculator
import android.app.Application import android.app.Application
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import devs.org.calculator.utils.PrefsUtil
class CalculatorApp : Application() { class CalculatorApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val prefs = PrefsUtil(this)
// Initialize theme settings
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
// Apply saved theme mode
val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
AppCompatDelegate.setDefaultNightMode(themeMode) AppCompatDelegate.setDefaultNightMode(themeMode)
// Apply dynamic colors only if dynamic theme is enabled
if (prefs.getBoolean("dynamic_theme", true)) { if (prefs.getBoolean("dynamic_theme", true)) {
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
} }

View File

@@ -5,7 +5,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.WindowManager import android.view.WindowManager
@@ -36,22 +35,20 @@ class HiddenActivity : AppCompatActivity() {
private var folderAdapter: FolderAdapter? = null private var folderAdapter: FolderAdapter? = null
private var listFolderAdapter: ListFolderAdapter? = null private var listFolderAdapter: ListFolderAdapter? = null
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR) private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
companion object {
private const val TAG = "HiddenActivity"
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHiddenBinding.inflate(layoutInflater) binding = ActivityHiddenBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
fileManager = FileManager(this, this) fileManager = FileManager(this, this)
folderManager = FolderManager(this) folderManager = FolderManager()
dialogUtil = DialogUtil(this) dialogUtil = DialogUtil(this)
setupInitialUIState() setupInitialUIState()
setupClickListeners() setupClickListeners()
setupBackPressedHandler() setupBackPressedHandler()
@@ -100,20 +97,17 @@ class HiddenActivity : AppCompatActivity() {
} }
binding.folderOrientation.setOnClickListener { binding.folderOrientation.setOnClickListener {
// Switch between grid mode and list mode
val currentIsList = PrefsUtil(this).getBoolean("isList", false) val currentIsList = PrefsUtil(this).getBoolean("isList", false)
val newIsList = !currentIsList val newIsList = !currentIsList
if (newIsList) { if (newIsList) {
// Switch to list view
showListUI() showListUI()
PrefsUtil(this).setBoolean("isList", true) PrefsUtil(this).setBoolean("isList", true)
binding.folderOrientation.setImageResource(R.drawable.ic_grid) binding.folderOrientation.setIconResource(R.drawable.ic_grid)
} else { } else {
// Switch to grid view
showGridUI() showGridUI()
PrefsUtil(this).setBoolean("isList", false) PrefsUtil(this).setBoolean("isList", false)
binding.folderOrientation.setImageResource(R.drawable.ic_list) binding.folderOrientation.setIconResource(R.drawable.ic_list)
} }
} }
} }
@@ -141,11 +135,10 @@ class HiddenActivity : AppCompatActivity() {
showEmptyState() showEmptyState()
} }
} else { } else {
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
showEmptyState() showEmptyState()
} }
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error listing folders: ${e.message}")
showEmptyState() showEmptyState()
} }
} }
@@ -164,26 +157,25 @@ class HiddenActivity : AppCompatActivity() {
val inputEditText = dialogView.findViewById<EditText>(R.id.editText) val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Enter Folder Name To Create") .setTitle(getString(R.string.enter_folder_name_to_create))
.setView(dialogView) .setView(dialogView)
.setPositiveButton("Create") { dialog, _ -> .setPositiveButton(getString(R.string.create)) { dialog, _ ->
val newName = inputEditText.text.toString().trim() val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty()) { if (newName.isNotEmpty()) {
try { try {
folderManager.createFolder(hiddenDir, newName) folderManager.createFolder(hiddenDir, newName)
refreshCurrentView() refreshCurrentView()
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error creating folder: ${e.message}")
Toast.makeText( Toast.makeText(
this@HiddenActivity, this@HiddenActivity,
"Failed to create folder", getString(R.string.failed_to_create_folder),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} }
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> .setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
.show() .show()
@@ -195,7 +187,6 @@ class HiddenActivity : AppCompatActivity() {
} }
private fun setupFlagSecure() { private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) { if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
@@ -220,11 +211,9 @@ class HiddenActivity : AppCompatActivity() {
showEmptyState() showEmptyState()
} }
} else { } else {
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
showEmptyState() showEmptyState()
} }
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error listing folders: ${e.message}")
showEmptyState() showEmptyState()
} }
} }
@@ -232,8 +221,6 @@ class HiddenActivity : AppCompatActivity() {
private fun showFolderList(folders: List<File>) { private fun showFolderList(folders: List<File>) {
binding.noItems.visibility = View.GONE binding.noItems.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE binding.recyclerView.visibility = View.VISIBLE
// Clear the existing adapter to avoid conflicts
listFolderAdapter = null listFolderAdapter = null
binding.recyclerView.layoutManager = GridLayoutManager(this, 2) binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
@@ -247,14 +234,13 @@ class HiddenActivity : AppCompatActivity() {
onSelectionModeChanged = { isSelectionMode -> onSelectionModeChanged = { isSelectionMode ->
handleFolderSelectionModeChange(isSelectionMode) handleFolderSelectionModeChange(isSelectionMode)
}, },
onSelectionCountChanged = { selectedCount -> onSelectionCountChanged = { _ ->
updateEditButtonVisibility() updateEditButtonVisibility()
} }
) )
binding.recyclerView.adapter = folderAdapter binding.recyclerView.adapter = folderAdapter
folderAdapter?.submitList(folders) folderAdapter?.submitList(folders)
// Ensure proper icon state for folder view
if (folderAdapter?.isInSelectionMode() != true) { if (folderAdapter?.isInSelectionMode() != true) {
showFolderViewIcons() showFolderViewIcons()
} }
@@ -262,8 +248,6 @@ class HiddenActivity : AppCompatActivity() {
private fun showFolderListStyle(folders: List<File>) { private fun showFolderListStyle(folders: List<File>) {
binding.noItems.visibility = View.GONE binding.noItems.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE binding.recyclerView.visibility = View.VISIBLE
// Clear the existing adapter to avoid conflicts
folderAdapter = null folderAdapter = null
binding.recyclerView.layoutManager = GridLayoutManager(this, 1) binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
@@ -277,14 +261,13 @@ class HiddenActivity : AppCompatActivity() {
onSelectionModeChanged = { isSelectionMode -> onSelectionModeChanged = { isSelectionMode ->
handleFolderSelectionModeChange(isSelectionMode) handleFolderSelectionModeChange(isSelectionMode)
}, },
onSelectionCountChanged = { selectedCount -> onSelectionCountChanged = { _ ->
updateEditButtonVisibility() updateEditButtonVisibility()
} }
) )
binding.recyclerView.adapter = listFolderAdapter binding.recyclerView.adapter = listFolderAdapter
listFolderAdapter?.submitList(folders) listFolderAdapter?.submitList(folders)
// Ensure proper icon state for folder view
if (listFolderAdapter?.isInSelectionMode() != true) { if (listFolderAdapter?.isInSelectionMode() != true) {
showFolderViewIcons() showFolderViewIcons()
} }
@@ -312,10 +295,10 @@ class HiddenActivity : AppCompatActivity() {
private fun refreshCurrentView() { private fun refreshCurrentView() {
val isList = PrefsUtil(this).getBoolean("isList", false) val isList = PrefsUtil(this).getBoolean("isList", false)
if (isList) { if (isList) {
binding.folderOrientation.setImageResource(R.drawable.ic_grid) binding.folderOrientation.setIconResource(R.drawable.ic_grid)
listFoldersInHiddenDirectoryListStyle() listFoldersInHiddenDirectoryListStyle()
} else { } else {
binding.folderOrientation.setImageResource(R.drawable.ic_list) binding.folderOrientation.setIconResource(R.drawable.ic_list)
listFoldersInHiddenDirectory() listFoldersInHiddenDirectory()
} }
} }
@@ -335,7 +318,7 @@ class HiddenActivity : AppCompatActivity() {
if (selectedFolders.isNotEmpty()) { if (selectedFolders.isNotEmpty()) {
dialogUtil.showMaterialDialog( dialogUtil.showMaterialDialog(
getString(R.string.delete_items), getString(R.string.delete_items),
getString(R.string.are_you_sure_you_want_to_delete_selected_items), "${getString(R.string.are_you_sure_you_want_to_delete_selected_items)}\n ${getString(R.string.folder_will_be_deleted_permanently)}" ,
getString(R.string.delete), getString(R.string.delete),
getString(R.string.cancel), getString(R.string.cancel),
object : DialogUtil.DialogCallback { object : DialogUtil.DialogCallback {
@@ -344,11 +327,11 @@ class HiddenActivity : AppCompatActivity() {
} }
override fun onNegativeButtonClicked() { override fun onNegativeButtonClicked() {
// Do nothing
} }
override fun onNaturalButtonClicked() { override fun onNaturalButtonClicked() {
// Do nothing
} }
} }
) )
@@ -360,7 +343,6 @@ class HiddenActivity : AppCompatActivity() {
selectedFolders.forEach { folder -> selectedFolders.forEach { folder ->
if (!folderManager.deleteFolder(folder)) { if (!folderManager.deleteFolder(folder)) {
allDeleted = false allDeleted = false
Log.e(TAG, "Failed to delete folder: ${folder.name}")
} }
} }
@@ -372,26 +354,20 @@ class HiddenActivity : AppCompatActivity() {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Clear selection from both adapters
folderAdapter?.clearSelection() folderAdapter?.clearSelection()
listFolderAdapter?.clearSelection() listFolderAdapter?.clearSelection()
// This will trigger the selection mode change callback and show proper icons
exitFolderSelectionMode() exitFolderSelectionMode()
// Refresh the current view based on orientation
refreshCurrentView() refreshCurrentView()
} }
private fun handleBackPress() { private fun handleBackPress() {
// Check if folder adapters are in selection mode
if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) { if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
return return
} }
// Handle navigation back
if (currentFolder != null) { if (currentFolder != null) {
navigateBackToFolders() navigateBackToFolders()
} else { } else {
@@ -402,25 +378,18 @@ class HiddenActivity : AppCompatActivity() {
private fun navigateBackToFolders() { private fun navigateBackToFolders() {
currentFolder = null currentFolder = null
// Clean up file adapter
refreshCurrentView() refreshCurrentView()
binding.folderName.text = getString(R.string.hidden_space) binding.folderName.text = getString(R.string.hidden_space)
// Set proper icons for folder view
showFolderViewIcons() showFolderViewIcons()
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// Remove any pending callbacks
mainHandler.removeCallbacksAndMessages(null) mainHandler.removeCallbacksAndMessages(null)
} }
//visibility related code
private fun showFolderViewIcons() { private fun showFolderViewIcons() {
binding.folderOrientation.visibility = View.VISIBLE binding.folderOrientation.visibility = View.VISIBLE
binding.settings.visibility = View.VISIBLE binding.settings.visibility = View.VISIBLE
@@ -429,7 +398,6 @@ class HiddenActivity : AppCompatActivity() {
binding.menuButton.visibility = View.GONE binding.menuButton.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE binding.addFolder.visibility = View.VISIBLE
binding.edit.visibility = View.GONE binding.edit.visibility = View.GONE
// Ensure FABs are properly managed
if (currentFolder == null) { if (currentFolder == null) {
binding.addFolder.visibility = View.VISIBLE binding.addFolder.visibility = View.VISIBLE
@@ -442,8 +410,6 @@ class HiddenActivity : AppCompatActivity() {
binding.deleteSelected.visibility = View.VISIBLE binding.deleteSelected.visibility = View.VISIBLE
binding.menuButton.visibility = View.GONE binding.menuButton.visibility = View.GONE
binding.addFolder.visibility = View.GONE binding.addFolder.visibility = View.GONE
// Update edit button visibility based on current selection count
updateEditButtonVisibility() updateEditButtonVisibility()
} }
private fun exitFolderSelectionMode() { private fun exitFolderSelectionMode() {
@@ -456,7 +422,6 @@ class HiddenActivity : AppCompatActivity() {
} else { } else {
enterFolderSelectionMode() enterFolderSelectionMode()
} }
// Always update edit button visibility when selection mode changes
updateEditButtonVisibility() updateEditButtonVisibility()
} }
@@ -468,7 +433,8 @@ class HiddenActivity : AppCompatActivity() {
} }
if (selectedFolders.size != 1) { if (selectedFolders.size != 1) {
Toast.makeText(this, "Please select exactly one folder to edit", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.please_select_exactly_one_folder_to_edit), Toast.LENGTH_SHORT).show()
return return
} }
@@ -483,20 +449,21 @@ class HiddenActivity : AppCompatActivity() {
inputEditText.selectAll() inputEditText.selectAll()
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Rename Folder") .setTitle(getString(R.string.rename_folder))
.setView(dialogView) .setView(dialogView)
.setPositiveButton("Rename") { dialog, _ -> .setPositiveButton(getString(R.string.rename)) { dialog, _ ->
val newName = inputEditText.text.toString().trim() val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != folder.name) { if (newName.isNotEmpty() && newName != folder.name) {
if (isValidFolderName(newName)) { if (isValidFolderName(newName)) {
renameFolder(folder, newName) renameFolder(folder, newName)
} else { } else {
Toast.makeText(this, "Invalid folder name", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.invalid_folder_name), Toast.LENGTH_SHORT).show()
} }
} }
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> .setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
.show() .show()
@@ -515,21 +482,19 @@ class HiddenActivity : AppCompatActivity() {
if (parentDir != null) { if (parentDir != null) {
val newFolder = File(parentDir, newName) val newFolder = File(parentDir, newName)
if (newFolder.exists()) { if (newFolder.exists()) {
Toast.makeText(this, "Folder with this name already exists", Toast.LENGTH_SHORT).show() Toast.makeText(this,
getString(R.string.folder_with_this_name_already_exists), Toast.LENGTH_SHORT).show()
return return
} }
if (oldFolder.renameTo(newFolder)) { if (oldFolder.renameTo(newFolder)) {
// Clear selection from both adapters
folderAdapter?.clearSelection() folderAdapter?.clearSelection()
listFolderAdapter?.clearSelection() listFolderAdapter?.clearSelection()
// Exit selection mode
exitFolderSelectionMode() exitFolderSelectionMode()
refreshCurrentView() refreshCurrentView()
} else { } else {
Toast.makeText(this, "Failed to rename folder", Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.failed_to_rename_folder), Toast.LENGTH_SHORT).show()
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -22,6 +23,7 @@ import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil 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
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback { class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -40,7 +42,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
enableEdgeToEdge()
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(result) handleActivityResult(result)
} }
@@ -49,15 +51,14 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
binding.display.text = getString(R.string.enter_123456) binding.display.text = getString(R.string.enter_123456)
} }
// Ask permission
if(!Environment.isExternalStorageManager()) { if(!Environment.isExternalStorageManager()) {
dialogUtil.showMaterialDialog( dialogUtil.showMaterialDialog(
"Storage Permission", getString(R.string.storage_permission),
"To ensure the app works properly and allows you to easily hide or un-hide your private files, please grant storage access permission.\n" + 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) +
"\n" + "\n" +
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.", getString(R.string.for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission),
"Grant", getString(R.string.grant_permission),
"Later", getString(R.string.later),
object : DialogUtil.DialogCallback { object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
fileManager.askPermission(this@MainActivity) fileManager.askPermission(this@MainActivity)
@@ -65,19 +66,20 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
override fun onNegativeButtonClicked() { override fun onNegativeButtonClicked() {
Toast.makeText(this@MainActivity, Toast.makeText(this@MainActivity,
"Storage permission is required for the app to function properly", getString(R.string.storage_permission_is_required_for_the_app_to_function_properly),
Toast.LENGTH_LONG).show() Toast.LENGTH_LONG).show()
} }
override fun onNaturalButtonClicked() { override fun onNaturalButtonClicked() {
Toast.makeText(this@MainActivity, Toast.makeText(this@MainActivity,
"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.btn1, "1") setupNumberButton(binding.btn1, "1")
setupNumberButton(binding.btn2, "2") setupNumberButton(binding.btn2, "2")
setupNumberButton(binding.btn3, "3") setupNumberButton(binding.btn3, "3")
@@ -99,6 +101,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
binding.cut.setOnClickListener { cutNumbers() } binding.cut.setOnClickListener { cutNumbers() }
} }
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) { private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
if (result.resultCode == RESULT_OK) { if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
@@ -107,10 +110,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
contentResolver.takePersistableUriPermission(uri, takeFlags) contentResolver.takePersistableUriPermission(uri, takeFlags)
val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE) val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE)
preferences.edit().putString("filestorageuri", uri.toString()).apply() preferences.edit { putString("filestorageuri", uri.toString()) }
} }
} else {
Log.e("FileUtility", "Error occurred or operation cancelled: $result")
} }
} }
@@ -149,7 +150,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
} }
private fun clearDisplay() { private fun clearDisplay() {
currentExpression = "0" currentExpression = ""
binding.total.text = "" binding.total.text = ""
lastWasOperator = false lastWasOperator = false
lastWasPercent = false lastWasPercent = false
@@ -173,41 +174,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
} }
} }
private fun calculatePercentage() {
try {
val value = currentExpression.toDouble()
currentExpression = (value / 100).toString()
updateDisplay()
} catch (e: Exception) {
binding.display.text = getString(R.string.invalid_message)
}
}
private fun preprocessExpression(expression: String): String { private fun preprocessExpression(expression: String): String {
val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%") val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%")
val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%") val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%")
var processedExpression = expression var processedExpression = expression
// Replace standalone percentages (like "50%") with their decimal form (0.5)
val matcher = percentagePattern.matcher(processedExpression) val matcher = percentagePattern.matcher(processedExpression)
while (matcher.find()) { while (matcher.find()) {
val fullMatch = matcher.group(0) val fullMatch = matcher.group(0)
val number = matcher.group(1) val number = matcher.group(1)
// Check if it's a standalone percentage or part of an operation
val start = matcher.start() val start = matcher.start()
if (start == 0 || !isOperator(processedExpression[start-1].toString())) { if (start == 0 || !isOperator(processedExpression[start-1].toString())) {
val percentageValue = number.toDouble() / 100 val percentageValue = number!!.toDouble() / 100
processedExpression = processedExpression.replace(fullMatch, percentageValue.toString()) processedExpression = processedExpression.replace(fullMatch!!.toString(), percentageValue.toString())
} }
} }
// Handle operator-percentage combinations (like "100-20%")
val opMatcher = operatorPercentPattern.matcher(processedExpression) val opMatcher = operatorPercentPattern.matcher(processedExpression)
val sb = StringBuilder(processedExpression) val sb = StringBuilder(processedExpression)
// We need to process matches from right to left to maintain indices
val matches = mutableListOf<Triple<Int, Int, String>>() val matches = mutableListOf<Triple<Int, Int, String>>()
while (opMatcher.find()) { while (opMatcher.find()) {
@@ -219,15 +203,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
matches.add(Triple(start, end, "$operator$percentValue%")) matches.add(Triple(start, end, "$operator$percentValue%"))
} }
// Process matches from right to left
for (match in matches.reversed()) { for (match in matches.reversed()) {
val (start, end, fullMatch) = match val (start, end, fullMatch) = match
// Find the number before this operator
var leftNumberEnd = start
var leftNumberStart = start - 1 var leftNumberStart = start - 1
// Skip parentheses and move to the actual number
if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') { if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') {
var openParens = 1 var openParens = 1
leftNumberStart-- leftNumberStart--
@@ -238,7 +218,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
leftNumberStart-- leftNumberStart--
} }
// Now we need to find the start of the expression
if (leftNumberStart >= 0) { if (leftNumberStart >= 0) {
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) { while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) {
leftNumberStart-- leftNumberStart--
@@ -248,26 +227,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
leftNumberStart = 0 leftNumberStart = 0
} }
} else { } else {
// For simple numbers, just find the start of the number
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) { while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) {
leftNumberStart-- leftNumberStart--
} }
leftNumberStart++ leftNumberStart++
} }
if (leftNumberStart < leftNumberEnd) { if (leftNumberStart < start) {
val leftPart = sb.substring(leftNumberStart, leftNumberEnd) val leftPart = sb.substring(leftNumberStart, start)
try { try {
// Extract the numerical values
val baseNumber = evaluateExpression(leftPart) val baseNumber = evaluateExpression(leftPart)
val operator = fullMatch.substring(0, 1) val operator = fullMatch.substring(0, 1)
val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble() val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble()
// Calculate the percentage of the base number
val percentValue = baseNumber * (percentNumber / 100) val percentValue = baseNumber * (percentNumber / 100)
// Calculate the new value based on the operator
val newValue = when (operator) { val newValue = when (operator) {
"+" -> baseNumber + percentValue "+" -> baseNumber + percentValue
"-" -> baseNumber - percentValue "-" -> baseNumber - percentValue
@@ -276,7 +253,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
else -> baseNumber else -> baseNumber
} }
// Replace the entire expression "number operator percent%" with the result
sb.replace(leftNumberStart, end, newValue.toString()) sb.replace(leftNumberStart, end, newValue.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Calculator", "Error processing percentage expression: $e") Log.e("Calculator", "Error processing percentage expression: $e")
@@ -298,15 +274,16 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
private fun evaluateExpression(expression: String): Double { private fun evaluateExpression(expression: String): Double {
return try { return try {
ExpressionBuilder(expression).build().evaluate() ExpressionBuilder(expression).build().evaluate()
} catch (e: Exception) { } catch (_: Exception) {
expression.toDouble() expression.toDouble()
} }
} }
@SuppressLint("DefaultLocale")
private fun calculateResult() { private fun calculateResult() {
if (currentExpression == "123456") { if (currentExpression == "123456") {
val intent = Intent(this, SetupPasswordActivity::class.java) val intent = Intent(this, SetupPasswordActivity::class.java)
sp.edit().putBoolean("isFirst", false).apply() sp.edit { putBoolean("isFirst", false) }
intent.putExtra("password", currentExpression) intent.putExtra("password", currentExpression)
startActivity(intent) startActivity(intent)
clearDisplay() clearDisplay()
@@ -363,8 +340,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
binding.total.text = "" binding.total.text = ""
return return
} }
// Process the expression for preview calculation
var processedExpression = currentExpression.replace("×", "*") var processedExpression = currentExpression.replace("×", "*")
if (processedExpression.contains("%")) { if (processedExpression.contains("%")) {
@@ -382,7 +357,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){ if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
binding.total.text = getString(R.string.now_enter_button) binding.total.text = getString(R.string.now_enter_button)
}else binding.total.text = formattedResult }else binding.total.text = formattedResult
} catch (e: Exception) { } catch (_: Exception) {
binding.total.text = "" binding.total.text = ""
} }
} }
@@ -395,7 +370,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
val lastChar = currentExpression.last() val lastChar = currentExpression.last()
currentExpression = currentExpression.substring(0, currentExpression.length - 1) currentExpression = currentExpression.substring(0, currentExpression.length - 1)
// Update flags based on what was removed
if (lastChar == '%') { if (lastChar == '%') {
lastWasPercent = false lastWasPercent = false
} else if (isOperator(lastChar.toString())) { } else if (isOperator(lastChar.toString())) {
@@ -411,27 +385,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
} }
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
// Handle positive button click for both DialogUtil and DialogActionsCallback
fileManager.askPermission(this) fileManager.askPermission(this)
} }
override fun onNegativeButtonClicked() { override fun onNegativeButtonClicked() {
// Handle negative button click Toast.makeText(this, getString(R.string.storage_permission_is_required_for_the_app_to_function_properly), Toast.LENGTH_LONG).show()
Toast.makeText(this, "Storage permission is required for the app to function properly", Toast.LENGTH_LONG).show()
} }
override fun onNaturalButtonClicked() { override fun onNaturalButtonClicked() {
// Handle neutral button click Toast.makeText(this, getString(R.string.you_can_grant_permission_later_from_settings), Toast.LENGTH_LONG).show()
Toast.makeText(this, "You can grant permission later from Settings", Toast.LENGTH_LONG).show()
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 6767) { if (requestCode == 6767) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.permission_granted), Toast.LENGTH_SHORT).show()
} else { } else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_SHORT).show()
} }
} }
} }

View File

@@ -2,15 +2,22 @@ package devs.org.calculator.activities
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
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
import devs.org.calculator.adapters.ImagePreviewAdapter import devs.org.calculator.adapters.ImagePreviewAdapter
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ActivityPreviewBinding import devs.org.calculator.databinding.ActivityPreviewBinding
import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@@ -18,43 +25,35 @@ class PreviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityPreviewBinding private lateinit var binding: ActivityPreviewBinding
private var currentPosition: Int = 0 private var currentPosition: Int = 0
private lateinit var files: List<File> private var files: MutableList<File> = mutableListOf()
private lateinit var type: String private lateinit var type: String
private lateinit var folder: String private lateinit var folder: String
private lateinit var filetype: FileManager.FileType private lateinit var filetype: FileManager.FileType
private lateinit var adapter: ImagePreviewAdapter private lateinit var adapter: ImagePreviewAdapter
private lateinit var fileManager: FileManager private lateinit var fileManager: FileManager
private val dialogUtil = DialogUtil(this) private val dialogUtil = DialogUtil(this)
private val mainHandler = Handler(Looper.getMainLooper())
private val prefs: PrefsUtil by lazy { PrefsUtil(this) }
private val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityPreviewBinding.inflate(layoutInflater) binding = ActivityPreviewBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
fileManager = FileManager(this, this) fileManager = FileManager(this, this)
currentPosition = intent.getIntExtra("position", 0) currentPosition = intent.getIntExtra("position", 0)
type = intent.getStringExtra("type").toString() type = intent.getStringExtra("type") ?: "IMAGE"
folder = intent.getStringExtra("folder").toString() folder = intent.getStringExtra("folder") ?: ""
setupFileType() setupFileType()
files = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder) loadFiles()
setupImagePreview() setupImagePreview()
clickListeners() setupClickListeners()
setupPageChangeCallback()
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
}
})
binding.back.setOnClickListener {
finish()
}
} }
override fun onResume() { override fun onResume() {
@@ -62,8 +61,17 @@ class PreviewActivity : AppCompatActivity() {
setupFlagSecure() setupFlagSecure()
} }
override fun onPause() {
super.onPause()
adapter.releaseAllResources()
}
override fun onDestroy() {
super.onDestroy()
adapter.releaseAllResources()
}
private fun setupFlagSecure() { private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) { if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
@@ -78,14 +86,17 @@ class PreviewActivity : AppCompatActivity() {
filetype = FileManager.FileType.IMAGE filetype = FileManager.FileType.IMAGE
binding.title.text = getString(R.string.preview_images) binding.title.text = getString(R.string.preview_images)
} }
"VIDEO" -> { "VIDEO" -> {
filetype = FileManager.FileType.VIDEO filetype = FileManager.FileType.VIDEO
binding.title.text = getString(R.string.preview_videos) binding.title.text = getString(R.string.preview_videos)
} }
"AUDIO" -> { "AUDIO" -> {
filetype = FileManager.FileType.AUDIO filetype = FileManager.FileType.AUDIO
binding.title.text = getString(R.string.preview_audios) binding.title.text = getString(R.string.preview_audios)
} }
else -> { else -> {
filetype = FileManager.FileType.DOCUMENT filetype = FileManager.FileType.DOCUMENT
binding.title.text = getString(R.string.preview_documents) binding.title.text = getString(R.string.preview_documents)
@@ -93,108 +104,243 @@ class PreviewActivity : AppCompatActivity() {
} }
} }
private fun loadFiles() {
try {
val filesList = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
files = filesList.toMutableList()
if (currentPosition >= files.size) {
currentPosition = 0
}
} catch (e: Exception) {
e.printStackTrace()
files = mutableListOf()
}
}
private fun setupImagePreview() { private fun setupImagePreview() {
if (files.isEmpty()) {
finish()
return
}
adapter = ImagePreviewAdapter(this, this) adapter = ImagePreviewAdapter(this, this)
adapter.images = files adapter.images = files
binding.viewPager.adapter = adapter binding.viewPager.adapter = adapter
if (currentPosition < files.size) {
binding.viewPager.setCurrentItem(currentPosition, false) binding.viewPager.setCurrentItem(currentPosition, false)
val fileUri = Uri.fromFile(files[currentPosition])
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri).toString()
}
override fun onPause() {
super.onPause()
if (filetype == FileManager.FileType.AUDIO) {
(binding.viewPager.adapter as? ImagePreviewAdapter)?.currentMediaPlayer?.pause()
} }
updateFileInfo()
} }
override fun onDestroy() { private fun setupPageChangeCallback() {
super.onDestroy() binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
if (filetype == FileManager.FileType.AUDIO) { override fun onPageSelected(position: Int) {
(binding.viewPager.adapter as? ImagePreviewAdapter)?.let { adapter -> super.onPageSelected(position)
adapter.currentMediaPlayer?.release() currentPosition = position
adapter.currentMediaPlayer = null updateFileInfo()
} }
})
}
private fun updateFileInfo() {
if (files.isNotEmpty() && currentPosition < files.size) {
val fileUri = Uri.fromFile(files[currentPosition])
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri) ?: "Unknown"
//For Now File Name not Needed, i am keeping it for later use
} }
} }
private fun setupClickListeners() {
binding.back.setOnClickListener {
finish()
}
private fun clickListeners() {
binding.delete.setOnClickListener { binding.delete.setOnClickListener {
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem]) handleDeleteFile()
if (fileUri != null) {
dialogUtil.showMaterialDialog(
getString(R.string.delete_file),
getString(R.string.are_you_sure_to_delete_this_file_permanently),
getString(R.string.delete_permanently),
getString(R.string.cancel),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
lifecycleScope.launch {
FileManager(this@PreviewActivity, this@PreviewActivity).deletePhotoFromExternalStorage(fileUri)
removeFileFromList(binding.viewPager.currentItem)
}
}
override fun onNegativeButtonClicked() {
// Handle negative button click
}
override fun onNaturalButtonClicked() {
// Handle neutral button click
}
}
)
}
} }
binding.unHide.setOnClickListener { binding.unHide.setOnClickListener {
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem]) performFileUnHiding()
if (fileUri != null) { }
dialogUtil.showMaterialDialog( binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
getString(R.string.un_hide_file), override fun onPageSelected(position: Int) {
getString(R.string.are_you_sure_you_want_to_un_hide_this_file), super.onPageSelected(position)
getString(R.string.un_hide), adapter.onItemScrolledAway(currentPosition)
getString(R.string.cancel), currentPosition = position
object : DialogUtil.DialogCallback { }
override fun onPositiveButtonClicked() { })
lifecycleScope.launch { }
FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri)
removeFileFromList(binding.viewPager.currentItem) private fun handleDeleteFile() {
if (files.isEmpty() || currentPosition >= files.size) return
val currentFile = files[currentPosition]
val fileUri = FileManager.FileManager().getContentUriImage(this, currentFile)
if (fileUri != null) {
dialogUtil.showMaterialDialog(
getString(R.string.delete_file),
getString(R.string.are_you_sure_to_delete_this_file_permanently),
getString(R.string.delete_permanently),
getString(R.string.cancel),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
lifecycleScope.launch {
try {
val hiddenFile =
hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
hiddenFile?.let {
hiddenFileRepository.deleteHiddenFile(it)
}
fileManager.deletePhotoFromExternalStorage(fileUri)
removeFileFromList(currentPosition)
} catch (e: Exception) {
e.printStackTrace()
} }
} }
override fun onNegativeButtonClicked() {
// Handle negative button click
}
override fun onNaturalButtonClicked() {
// Handle neutral button click
}
} }
)
} override fun onNegativeButtonClicked() {}
override fun onNaturalButtonClicked() {}
}
)
} }
} }
private fun performFileUnHiding() {
if (files.isEmpty() || currentPosition >= files.size) return
val file = files[currentPosition]
dialogUtil.showMaterialDialog(
getString(R.string.un_hide_file),
getString(R.string.are_you_sure_you_want_to_un_hide_this_file),
getString(R.string.un_hide),
getString(R.string.cancel),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
lifecycleScope.launch {
var allUnhidden = true
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile != null){
if (hiddenFile.isEncrypted) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@PreviewActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
val fileUri = FileManager.FileManager()
.getContentUriImage(this@PreviewActivity, decryptedFile)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile.let {
hiddenFileRepository.deleteHiddenFile(it)
}
file.delete()
decryptedFile.delete()
removeFileFromList(currentPosition)
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
allUnhidden = false
}
} else {
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile.let {
hiddenFileRepository.deleteHiddenFile(it)
}
file.delete()
removeFileFromList(currentPosition)
} else {
allUnhidden = false
}
} else {
allUnhidden = false
}
}
}else{
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
file.delete()
removeFileFromList(currentPosition)
} else {
allUnhidden = false
}
} else {
allUnhidden = false
}
}
} catch (_: Exception) {
allUnhidden = false
}
mainHandler.post {
val message = if (allUnhidden) {
getString(R.string.file_unhidden_successfully)
} else {
getString(R.string.something_went_wrong)
}
Toast.makeText(this@PreviewActivity, message, Toast.LENGTH_SHORT).show()
}
}
}
override fun onNegativeButtonClicked() {}
override fun onNaturalButtonClicked() {}
}
)
}
private fun removeFileFromList(position: Int) { private fun removeFileFromList(position: Int) {
val updatedFiles = files.toMutableList().apply { removeAt(position) } if (position < 0 || position >= files.size) return
files = updatedFiles adapter.releaseAllResources()
adapter.images = updatedFiles // Update adapter with the new list files.removeAt(position)
adapter.images = files
// Update the ViewPager's position if (files.isEmpty()) {
if (updatedFiles.isEmpty()) finish() finish()
return
}
currentPosition = if (position >= files.size) {
files.size - 1
} else {
position
}
binding.viewPager.setCurrentItem(currentPosition, false)
updateFileInfo()
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
onBackPressed() finish()
return true return true
} }
}
}

View File

@@ -1,36 +1,36 @@
package devs.org.calculator.activities package devs.org.calculator.activities
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.enableEdgeToEdge import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.ViewCompat import androidx.core.net.toUri
import androidx.core.view.WindowCompat import com.google.android.material.color.DynamicColors
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
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
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.databinding.ActivitySettingsBinding import devs.org.calculator.databinding.ActivitySettingsBinding
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding private lateinit var binding: ActivitySettingsBinding
private lateinit var prefs: SharedPreferences private lateinit var prefs: PrefsUtil
private val DEV_GITHUB_URL = "https://github.com/binondi" private var DEV_GITHUB_URL = ""
private val GITHUB_URL = "$DEV_GITHUB_URL/calculator-hide-files" private var GITHUB_URL = ""
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
prefs = PrefsUtil(this)
prefs = getSharedPreferences("app_settings", MODE_PRIVATE) DEV_GITHUB_URL = getString(R.string.github_profile)
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
setupUI() setupUI()
loadSettings() loadSettings()
setupListeners() setupListeners()
@@ -42,6 +42,7 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
private fun loadSettings() { private fun loadSettings() {
binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true) binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
@@ -55,8 +56,11 @@ class SettingsActivity : AppCompatActivity() {
else -> binding.systemThemeRadio.isChecked = true else -> binding.systemThemeRadio.isChecked = true
} }
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
binding.customKeyStatus.isChecked = isUsingCustomKey
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true) binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true) binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
updateThemeModeVisibility() updateThemeModeVisibility()
} }
@@ -72,11 +76,19 @@ class SettingsActivity : AppCompatActivity() {
} }
binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked -> binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("dynamic_theme", isChecked).apply() prefs.setBoolean("dynamic_theme", isChecked)
if (!isChecked) { if (!isChecked) {
showThemeModeDialog() showThemeModeDialog()
}else{
showThemeModeDialog()
if (!prefs.getBoolean("isAppReopened",false)){
DynamicColors.applyToActivityIfAvailable(this)
}
} }
} }
binding.encryptionSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.setBoolean("encryption", isChecked)
}
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked -> binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
@@ -97,7 +109,7 @@ class SettingsActivity : AppCompatActivity() {
} }
binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked -> binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("screenshot_restriction", isChecked).apply() prefs.setBoolean("screenshot_restriction", isChecked)
if (isChecked) { if (isChecked) {
enableScreenshotRestriction() enableScreenshotRestriction()
} else { } else {
@@ -105,7 +117,11 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
binding.showFileNames.setOnCheckedChangeListener { _, isChecked -> binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("showFileName", isChecked).apply() prefs.setBoolean("showFileName", isChecked)
}
binding.customKeyStatus.setOnClickListener {
showCustomKeyDialog()
} }
} }
@@ -115,20 +131,16 @@ class SettingsActivity : AppCompatActivity() {
private fun showThemeModeDialog() { private fun showThemeModeDialog() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Theme Mode") .setTitle(getString(R.string.attention))
.setMessage("Would you like to set a specific theme mode?") .setMessage(getString(R.string.if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app))
.setPositiveButton("Yes") { _, _ -> .setPositiveButton(getString(R.string.ok)) { _, _ ->
binding.themeModeSwitch.isChecked = true
}
.setNegativeButton("No") { _, _ ->
binding.systemThemeRadio.isChecked = true
applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
} }
.show() .show()
} }
private fun applyThemeMode(themeMode: Int) { private fun applyThemeMode(themeMode: Int) {
prefs.edit().putInt("theme_mode", themeMode).apply() prefs.setInt("theme_mode", themeMode)
AppCompatDelegate.setDefaultNightMode(themeMode) AppCompatDelegate.setDefaultNightMode(themeMode)
} }
@@ -145,10 +157,64 @@ class SettingsActivity : AppCompatActivity() {
private fun openUrl(url: String) { private fun openUrl(url: String) {
try { try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Snackbar.make(binding.root, "Could not open URL", Snackbar.LENGTH_SHORT).show() Snackbar.make(binding.root,
getString(R.string.could_not_open_url), Snackbar.LENGTH_SHORT).show()
} }
} }
private fun showCustomKeyDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_custom_key, null)
val keyInput = dialogView.findViewById<EditText>(R.id.keyInput)
val confirmKeyInput = dialogView.findViewById<EditText>(R.id.confirmKeyInput)
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.set_custom_encryption_key))
.setView(dialogView)
.setPositiveButton(getString(R.string.set)) { dialog, _ ->
val key = keyInput.text.toString()
val confirmKey = confirmKeyInput.text.toString()
if (key.isEmpty()) {
Toast.makeText(this, getString(R.string.key_cannot_be_empty), Toast.LENGTH_SHORT).show()
updateUI()
return@setPositiveButton
}
if (key != confirmKey) {
Toast.makeText(this, getString(R.string.keys_do_not_match), Toast.LENGTH_SHORT).show()
updateUI()
return@setPositiveButton
}
if (SecurityUtils.setCustomKey(this, key)) {
Toast.makeText(this,
getString(R.string.custom_key_set_successfully), Toast.LENGTH_SHORT).show()
updateUI()
} else {
Toast.makeText(this,
getString(R.string.failed_to_set_custom_key), Toast.LENGTH_SHORT).show()
updateUI()
}
}
.setNegativeButton(getString(R.string.cancel)){ _, _ ->
updateUI()
}
.setNeutralButton(getString(R.string.delete_key)) { _, _ ->
SecurityUtils.clearCustomKey(this)
Toast.makeText(this,
getString(R.string.custom_encryption_key_cleared), Toast.LENGTH_SHORT).show()
updateUI()
}
.show()
}
private fun updateUI() {
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
binding.customKeyStatus.isChecked = isUsingCustomKey
}
} }

View File

@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
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
import devs.org.calculator.databinding.ActivitySetupPasswordBinding import devs.org.calculator.databinding.ActivitySetupPasswordBinding
@@ -18,6 +19,7 @@ class SetupPasswordActivity : AppCompatActivity() {
private lateinit var binding2: ActivityChangePasswordBinding private lateinit var binding2: ActivityChangePasswordBinding
private lateinit var prefsUtil: PrefsUtil private lateinit var prefsUtil: PrefsUtil
private var hasPassword = false private var hasPassword = false
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -72,8 +74,6 @@ class SetupPasswordActivity : AppCompatActivity() {
} }
binding.btnResetPassword.setOnClickListener { binding.btnResetPassword.setOnClickListener {
// Implement password reset logic
// Could use security questions or email verification
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString()) if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
else Toast.makeText(this, else Toast.makeText(this,
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show() getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()

View File

@@ -2,12 +2,12 @@ package devs.org.calculator.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.animation.Animation import android.view.animation.Animation
@@ -20,20 +20,27 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.adapters.FileAdapter import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderSelectionAdapter import devs.org.calculator.adapters.FolderSelectionAdapter
import devs.org.calculator.callbacks.FileProcessCallback import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.databinding.ActivityViewFolderBinding import devs.org.calculator.databinding.ActivityViewFolderBinding
import devs.org.calculator.databinding.ProccessingDialogBinding import devs.org.calculator.databinding.ProccessingDialogBinding
import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileManager.Companion.ENCRYPTED_EXTENSION
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
import devs.org.calculator.utils.FolderManager import devs.org.calculator.utils.FolderManager
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import android.widget.CheckBox
import android.widget.CompoundButton
import android.app.AlertDialog
class ViewFolderActivity : AppCompatActivity() { class ViewFolderActivity : AppCompatActivity() {
@@ -51,12 +58,12 @@ class ViewFolderActivity : AppCompatActivity() {
private var currentFolder: File? = null private var currentFolder: File? = null
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR) private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent> private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
private lateinit var prefs: SharedPreferences
private var customDialog: androidx.appcompat.app.AlertDialog? = null private var customDialog: androidx.appcompat.app.AlertDialog? = null
private var dialogShowTime: Long = 0 private var dialogShowTime: Long = 0
private val MINIMUM_DIALOG_DURATION = 1200L private val MINIMUM_DIALOG_DURATION = 1200L
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -81,9 +88,9 @@ class ViewFolderActivity : AppCompatActivity() {
private fun initialize() { private fun initialize() {
fileManager = FileManager(this, this) fileManager = FileManager(this, this)
folderManager = FolderManager(this) folderManager = FolderManager()
dialogUtil = DialogUtil(this) dialogUtil = DialogUtil(this)
prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
} }
private fun setupActivityResultLaunchers() { private fun setupActivityResultLaunchers() {
@@ -162,6 +169,34 @@ class ViewFolderActivity : AppCompatActivity() {
object : FileProcessCallback { object : FileProcessCallback {
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) { override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
mainHandler.post { mainHandler.post {
copiedFiles.forEach { file ->
val fileType = fileManager.getFileType(file)
var finalFile = file
val extension = ".${file.extension}"
val isEncrypted = prefs.getBoolean("encryption",false)
if (isEncrypted) {
val encryptedFile = SecurityUtils.changeFileExtension(file, ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(this@ViewFolderActivity, file, encryptedFile)) {
finalFile = encryptedFile
file.delete()
}
}
lifecycleScope.launch {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = finalFile.absolutePath,
fileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = isEncrypted,
encryptedFileName = finalFile.name
)
)
}
}
mainHandler.postDelayed({ mainHandler.postDelayed({
dismissCustomDialog() dismissCustomDialog()
}, 1000) }, 1000)
@@ -192,19 +227,12 @@ class ViewFolderActivity : AppCompatActivity() {
} }
} }
private fun refreshCurrentView() {
if (currentFolder != null) {
refreshCurrentFolder()
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
refreshCurrentFolder() refreshCurrentFolder()
} }
private fun openFolder(folder: File) { private fun openFolder(folder: File) {
// Ensure folder exists and has .nomedia file
if (!folder.exists()) { if (!folder.exists()) {
folder.mkdirs() folder.mkdirs()
File(folder, ".nomedia").createNewFile() File(folder, ".nomedia").createNewFile()
@@ -227,8 +255,6 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileList(files: List<File>, folder: File) { private fun showFileList(files: List<File>, folder: File) {
binding.recyclerView.layoutManager = GridLayoutManager(this, 3) binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
// Clean up previous adapter
fileAdapter?.cleanup() fileAdapter?.cleanup()
fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true), fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
@@ -306,24 +332,244 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileOptionsMenu(selectedFiles: List<File>) { private fun showFileOptionsMenu(selectedFiles: List<File>) {
if (selectedFiles.isEmpty()) return if (selectedFiles.isEmpty()) return
val options = arrayOf( lifecycleScope.launch {
getString(R.string.un_hide), var hasEncryptedFiles = false
getString(R.string.delete), var hasDecryptedFiles = false
getString(R.string.copy_to_another_folder), var hasEncFilesWithoutMetadata = false
getString(R.string.move_to_another_folder)
) for (file in selectedFiles) {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.file_options)) if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
.setItems(options) { _, which -> if (hiddenFile?.isEncrypted == true) {
when (which) { hasEncryptedFiles = true
0 -> unhideSelectedFiles(selectedFiles) } else {
1 -> deleteSelectedFiles(selectedFiles) hasEncFilesWithoutMetadata = true
2 -> copyToAnotherFolder(selectedFiles) }
3 -> moveToAnotherFolder(selectedFiles) } else {
hasDecryptedFiles = true
} }
} }
.show()
val options = mutableListOf(
getString(R.string.un_hide),
getString(R.string.delete),
getString(R.string.copy_to_another_folder),
getString(R.string.move_to_another_folder)
)
if (hasDecryptedFiles) {
options.add(getString(R.string.encrypt_file))
}
if (hasEncryptedFiles || hasEncFilesWithoutMetadata) {
options.add(getString(R.string.decrypt_file))
}
MaterialAlertDialogBuilder(this@ViewFolderActivity)
.setTitle(getString(R.string.file_options))
.setItems(options.toTypedArray()) { _, which ->
when (which) {
0 -> unhideSelectedFiles(selectedFiles)
1 -> deleteSelectedFiles(selectedFiles)
2 -> copyToAnotherFolder(selectedFiles)
3 -> moveToAnotherFolder(selectedFiles)
else -> {
val option = options[which]
when (option) {
getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles()
getString(R.string.decrypt_file) -> {
lifecycleScope.launch {
val filesWithoutMetadata = selectedFiles.filter { file ->
file.name.endsWith(ENCRYPTED_EXTENSION) &&
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
}
if (filesWithoutMetadata.isNotEmpty()) {
showDecryptionTypeDialog(filesWithoutMetadata)
} else {
fileAdapter?.decryptSelectedFiles()
}
}
}
}
}
}
}
.show()
}
}
private fun showDecryptionTypeDialog(selectedFiles: List<File>) {
val dialogView = layoutInflater.inflate(R.layout.dialog_file_type_selection, null)
val imageCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxImage)
val videoCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxVideo)
val audioCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxAudio)
val checkboxes = listOf(imageCheckbox, videoCheckbox, audioCheckbox)
checkboxes.forEach { checkbox ->
checkbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
checkboxes.filter { it != checkbox }.forEach { it.isChecked = false }
}
}
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.select_file_type))
.setMessage(getString(R.string.please_select_the_type_of_file_to_decrypt))
.setView(dialogView)
.setNegativeButton(getString(R.string.cancel), null)
.setPositiveButton(getString(R.string.decrypt), null)
.create()
dialog.setOnShowListener {
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.isEnabled = false
val checkboxListener = CompoundButton.OnCheckedChangeListener { _, _ ->
positiveButton.isEnabled = checkboxes.any { it.isChecked }
}
checkboxes.forEach { it.setOnCheckedChangeListener(checkboxListener) }
}
dialog.setOnDismissListener {
checkboxes.forEach { it.setOnCheckedChangeListener(null) }
}
dialog.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val selectedType = when {
imageCheckbox.isChecked -> FileManager.FileType.IMAGE
videoCheckbox.isChecked -> FileManager.FileType.VIDEO
audioCheckbox.isChecked -> FileManager.FileType.AUDIO
else -> return@setOnClickListener
}
lifecycleScope.launch {
performDecryptionWithType(selectedFiles, selectedType)
}
dialog.dismiss()
}
}
private fun performDecryptionWithType(selectedFiles: List<File>, fileType: FileManager.FileType) {
lifecycleScope.launch {
var successCount = 0
var failCount = 0
for (file in selectedFiles) {
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
val extension = when (fileType) {
FileManager.FileType.IMAGE -> ".jpg"
FileManager.FileType.VIDEO -> ".mp4"
FileManager.FileType.AUDIO -> ".mp3"
else -> ".txt"
}
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = decryptedFile.absolutePath,
fileName = decryptedFile.name,
encryptedFileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = false
)
)
if (file.delete()) {
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
mainHandler.post {
fileAdapter?.exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
refreshCurrentFolder()
}
}
} }
private fun moveToAnotherFolder(selectedFiles: List<File>) { private fun moveToAnotherFolder(selectedFiles: List<File>) {
@@ -342,15 +588,12 @@ class ViewFolderActivity : AppCompatActivity() {
object : DialogUtil.DialogCallback { object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() { override fun onPositiveButtonClicked() {
performFileUnhiding(selectedFiles) performFileUnhiding(selectedFiles)
} }
override fun onNegativeButtonClicked() { override fun onNegativeButtonClicked() {}
// Do nothing
}
override fun onNaturalButtonClicked() { override fun onNaturalButtonClicked() {}
// Do nothing
}
} }
) )
} }
@@ -359,8 +602,8 @@ class ViewFolderActivity : AppCompatActivity() {
private fun deleteSelectedFiles(selectedFiles: List<File>) { private fun deleteSelectedFiles(selectedFiles: List<File>) {
dialogUtil.showMaterialDialog( dialogUtil.showMaterialDialog(
getString(R.string.delete_items), getString(R.string.delete_file),
getString(R.string.are_you_sure_you_want_to_delete_selected_items), getString(R.string.are_you_sure_to_delete_selected_files_permanently),
getString(R.string.delete), getString(R.string.delete),
getString(R.string.cancel), getString(R.string.cancel),
object : DialogUtil.DialogCallback { object : DialogUtil.DialogCallback {
@@ -368,13 +611,9 @@ class ViewFolderActivity : AppCompatActivity() {
performFileDeletion(selectedFiles) performFileDeletion(selectedFiles)
} }
override fun onNegativeButtonClicked() { override fun onNegativeButtonClicked() {}
// Do nothing
}
override fun onNaturalButtonClicked() { override fun onNaturalButtonClicked() {}
// Do nothing
}
} }
) )
} }
@@ -383,20 +622,32 @@ class ViewFolderActivity : AppCompatActivity() {
private fun refreshCurrentFolder() { private fun refreshCurrentFolder() {
currentFolder?.let { folder -> currentFolder?.let { folder ->
val files = folderManager.getFilesInFolder(folder) lifecycleScope.launch {
if (files.isNotEmpty()) { try {
binding.recyclerView.visibility = View.VISIBLE val files = folderManager.getFilesInFolder(folder)
binding.noItems.visibility = View.GONE mainHandler.post {
fileAdapter?.submitList(files.toMutableList()) if (files.isNotEmpty()) {
fileAdapter?.let { adapter -> binding.recyclerView.visibility = View.VISIBLE
if (adapter.isInSelectionMode()) { binding.noItems.visibility = View.GONE
showFileSelectionIcons()
} else { fileAdapter?.submitList(files.toMutableList())
showFileViewIcons() fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) {
showFileSelectionIcons()
} else {
showFileViewIcons()
}
}
} else {
showEmptyState()
}
}
} catch (e: Exception) {
mainHandler.post {
showEmptyState()
} }
} }
} else {
showEmptyState()
} }
} }
} }
@@ -499,18 +750,59 @@ class ViewFolderActivity : AppCompatActivity() {
var allUnhidden = true var allUnhidden = true
selectedFiles.forEach { file -> selectedFiles.forEach { file ->
try { try {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file) val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (fileUri != null) { if (hiddenFile?.isEncrypted == true) {
val result = fileManager.copyFileToNormalDir(fileUri) val originalExtension = hiddenFile.originalExtension
if (result == null) { val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, decryptedFile)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
decryptedFile.delete()
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
allUnhidden = false allUnhidden = false
} }
} else { } else {
allUnhidden = false val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
} else {
allUnhidden = false
}
} else {
allUnhidden = false
}
} }
} catch (e: Exception) { } catch (e: Exception) {
allUnhidden = false allUnhidden = false
} }
} }
@@ -523,8 +815,6 @@ class ViewFolderActivity : AppCompatActivity() {
} }
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show() Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
// Fixed: Ensure proper order of operations
fileAdapter?.exitSelectionMode() fileAdapter?.exitSelectionMode()
refreshCurrentFolder() refreshCurrentFolder()
} }
@@ -532,24 +822,35 @@ class ViewFolderActivity : AppCompatActivity() {
} }
private fun performFileDeletion(selectedFiles: List<File>) { private fun performFileDeletion(selectedFiles: List<File>) {
var allDeleted = true lifecycleScope.launch {
selectedFiles.forEach { file -> var allDeleted = true
if (!file.delete()) { selectedFiles.forEach { file ->
allDeleted = false try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
if (!file.delete()) {
allDeleted = false
}
} catch (e: Exception) {
allDeleted = false
}
}
mainHandler.post {
val message = if (allDeleted) {
getString(R.string.files_deleted_successfully)
} else {
getString(R.string.some_items_could_not_be_deleted)
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
} }
val message = if (allDeleted) {
getString(R.string.files_deleted_successfully)
} else {
getString(R.string.some_items_could_not_be_deleted)
}
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Fixed: Ensure proper order of operations
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
private fun copyToAnotherFolder(selectedFiles: List<File>) { private fun copyToAnotherFolder(selectedFiles: List<File>) {
@@ -559,47 +860,78 @@ class ViewFolderActivity : AppCompatActivity() {
} }
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) { private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
var allCopied = true lifecycleScope.launch {
selectedFiles.forEach { file -> var allCopied = true
try { selectedFiles.forEach { file ->
val newFile = File(destinationFolder, file.name) try {
file.copyTo(newFile, overwrite = true) val newFile = File(destinationFolder, file.name)
} catch (e: Exception) { file.copyTo(newFile, overwrite = true)
allCopied = false
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
HiddenFileEntity(
filePath = newFile.absolutePath,
fileName = it.fileName,
fileType = it.fileType,
originalExtension = it.originalExtension,
isEncrypted = it.isEncrypted,
encryptedFileName = it.encryptedFileName
)
)
}
} catch (e: Exception) {
allCopied = false
}
}
mainHandler.post {
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
} }
val message = if (allCopied) "Files copied successfully" else "Some files could not be copied"
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Fixed: Ensure proper order of operations
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) { private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
var allMoved = true lifecycleScope.launch {
selectedFiles.forEach { file -> var allMoved = true
try { selectedFiles.forEach { file ->
val newFile = File(destinationFolder, file.name) try {
file.copyTo(newFile, overwrite = true) val newFile = File(destinationFolder, file.name)
file.delete() file.copyTo(newFile, overwrite = true)
} catch (e: Exception) {
allMoved = false val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = newFile.absolutePath,
encryptedFileName = it.encryptedFileName,
isEncrypted = it.isEncrypted
)
}
file.delete()
} catch (e: Exception) {
allMoved = false
}
}
mainHandler.post {
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
} }
val message = if (allMoved) "Files moved successfully" else "Some files could not be moved"
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Fixed: Ensure proper order of operations
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
} }
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 } // Exclude current folder .filter { it != currentFolder }
if (folders.isEmpty()) { if (folders.isEmpty()) {
Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()

View File

@@ -15,6 +15,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -22,7 +23,14 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R import devs.org.calculator.R
import devs.org.calculator.activities.PreviewActivity import devs.org.calculator.activities.PreviewActivity
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.SecurityUtils
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -38,18 +46,15 @@ class FileAdapter(
private val selectedItems = mutableSetOf<Int>() private val selectedItems = mutableSetOf<Int>()
private var isSelectionMode = false private var isSelectionMode = false
// Use WeakReference to prevent memory leaks
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
// Background executor for file operations
private val fileExecutor = Executors.newSingleThreadExecutor() private val fileExecutor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
companion object { val hiddenFileRepository: HiddenFileRepository by lazy {
private const val TAG = "FileAdapter" HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
} }
// Callback interface for handling file operations and selection changes
interface FileOperationCallback { interface FileOperationCallback {
fun onFileDeleted(file: File) fun onFileDeleted(file: File)
fun onFileRenamed(oldFile: File, newFile: File) fun onFileRenamed(oldFile: File, newFile: File)
@@ -67,29 +72,42 @@ class FileAdapter(
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView) val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
val playIcon: ImageView = view.findViewById(R.id.videoPlay) val playIcon: ImageView = view.findViewById(R.id.videoPlay)
val selectedLayer: View = view.findViewById(R.id.selectedLayer) val selectedLayer: View = view.findViewById(R.id.selectedLayer)
val shade: View = view.findViewById(R.id.shade)
val selected: ImageView = view.findViewById(R.id.selected) val selected: ImageView = view.findViewById(R.id.selected)
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
fun bind(file: File) { fun bind(file: File) {
val fileType = FileManager(context, lifecycleOwner).getFileType(file) lifecycleOwner.lifecycleScope.launch {
setupFileDisplay(file, fileType) try {
setupClickListeners(file, fileType) val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
else {
FileManager(context, lifecycleOwner).getFileType(file)
}
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
setupClickListeners(file, fileType)
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
// Update selection state val position = adapterPosition
val position = adapterPosition if (position != RecyclerView.NO_POSITION) {
if (position != RecyclerView.NO_POSITION) { val isSelected = selectedItems.contains(position)
val isSelected = selectedItems.contains(position) updateSelectionUI(isSelected)
updateSelectionUI(isSelected) }
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
} catch (e: Exception) {
}
} }
} }
fun bind(file: File, payloads: List<Any>) { fun bind(file: File, payloads: List<Any>) {
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
bind(file) bind(file)
return return
} }
// Handle partial updates based on payload
val changes = payloads.firstOrNull() as? List<String> val changes = payloads.firstOrNull() as? List<String>
changes?.forEach { change -> changes?.forEach { change ->
when (change) { when (change) {
@@ -97,14 +115,13 @@ class FileAdapter(
fileNameTextView.text = file.name fileNameTextView.text = file.name
} }
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> { "SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
// Could update file info if displayed
} }
"SELECTION_CHANGED" -> { "SELECTION_CHANGED" -> {
val position = adapterPosition val position = adapterPosition
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
val isSelected = selectedItems.contains(position) val isSelected = selectedItems.contains(position)
updateSelectionUI(isSelected) updateSelectionUI(isSelected)
// Notify activity about selection change
notifySelectionModeChange() notifySelectionModeChange()
} }
} }
@@ -117,37 +134,98 @@ class FileAdapter(
selected.visibility = if (isSelected) View.VISIBLE else View.GONE selected.visibility = if (isSelected) View.VISIBLE else View.GONE
} }
private fun setupFileDisplay(file: File, fileType: FileManager.FileType) { private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
fileNameTextView.text = file.name fileNameTextView.text = metadata?.fileName ?: file.name
when (fileType) { when (fileType) {
FileManager.FileType.IMAGE -> { FileManager.FileType.IMAGE -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
Glide.with(context) if (isEncrypted) {
.load(file) try {
.centerCrop() val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
.error(R.drawable.ic_document) val uri = getUriForPreviewFile(context, decryptedFile)
.placeholder(R.drawable.ic_document) if (uri != null) {
.into(imageView) 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 -> { FileManager.FileType.VIDEO -> {
playIcon.visibility = View.VISIBLE playIcon.visibility = View.VISIBLE
Glide.with(context) if (isEncrypted) {
.load(file) try {
.centerCrop() val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
.error(R.drawable.ic_document) val uri = getUriForPreviewFile(context, decryptedFile)
.placeholder(R.drawable.ic_document) if (uri != null) {
.into(imageView) 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 -> { FileManager.FileType.AUDIO -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
imageView.setImageResource(R.drawable.ic_audio) if (isEncrypted) {
imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_audio)
}
imageView.setPadding(50, 50, 50, 50)
} }
else -> { else -> {
playIcon.visibility = View.GONE playIcon.visibility = View.GONE
imageView.setImageResource(R.drawable.ic_document) if (isEncrypted) {
imageView.setImageResource(R.drawable.encrypted)
} else {
imageView.setImageResource(R.drawable.ic_document)
}
imageView.setPadding(50, 50, 50, 50)
} }
} }
} }
@@ -178,33 +256,83 @@ class FileAdapter(
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, "File no longer exists", Toast.LENGTH_SHORT).show() Toast.makeText(context,
context.getString(R.string.file_no_longer_exists), Toast.LENGTH_SHORT).show()
return return
} }
when (fileType) { when (fileType) {
FileManager.FileType.AUDIO -> openAudioFile(file) FileManager.FileType.AUDIO -> openAudioFile(file)
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> openInPreview(fileType) FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
lifecycleOwner.lifecycleScope.launch {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true || file.extension == FileManager.ENCRYPTED_EXTENSION) {
if (file.extension == FileManager.ENCRYPTED_EXTENSION && hiddenFile == null) {
showDecryptionTypeDialog(file)
} else {
val tempFile = File(context.cacheDir, "preview_${file.name}")
if (SecurityUtils.decryptFile(context, file, tempFile)) {
if (tempFile.exists() && tempFile.length() > 0) {
mainHandler.post {
val fileTypeString = when (fileType) {
FileManager.FileType.IMAGE -> context.getString(R.string.image)
FileManager.FileType.VIDEO -> context.getString(R.string.video)
else -> "unknown"
}
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
putExtra("isEncrypted", true)
putExtra("tempFile", tempFile.absolutePath)
}
context.startActivity(intent)
}
} else {
mainHandler.post {
Toast.makeText(context, "Failed to prepare file for preview", Toast.LENGTH_SHORT).show()
}
}
} else {
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file for preview", Toast.LENGTH_SHORT).show()
}
}
}
} else {
openInPreview(fileType)
}
} catch (e: Exception) {
mainHandler.post {
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
}
}
}
}
FileManager.FileType.DOCUMENT -> openDocumentFile(file) FileManager.FileType.DOCUMENT -> openDocumentFile(file)
else -> openDocumentFile(file) else -> openDocumentFile(file)
} }
} }
private fun openAudioFile(file: File) { private fun openAudioFile(file: File) {
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
try { try {
val uri = FileProvider.getUriForFile( val fileTypeString = when (fileType) {
context, FileManager.FileType.IMAGE -> context.getString(R.string.image)
"${context.packageName}.fileprovider", FileManager.FileType.VIDEO -> context.getString(R.string.video)
file else -> "unknown"
) }
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "audio/*") val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", fileTypeString)
putExtra("folder", currentFolder.toString()) putExtra("folder", currentFolder.toString())
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) putExtra("position", adapterPosition)
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to open audio file: ${e.message}")
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.no_audio_player_found), context.getString(R.string.no_audio_player_found),
@@ -227,7 +355,7 @@ class FileAdapter(
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to open document file: ${e.message}")
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.no_suitable_app_found_to_open_this_document), context.getString(R.string.no_suitable_app_found_to_open_this_document),
@@ -251,102 +379,6 @@ class FileAdapter(
context.startActivity(intent) context.startActivity(intent)
} }
private fun showFileOptionsDialog(file: File) {
val options = if (isSelectionMode) {
arrayOf(
context.getString(R.string.un_hide),
context.getString(R.string.delete),
context.getString(R.string.copy_to_another_folder),
context.getString(R.string.move_to_another_folder)
)
} else {
arrayOf(
context.getString(R.string.un_hide),
context.getString(R.string.select_multiple),
context.getString(R.string.rename),
context.getString(R.string.delete),
context.getString(R.string.share)
)
}
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.file_options))
.setItems(options) { dialog, which ->
if (isSelectionMode) {
when (which) {
0 -> unHideFile(file)
1 -> deleteFile(file)
2 -> copyToAnotherFolder(file)
3 -> moveToAnotherFolder(file)
}
} else {
when (which) {
0 -> unHideFile(file)
1 -> enableSelectMultipleFiles()
2 -> renameFile(file)
3 -> deleteFile(file)
4 -> shareFile(file)
}
}
dialog.dismiss()
}
.create()
.show()
}
private fun enableSelectMultipleFiles() {
val position = adapterPosition
if (position == RecyclerView.NO_POSITION) return
enterSelectionMode()
selectedItems.add(position)
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
private fun unHideFile(file: File) {
FileManager(context, lifecycleOwner).unHideFile(
file = file,
onSuccess = {
fileOperationCallback?.get()?.onFileDeleted(file)
},
onError = { errorMessage ->
Toast.makeText(context, "Failed to unhide: $errorMessage", Toast.LENGTH_SHORT).show()
}
)
}
private fun deleteFile(file: File) {
// Show confirmation dialog first
MaterialAlertDialogBuilder(context)
.setTitle("Delete File")
.setMessage("Are you sure you want to delete ${file.name}?")
.setPositiveButton("Delete") { _, _ ->
deleteFileAsync(file)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun deleteFileAsync(file: File) {
fileExecutor.execute {
val success = try {
file.delete()
} catch (e: Exception) {
Log.e(TAG, "Failed to delete file: ${e.message}")
false
}
mainHandler.post {
if (success) {
fileOperationCallback?.get()?.onFileDeleted(file)
Toast.makeText(context, "File deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show()
}
}
}
}
@SuppressLint("MissingInflatedId") @SuppressLint("MissingInflatedId")
private fun renameFile(file: File) { private fun renameFile(file: File) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null) val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
@@ -398,7 +430,6 @@ class FileAdapter(
val success = try { val success = try {
file.renameTo(newFile) file.renameTo(newFile)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to rename file: ${e.message}")
false false
} }
@@ -414,36 +445,6 @@ class FileAdapter(
} }
} }
private fun shareFile(file: File) {
try {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = context.contentResolver.getType(uri) ?: "*/*"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
)
} catch (e: Exception) {
Log.e(TAG, "Failed to share file: ${e.message}")
Toast.makeText(context, "Failed to share file", Toast.LENGTH_SHORT).show()
}
}
private fun copyToAnotherFolder(file: File) {
// This will be handled by the activity
fileOperationCallback?.get()?.onRefreshNeeded()
}
private fun moveToAnotherFolder(file: File) {
// This will be handled by the activity
fileOperationCallback?.get()?.onRefreshNeeded()
}
private fun toggleSelection(position: Int) { private fun toggleSelection(position: Int) {
if (selectedItems.contains(position)) { if (selectedItems.contains(position)) {
@@ -457,6 +458,91 @@ class FileAdapter(
onSelectionCountChanged(selectedItems.size) onSelectionCountChanged(selectedItems.size)
notifyItemChanged(position) notifyItemChanged(position)
} }
private fun showEncryptedIcon() {
imageView.setImageResource(R.drawable.encrypted)
imageView.setPadding(50, 50, 50, 50)
}
private fun showDecryptionTypeDialog(file: File) {
val options = arrayOf("Image", "Video", "Audio")
MaterialAlertDialogBuilder(context)
.setTitle("Select File Type")
.setMessage("Please select the type of file to decrypt")
.setItems(options) { _, which ->
val selectedType = when (which) {
0 -> FileManager.FileType.IMAGE
1 -> FileManager.FileType.VIDEO
2 -> FileManager.FileType.AUDIO
else -> FileManager.FileType.DOCUMENT
}
performDecryptionWithType(file, selectedType)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
lifecycleOwner.lifecycleScope.launch {
try {
val extension = when (fileType) {
FileManager.FileType.IMAGE -> ".jpg"
FileManager.FileType.VIDEO -> ".mp4"
FileManager.FileType.AUDIO -> ".mp3"
else -> ".txt"
}
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = decryptedFile.absolutePath,
fileName = decryptedFile.name,
encryptedFileName = file.name,
fileType = fileType,
originalExtension = extension,
isEncrypted = false
)
)
when (fileType) {
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
val intent = Intent(context, PreviewActivity::class.java).apply {
putExtra("type", if (fileType == FileManager.FileType.IMAGE) "image" else "video")
putExtra("folder", currentFolder.toString())
putExtra("position", adapterPosition)
putExtra("isEncrypted", false)
putExtra("file", decryptedFile.absolutePath)
}
context.startActivity(intent)
}
FileManager.FileType.AUDIO -> openAudioFile(decryptedFile)
else -> openDocumentFile(decryptedFile)
}
file.delete()
} else {
decryptedFile.delete()
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
}
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
mainHandler.post {
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
mainHandler.post {
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
}
}
}
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
@@ -489,8 +575,7 @@ class FileAdapter(
currentList.clear() currentList.clear()
super.submitList(null) super.submitList(null)
} else { } else {
// Create a new list to force update val newList = list.filter { it.name != ".nomedia" }.toMutableList()
val newList = list.toMutableList()
super.submitList(newList) super.submitList(newList)
} }
} }
@@ -502,6 +587,7 @@ class FileAdapter(
} }
} }
@SuppressLint("NotifyDataSetChanged")
fun exitSelectionMode() { fun exitSelectionMode() {
if (isSelectionMode) { if (isSelectionMode) {
isSelectionMode = false isSelectionMode = false
@@ -528,19 +614,12 @@ class FileAdapter(
if (!isSelectionMode) { if (!isSelectionMode) {
enterSelectionMode() enterSelectionMode()
} }
val previouslySelected = selectedItems.toSet() val previouslySelected = selectedItems.toSet()
selectedItems.clear() selectedItems.clear()
// Add all positions to selection
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
selectedItems.add(i) selectedItems.add(i)
} }
// Notify callback about selection change
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size) fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
// Update UI for changed items efficiently
updateSelectionItems(selectedItems.toSet(), previouslySelected) updateSelectionItems(selectedItems.toSet(), previouslySelected)
} }
@@ -571,128 +650,6 @@ class FileAdapter(
fun isInSelectionMode(): Boolean = isSelectionMode fun isInSelectionMode(): Boolean = isSelectionMode
fun deleteSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
// Show confirmation dialog
MaterialAlertDialogBuilder(context)
.setTitle("Delete Files")
.setMessage("Are you sure you want to delete ${selectedFiles.size} file(s)?")
.setPositiveButton("Delete") { _, _ ->
deleteFilesAsync(selectedFiles)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun deleteFilesAsync(selectedFiles: List<File>) {
fileExecutor.execute {
var deletedCount = 0
var failedCount = 0
val failedFiles = mutableListOf<String>()
selectedFiles.forEach { file ->
try {
if (file.delete()) {
deletedCount++
mainHandler.post {
fileOperationCallback?.get()?.onFileDeleted(file)
}
} else {
failedCount++
failedFiles.add(file.name)
}
} catch (e: Exception) {
failedCount++
failedFiles.add(file.name)
Log.e(TAG, "Failed to delete ${file.name}: ${e.message}")
}
}
mainHandler.post {
// Exit selection mode first
exitSelectionMode()
// Show detailed result message
when {
deletedCount > 0 && failedCount == 0 -> {
Toast.makeText(context, "Deleted $deletedCount file(s)", Toast.LENGTH_SHORT).show()
}
deletedCount > 0 && failedCount > 0 -> {
Toast.makeText(context,
"Deleted $deletedCount file(s), failed to delete $failedCount",
Toast.LENGTH_LONG).show()
}
failedCount > 0 -> {
Toast.makeText(context,
"Failed to delete $failedCount file(s)",
Toast.LENGTH_SHORT).show()
}
}
}
}
}
fun shareSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
try {
if (selectedFiles.size == 1) {
// Share single file
val file = selectedFiles.first()
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = context.contentResolver.getType(uri) ?: "*/*"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
)
} else {
// Share multiple files
val uris = selectedFiles.mapNotNull { file ->
try {
FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
} catch (e: Exception) {
Log.e(TAG, "Failed to get URI for file ${file.name}: ${e.message}")
null
}
}
if (uris.isNotEmpty()) {
val shareIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "*/*"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, "Share ${selectedFiles.size} files")
)
} else {
Toast.makeText(context, "No files could be shared", Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to share files: ${e.message}")
Toast.makeText(context, "Failed to share files", Toast.LENGTH_SHORT).show()
}
exitSelectionMode()
}
fun onBackPressed(): Boolean { fun onBackPressed(): Boolean {
return if (isSelectionMode) { return if (isSelectionMode) {
exitSelectionMode() exitSelectionMode()
@@ -702,25 +659,12 @@ class FileAdapter(
} }
} }
fun refreshSelectionStates() {
if (isSelectionMode) {
selectedItems.forEach { position ->
if (position < itemCount) {
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
}
// Ensure callback is notified
notifySelectionModeChange()
}
}
fun cleanup() { fun cleanup() {
try { try {
if (!fileExecutor.isShutdown) { if (!fileExecutor.isShutdown) {
fileExecutor.shutdown() fileExecutor.shutdown()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error shutting down executor: ${e.message}")
} }
fileOperationCallback?.clear() fileOperationCallback?.clear()
@@ -734,4 +678,169 @@ class FileAdapter(
private fun onSelectionCountChanged(count: Int) { private fun onSelectionCountChanged(count: Int) {
fileOperationCallback?.get()?.onSelectionCountChanged(count) fileOperationCallback?.get()?.onSelectionCountChanged(count)
} }
fun encryptSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.encrypt_files))
.setMessage(context.getString(R.string.encryption_disclaimer))
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
performEncryption(selectedFiles)
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performEncryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) continue
val originalExtension = ".${file.extension.lowercase()}"
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
if (encryptedFile.exists()) {
if (hiddenFile == null){
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = encryptedFile.absolutePath,
isEncrypted = true,
encryptedFileName = encryptedFile.name,
fileType = fileType,
fileName = file.name,
originalExtension = originalExtension
)
)
}else{
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = hiddenFile.filePath,
newFilePath = encryptedFile.absolutePath,
encryptedFileName = encryptedFile.name,
isEncrypted = true
)
}
}
if (file.delete()) {
updatedFiles.add(encryptedFile)
successCount++
} else {
failCount++
}
} else {
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
}
fun decryptSelectedFiles() {
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.decrypt_files))
.setMessage(context.getString(R.string.decryption_disclaimer))
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
performDecryption(selectedFiles)
}
.setNegativeButton(context.getString(R.string.cancel), null)
.show()
}
private fun performDecryption(selectedFiles: List<File>) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val updatedFiles = mutableListOf<File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted != true) continue
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
updatedFiles.add(decryptedFile)
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} catch (e: Exception) {
failCount++
}
}
mainHandler.post {
exitSelectionMode()
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
}
failCount > 0 -> {
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
}
}
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
submitList(currentFiles)
fileOperationCallback?.get()?.onRefreshNeeded()
}
}
}
} }

View File

@@ -6,13 +6,10 @@ import java.io.File
class FileDiffCallback : DiffUtil.ItemCallback<File>() { class FileDiffCallback : DiffUtil.ItemCallback<File>() {
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean { override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
// Compare by absolute path since File objects might be different instances
// but represent the same file
return oldItem.absolutePath == newItem.absolutePath return oldItem.absolutePath == newItem.absolutePath
} }
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean { override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
// Compare all relevant properties that might change and affect the UI
return oldItem.name == newItem.name && return oldItem.name == newItem.name &&
oldItem.length() == newItem.length() && oldItem.length() == newItem.length() &&
oldItem.lastModified() == newItem.lastModified() && oldItem.lastModified() == newItem.lastModified() &&
@@ -22,8 +19,6 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
} }
override fun getChangePayload(oldItem: File, newItem: File): Any? { override fun getChangePayload(oldItem: File, newItem: File): Any? {
// Return a payload if only specific properties changed
// This allows for partial updates instead of full rebinding
val changes = mutableListOf<String>() val changes = mutableListOf<String>()
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
@@ -42,6 +37,6 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
changes.add("EXISTENCE_CHANGED") changes.add("EXISTENCE_CHANGED")
} }
return if (changes.isNotEmpty()) changes else null return changes.ifEmpty { null }
} }
} }

View File

@@ -1,5 +1,6 @@
package devs.org.calculator.adapters package devs.org.calculator.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -80,6 +81,7 @@ class FolderAdapter(
} }
} }
@SuppressLint("NotifyDataSetChanged")
fun clearSelection() { fun clearSelection() {
val wasInSelectionMode = isSelectionMode val wasInSelectionMode = isSelectionMode
selectedItems.clear() selectedItems.clear()

View File

@@ -9,15 +9,20 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import android.widget.SeekBar
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import devs.org.calculator.R
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ViewpagerItemsBinding import devs.org.calculator.databinding.ViewpagerItemsBinding
import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import devs.org.calculator.R
class ImagePreviewAdapter( class ImagePreviewAdapter(
private val context: Context, private val context: Context,
@@ -25,11 +30,11 @@ class ImagePreviewAdapter(
) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() { ) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() {
private val differ = AsyncListDiffer(this, FileDiffCallback()) private val differ = AsyncListDiffer(this, FileDiffCallback())
var currentMediaPlayer: MediaPlayer? = null
var isMediaPlayerPrepared = false
var currentViewHolder: ImageViewHolder? = null
private var currentPlayingPosition = -1 private var currentPlayingPosition = -1
private var isPlaying = false private var currentViewHolder: ImageViewHolder? = null
private val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
var images: List<File> var images: List<File>
get() = differ.currentList get() = differ.currentList
@@ -42,40 +47,78 @@ class ImagePreviewAdapter(
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = images[position] val imageUrl = images[position]
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position]) val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
holder.bind(imageUrl,fileType) stopAndResetCurrentAudio()
currentViewHolder = holder
currentMediaPlayer?.let { holder.bind(imageUrl, position,fileType)
if (it.isPlaying) it.pause()
it.seekTo(0)
}
currentMediaPlayer = null
isMediaPlayerPrepared = false
if (currentMediaPlayer?.isPlaying == true) {
currentMediaPlayer?.stop()
currentMediaPlayer?.release()
}
currentMediaPlayer = null
} }
override fun getItemCount(): Int = images.size override fun getItemCount(): Int = images.size
private fun stopAndResetCurrentAudio() {
currentViewHolder?.stopAndResetAudio()
currentPlayingPosition = -1
currentViewHolder = null
}
inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) { inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) {
private var mediaPlayer: MediaPlayer? = null
private var seekHandler = Handler(Looper.getMainLooper()) private var seekHandler = Handler(Looper.getMainLooper())
private var seekRunnable: Runnable? = null private var seekRunnable: Runnable? = null
private var mediaPlayer: MediaPlayer? = null
private var isMediaPlayerPrepared = false
private var isPlaying = false
private var currentPosition = 0
private var tempDecryptedFile: File? = null
fun bind(file: File, fileType: FileManager.FileType) { fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
currentPosition = position
releaseMediaPlayer()
resetAudioUI()
cleanupTempFile()
try {
lifecycleOwner.lifecycleScope.launch {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile != null){
val isEncrypted = hiddenFile.isEncrypted
val fileType = hiddenFile.fileType
if (isEncrypted) {
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
displayFile(tempDecryptedFile, fileType,true)
} else {
showEncryptedError()
}
} else {
displayFile(file, decryptedFileType,false)
}
}else{
displayFile(file, decryptedFileType,false)
}
}
} catch (_: Exception) {
displayFile(file, decryptedFileType,false)
}
}
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
val uri = getUriForPreviewFile(context, file)
when (fileType) { when (fileType) {
FileManager.FileType.VIDEO -> { FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE binding.videoView.visibility = View.VISIBLE
val videoUri = Uri.fromFile(file) val videoUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.videoView.setVideoURI(videoUri) binding.videoView.setVideoURI(videoUri)
binding.videoView.start() binding.videoView.start()
@@ -100,21 +143,30 @@ class ImagePreviewAdapter(
} }
} }
FileManager.FileType.IMAGE -> { FileManager.FileType.IMAGE -> {
val imageUri = if (isEncrypted){
uri
}else{
Uri.fromFile(file)
}
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE binding.audioBg.visibility = View.GONE
Glide.with(context) Glide.with(context)
.load(file) .load(imageUri)
.into(binding.imageView) .into(binding.imageView)
} }
FileManager.FileType.AUDIO -> { FileManager.FileType.AUDIO -> {
val audioFile: File? = if (isEncrypted) {
getFileFromUri(context, uri!!)
} else {
file
}
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name binding.audioTitle.text = file.name
setupAudioPlayer(file) setupAudioPlayer(audioFile!!)
setupSeekBar()
setupPlaybackControls() setupPlaybackControls()
} }
else -> { else -> {
@@ -125,27 +177,73 @@ class ImagePreviewAdapter(
} }
} }
fun getFileFromUri(context: Context, uri: Uri): File? {
return try {
val inputStream = context.contentResolver.openInputStream(uri)
val tempFile = File.createTempFile("temp_audio", null, context.cacheDir)
tempFile.outputStream().use { output ->
inputStream?.copyTo(output)
}
tempFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun showEncryptedError() {
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.imageView.setImageResource(R.drawable.encrypted)
}
private fun cleanupTempFile() {
tempDecryptedFile?.let { file ->
if (file.exists()) {
try {
file.delete()
} catch (_: Exception) {
}
}
tempDecryptedFile = null
}
}
private fun resetAudioUI() {
binding.playPause.setImageResource(R.drawable.play)
binding.audioSeekBar.value = 0f
binding.audioSeekBar.valueTo = 100f
seekRunnable?.let { seekHandler.removeCallbacks(it) }
}
private fun setupAudioPlayer(file: File) { private fun setupAudioPlayer(file: File) {
mediaPlayer = MediaPlayer().apply { try {
setDataSource(file.absolutePath) mediaPlayer = MediaPlayer().apply {
setOnPreparedListener { mp -> setDataSource(file.absolutePath)
binding.audioSeekBar.valueTo = mp.duration.toFloat() setOnPreparedListener { mp ->
binding.audioSeekBar.valueTo = mp.duration.toFloat()
isMediaPlayerPrepared = true binding.audioSeekBar.value = 0f
setupSeekBar()
isMediaPlayerPrepared = true
}
setOnCompletionListener {
stopAndResetAudio()
}
setOnErrorListener { _, _, _ ->
releaseMediaPlayer()
true
}
prepareAsync()
} }
setOnCompletionListener { } catch (e: Exception) {
// isPlaying = false e.printStackTrace()
binding.playPause.setImageResource(R.drawable.play) releaseMediaPlayer()
binding.audioSeekBar.value = 0f
seekHandler.removeCallbacks(seekRunnable!!)
}
prepareAsync()
} }
} }
private fun setupSeekBar() { private fun setupSeekBar() {
binding.audioSeekBar.addOnChangeListener { slider, value, fromUser -> binding.audioSeekBar.addOnChangeListener { _, value, fromUser ->
if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) { if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) {
mediaPlayer?.seekTo(value.toInt()) mediaPlayer?.seekTo(value.toInt())
} }
@@ -153,15 +251,18 @@ class ImagePreviewAdapter(
seekRunnable = Runnable { seekRunnable = Runnable {
mediaPlayer?.let { mp -> mediaPlayer?.let { mp ->
if (mp.isPlaying) { if (mp.isPlaying && isMediaPlayerPrepared) {
binding.audioSeekBar.value = mp.currentPosition.toFloat() try {
seekHandler.postDelayed(seekRunnable!!, 100) binding.audioSeekBar.value = mp.currentPosition.toFloat()
seekHandler.postDelayed(seekRunnable!!, 100)
} catch (e: Exception) {
e.printStackTrace()
}
} }
} }
} }
} }
private fun setupPlaybackControls() { private fun setupPlaybackControls() {
binding.playPause.setOnClickListener { binding.playPause.setOnClickListener {
if (isPlaying) { if (isPlaying) {
@@ -173,65 +274,127 @@ class ImagePreviewAdapter(
binding.preview.setOnClickListener { binding.preview.setOnClickListener {
mediaPlayer?.let { mp -> mediaPlayer?.let { mp ->
val newPosition = mp.currentPosition - 10000 if (isMediaPlayerPrepared) {
mp.seekTo(maxOf(0, newPosition)) try {
binding.audioSeekBar.value = mp.currentPosition.toFloat() val newPosition = mp.currentPosition - 10000
mp.seekTo(maxOf(0, newPosition))
binding.audioSeekBar.value = mp.currentPosition.toFloat()
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
} }
binding.next.setOnClickListener { binding.next.setOnClickListener {
mediaPlayer?.let { mp -> mediaPlayer?.let { mp ->
val newPosition = mp.currentPosition + 10000 if (isMediaPlayerPrepared) {
mp.seekTo(minOf(mp.duration, newPosition)) try {
binding.audioSeekBar.value = mp.currentPosition.toFloat() val newPosition = mp.currentPosition + 10000
mp.seekTo(minOf(mp.duration, newPosition))
binding.audioSeekBar.value = mp.currentPosition.toFloat()
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
} }
} }
private fun playAudio() { private fun playAudio() {
mediaPlayer?.let { mp -> mediaPlayer?.let { mp ->
if (currentPlayingPosition != -1 && currentPlayingPosition != adapterPosition) { if (isMediaPlayerPrepared) {
currentViewHolder?.pauseAudio() try {
if (currentPlayingPosition != currentPosition) {
stopAndResetCurrentAudio()
}
mp.start()
isPlaying = true
binding.playPause.setImageResource(R.drawable.pause)
seekRunnable?.let { seekHandler.post(it) }
currentPlayingPosition = currentPosition
currentViewHolder = this@ImageViewHolder
} catch (e: Exception) {
e.printStackTrace()
releaseMediaPlayer()
}
} }
mp.start()
isPlaying = true
binding.playPause.setImageResource(R.drawable.pause)
seekHandler.post(seekRunnable!!)
currentPlayingPosition = adapterPosition
currentViewHolder = this
} }
} }
private fun pauseAudio() { private fun pauseAudio() {
mediaPlayer?.let { mp -> mediaPlayer?.let { mp ->
if (mp.isPlaying) { try {
mp.pause() if (mp.isPlaying) {
isPlaying = false mp.pause()
binding.playPause.setImageResource(R.drawable.play) isPlaying = false
seekHandler.removeCallbacks(seekRunnable!!) binding.playPause.setImageResource(R.drawable.play)
seekRunnable?.let { seekHandler.removeCallbacks(it) }
}
} catch (e: Exception) {
e.printStackTrace()
releaseMediaPlayer()
} }
} }
} }
fun stopAndResetAudio() {
try {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
mp.stop()
mp.prepare()
} else if (isMediaPlayerPrepared) {
mp.seekTo(0)
}
}
isPlaying = false
resetAudioUI()
if (currentPlayingPosition == currentPosition) {
currentPlayingPosition = -1
currentViewHolder = null
}
} catch (e: Exception) {
e.printStackTrace()
releaseMediaPlayer()
}
}
fun releaseMediaPlayer() { fun releaseMediaPlayer() {
mediaPlayer?.let { mp -> try {
if (mp.isPlaying) { mediaPlayer?.let { mp ->
mp.stop() if (mp.isPlaying) {
mp.stop()
}
mp.release()
} }
mp.release() } catch (e: Exception) {
e.printStackTrace()
} finally {
mediaPlayer = null mediaPlayer = null
isPlaying = false isPlaying = false
seekHandler.removeCallbacks(seekRunnable!!) isMediaPlayerPrepared = false
seekRunnable?.let { seekHandler.removeCallbacks(it) }
if (currentPlayingPosition == currentPosition) {
currentPlayingPosition = -1
currentViewHolder = null
}
} }
} }
private fun playVideoAtPosition(position: Int) { private fun playVideoAtPosition(position: Int) {
val nextFile = images[position] if (position < images.size) {
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position]) val nextFile = images[position]
if (fileType == FileManager.FileType.VIDEO) { val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
val videoUri = Uri.fromFile(nextFile) if (fileType == FileManager.FileType.VIDEO) {
binding.videoView.setVideoURI(videoUri) val videoUri = Uri.fromFile(nextFile)
binding.videoView.start() binding.videoView.setVideoURI(videoUri)
binding.videoView.start()
}
} }
} }
} }
@@ -240,5 +403,14 @@ class ImagePreviewAdapter(
super.onViewRecycled(holder) super.onViewRecycled(holder)
holder.releaseMediaPlayer() holder.releaseMediaPlayer()
} }
}
fun onItemScrolledAway(position: Int) {
if (currentPlayingPosition == position) {
stopAndResetCurrentAudio()
}
}
fun releaseAllResources() {
stopAndResetCurrentAudio()
}
}

View File

@@ -1,9 +1,9 @@
package devs.org.calculator.adapters package devs.org.calculator.adapters
import android.annotation.SuppressLint
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.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
@@ -22,9 +22,9 @@ class ListFolderAdapter(
private var isSelectionMode = false private var isSelectionMode = false
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val folderNameTextView: TextView = itemView.findViewById(R.id.folderName) private val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
val selectedLayer: View = itemView.findViewById(R.id.selectedLayer) private val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) { fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
folderNameTextView.text = folder.name folderNameTextView.text = folder.name
@@ -89,6 +89,7 @@ class ListFolderAdapter(
} }
} }
@SuppressLint("NotifyDataSetChanged")
fun clearSelection() { fun clearSelection() {
val wasInSelectionMode = isSelectionMode val wasInSelectionMode = isSelectionMode
selectedItems.clear() selectedItems.clear()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,14 @@
package devs.org.calculator.utils package devs.org.calculator.utils
import android.content.Context
import android.os.Environment
import java.io.File import java.io.File
class FolderManager(private val context: Context) { class FolderManager {
companion object {
const val HIDDEN_DIR = ".CalculatorHide"
}
fun createFolder(parentDir: File, folderName: String): Boolean { fun createFolder(parentDir: File, folderName: String): Boolean {
val newFolder = File(parentDir, folderName) val newFolder = File(parentDir, folderName)
return if (!newFolder.exists()) { return if (!newFolder.exists()) {
newFolder.mkdirs() newFolder.mkdirs()
// Create .nomedia file to hide from media scanners
File(newFolder, ".nomedia").createNewFile() File(newFolder, ".nomedia").createNewFile()
true true
} else { } else {
@@ -54,20 +49,4 @@ class FolderManager(private val context: Context) {
emptyList() emptyList()
} }
} }
fun moveFileToFolder(file: File, targetFolder: File): Boolean {
return try {
if (!targetFolder.exists()) {
targetFolder.mkdirs()
File(targetFolder, ".nomedia").createNewFile()
}
val newFile = File(targetFolder, file.name)
file.copyTo(newFile, overwrite = true)
file.delete()
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
} }

View File

@@ -3,6 +3,7 @@ package devs.org.calculator.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import java.security.MessageDigest import java.security.MessageDigest
import androidx.core.content.edit
class PrefsUtil(context: Context) { class PrefsUtil(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE) private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE)
@@ -13,26 +14,33 @@ class PrefsUtil(context: Context) {
fun savePassword(password: String) { fun savePassword(password: String) {
val hashedPassword = hashPassword(password) val hashedPassword = hashPassword(password)
prefs.edit() prefs.edit {
.putString("password", hashedPassword) putString("password", hashedPassword)
.apply() }
} }
fun setBoolean(key:String, value: Boolean){ fun setBoolean(key:String, value: Boolean){
return prefs.edit().putBoolean(key,value).apply() return prefs.edit { putBoolean(key, value) }
}
fun setInt(key:String, value: Int){
return prefs.edit { putInt(key, value) }
} }
fun getBoolean(key: String, defValue: Boolean = false): Boolean{ fun getBoolean(key: String, defValue: Boolean = false): Boolean{
return prefs.getBoolean(key,defValue) return prefs.getBoolean(key,defValue)
} }
fun getInt(key: String, defValue: Int): Int{
return prefs.getInt(key,defValue)
}
fun resetPassword(){ fun resetPassword(){
prefs.edit() prefs.edit {
.remove("password") remove("password")
.remove("security_question") .remove("security_question")
.remove("security_answer") .remove("security_answer")
.apply() }
} }
fun validatePassword(input: String): Boolean { fun validatePassword(input: String): Boolean {
@@ -40,11 +48,15 @@ class PrefsUtil(context: Context) {
return stored == hashPassword(input) return stored == hashPassword(input)
} }
fun getPassword(): String{
return prefs.getString("password", "") ?: ""
}
fun saveSecurityQA(question: String, answer: String) { fun saveSecurityQA(question: String, answer: String) {
prefs.edit() prefs.edit {
.putString("security_question", question) putString("security_question", question)
.putString("security_answer", hashPassword(answer)) .putString("security_answer", hashPassword(answer))
.apply() }
} }
fun validateSecurityAnswer(answer: String): Boolean { fun validateSecurityAnswer(answer: String): Boolean {

View File

@@ -5,60 +5,237 @@ import android.net.Uri
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import android.content.SharedPreferences
import androidx.core.content.FileProvider
import devs.org.calculator.database.HiddenFileEntity
import androidx.core.content.edit
class SecurityUtils { object SecurityUtils {
companion object { private const val ALGORITHM = "AES"
private const val ALGORITHM = "AES" private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val HIDDEN_FOLDER = "Calculator_Data" private const val KEY_SIZE = 256
private fun getSecretKey(context: Context): SecretKey {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
val useCustomKey = keyStore.getBoolean("use_custom_key", false)
fun validatePassword(input: String, storedHash: String): Boolean { if (useCustomKey) {
return input.hashCode().toString() == storedHash val customKey = keyStore.getString("custom_key", null)
} if (customKey != null) {
try {
fun encryptFile(context: Context, sourceUri: Uri, password: String): File { val messageDigest = java.security.MessageDigest.getInstance("SHA-256")
val inputStream = context.contentResolver.openInputStream(sourceUri) val keyBytes = messageDigest.digest(customKey.toByteArray())
val secretKey = generateKey(password) return SecretKeySpec(keyBytes, ALGORITHM)
val cipher = Cipher.getInstance(ALGORITHM) } catch (_: Exception) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val hiddenDir = File(context.getExternalFilesDir(null), HIDDEN_FOLDER)
if (!hiddenDir.exists()) hiddenDir.mkdirs()
val encryptedFile = File(hiddenDir, "${System.currentTimeMillis()}_encrypted")
val outputStream = FileOutputStream(encryptedFile)
inputStream?.use { input ->
val buffer = ByteArray(1024)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
val encrypted = cipher.update(buffer, 0, read)
outputStream.write(encrypted)
} }
val finalBlock = cipher.doFinal()
outputStream.write(finalBlock)
} }
outputStream.close()
return encryptedFile
} }
fun decryptFile(file: File, password: String): ByteArray { val encodedKey = keyStore.getString("secret_key", null)
val secretKey = generateKey(password) return if (encodedKey != null) {
val cipher = Cipher.getInstance(ALGORITHM) try {
cipher.init(Cipher.DECRYPT_MODE, secretKey) val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
SecretKeySpec(decodedKey, ALGORITHM)
val inputStream = FileInputStream(file) } catch (_: Exception) {
val bytes = inputStream.readBytes() generateAndStoreNewKey(keyStore)
inputStream.close() }
} else {
return cipher.doFinal(bytes) generateAndStoreNewKey(keyStore)
}
private fun generateKey(password: String): SecretKey {
val keyBytes = password.toByteArray().copyOf(16)
return SecretKeySpec(keyBytes, ALGORITHM)
} }
} }
private fun generateAndStoreNewKey(keyStore: SharedPreferences): SecretKey {
val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
keyGenerator.init(KEY_SIZE, SecureRandom())
val key = keyGenerator.generateKey()
val encodedKey = android.util.Base64.encodeToString(key.encoded, android.util.Base64.DEFAULT)
keyStore.edit { putString("secret_key", encodedKey) }
return key
}
fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try {
if (!inputFile.exists()) {
return false
}
val secretKey = getSecretKey(context)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
FileInputStream(inputFile).use { input ->
FileOutputStream(outputFile).use { output ->
output.write(iv)
CipherOutputStream(output, cipher).use { cipherOutput ->
input.copyTo(cipherOutput)
}
}
}
if (!outputFile.exists() || outputFile.length() == 0L) {
return false
}
FileInputStream(outputFile).use { input ->
val iv = ByteArray(16)
val bytesRead = input.read(iv)
if (bytesRead != 16) {
return false
}
}
true
} catch (_: Exception) {
if (outputFile.exists()) {
outputFile.delete()
}
false
}
}
fun getDecryptedPreviewFile(context: Context, meta: HiddenFileEntity): File? {
try {
val encryptedFile = File(meta.filePath)
if (!encryptedFile.exists()) {
return null
}
val tempDir = File(context.cacheDir, "preview_temp")
if (!tempDir.exists()) tempDir.mkdirs()
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
tempDir.listFiles()?.forEach { it.delete() }
val success = decryptFile(context, encryptedFile, tempFile)
if (success && tempFile.exists() && tempFile.length() > 0) {
return tempFile
} else {
if (tempFile.exists()) tempFile.delete()
return null
}
} catch (_: Exception) {
return null
}
}
fun getUriForPreviewFile(context: Context, file: File): Uri? {
return try {
FileProvider.getUriForFile(
context,
"${context.packageName}.provider", // Must match AndroidManifest
file
)
} catch (_: Exception) {
null
}
}
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try {
if (!inputFile.exists()) {
return false
}
if (inputFile.length() == 0L) {
return false
}
val secretKey = getSecretKey(context)
val cipher = Cipher.getInstance(TRANSFORMATION)
FileInputStream(inputFile).use { input ->
val iv = ByteArray(16)
val bytesRead = input.read(iv)
if (bytesRead != 16) {
return false
}
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
FileInputStream(inputFile).use { decInput ->
decInput.skip(16)
FileOutputStream(outputFile).use { output ->
CipherInputStream(decInput, cipher).use { cipherInput ->
cipherInput.copyTo(output)
}
}
}
}
if (!outputFile.exists() || outputFile.length() == 0L) {
return false
}
true
} catch (_: Exception) {
if (outputFile.exists()) {
outputFile.delete()
}
false
}
}
fun getFileExtension(file: File): String {
val name = file.name
val lastDotIndex = name.lastIndexOf('.')
return if (lastDotIndex > 0) {
name.substring(lastDotIndex)
} else {
""
}
}
fun changeFileExtension(file: File, newExtension: String): File {
val name = file.name
val lastDotIndex = name.lastIndexOf('.')
val newName = if (lastDotIndex > 0) {
name.substring(0, lastDotIndex) + newExtension
} else {
name + newExtension
}
return File(file.parent, newName)
}
fun setCustomKey(context: Context, key: String): Boolean {
return try {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
keyStore.edit {
putString("custom_key", key)
putBoolean("use_custom_key", true)
}
true
} catch (_: Exception) {
false
}
}
fun clearCustomKey(context: Context) {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
keyStore.edit {
remove("custom_key")
putBoolean("use_custom_key", false)
}
}
fun isUsingCustomKey(context: Context): Boolean {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
return keyStore.getBoolean("use_custom_key", false)
}
} }

View File

@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker import androidx.core.content.PermissionChecker
import androidx.core.net.toUri
class StoragePermissionUtil(private val activity: AppCompatActivity) { class StoragePermissionUtil(private val activity: AppCompatActivity) {
@@ -34,7 +35,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
onGranted() onGranted()
} else { } else {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${activity.packageName}") data = "package:${activity.packageName}".toUri()
} }
activity.startActivity(intent) activity.startActivity(intent)
} }

View File

@@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp" android:width="50dp"
android:height="800dp" android:height="50dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

View File

@@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:bottomLeftRadius="15dp" android:bottomRightRadius="15dp"/> <corners android:bottomLeftRadius="15dp" android:bottomRightRadius="15dp"/>
<solid android:color="?attr/cardForegroundColor"/> <solid android:color="?attr/colorSecondaryContainer"/>
</shape> </shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="#CC000000" android:centerColor="#00ffffff" android:endColor="#00ffffff" android:angle="90"/>
</shape>

View File

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

View File

@@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="50dp"
android:height="24dp" android:height="50dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/textColor" android:fillColor="#FF000000"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6H6zM13,9V3.5L18.5,9H13z"/> android:pathData="M13.172,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.828c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828C14.211,2.211 13.702,2 13.172,2zM15,18H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,17.552 15.552,18 15,18zM15,14H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,13.552 15.552,14 15,14zM13,9V3.5L18.5,9H13z"/>
</vector> </vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#757575"
android:pathData="M13.172,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.828c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828C14.211,2.211 13.702,2 13.172,2zM15,18H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,17.552 15.552,18 15,18zM15,14H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,13.552 15.552,14 15,14zM13,9V3.5L18.5,9H13z"/>
</vector>

View File

@@ -58,7 +58,7 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnChangePassword" android:id="@+id/btnChangePassword"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="50dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:padding="12dp" android:padding="12dp"
android:text="@string/change_password" android:text="@string/change_password"

View File

@@ -39,57 +39,38 @@
android:layout_weight="1" android:layout_weight="1"
android:id="@+id/folderName"/> android:id="@+id/folderName"/>
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:src="@drawable/ic_edit" app:icon="@drawable/ic_edit"
app:tint="?attr/colorPrimary" style="@style/Widget.Material3.Button.IconButton"
android:scaleType="fitCenter"
android:padding="9dp"
android:visibility="gone"
android:background="#00000000"
android:id="@+id/edit"/> android:id="@+id/edit"/>
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:id="@+id/delete" android:id="@+id/delete"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:background="#00000000" app:icon="@drawable/ic_delete"
android:padding="9dp" style="@style/Widget.Material3.Button.IconButton" />
android:scaleType="fitCenter"
android:src="@drawable/ic_delete"
app:tint="?attr/colorPrimary"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:id="@+id/menuButton" android:id="@+id/menuButton"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:src="@drawable/ic_more" app:icon="@drawable/ic_more"
app:tint="?attr/colorPrimary" style="@style/Widget.Material3.Button.IconButton"/>
android:scaleType="fitCenter"
android:padding="9dp"
android:visibility="gone"
android:background="#00000000"/>
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:src="@drawable/ic_list" app:icon="@drawable/ic_list"
android:scaleType="fitCenter" style="@style/Widget.Material3.Button.IconButton"
app:tint="?attr/colorPrimary"
android:background="#00000000"
android:padding="8dp"
android:id="@+id/folderOrientation"/> android:id="@+id/folderOrientation"/>
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:src="@drawable/ic_settings" app:icon="@drawable/ic_settings"
android:scaleType="fitCenter" style="@style/Widget.Material3.Button.IconButton"
app:tint="?attr/colorPrimary"
android:background="#00000000"
android:padding="8dp"
android:id="@+id/settings"/> android:id="@+id/settings"/>
</LinearLayout> </LinearLayout>
@@ -128,7 +109,7 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:gravity="center" android:gravity="center"
android:padding="10dp" android:padding="10dp"
android:text="@string/no_items_available_add_one_by_clicking_on_the_plus_button" android:text="@string/there_is_no_folders_available_create_one_by_clicking_on_the_add_folder_button_showing_in_the_bottom"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </LinearLayout>

View File

@@ -14,7 +14,7 @@
android:layout_margin="0dp" android:layout_margin="0dp"
android:background="@drawable/bottom_corner" android:background="@drawable/bottom_corner"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3" app:layout_constraintHeight_percent="0.4"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@@ -23,6 +23,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:scrollbars="none" android:scrollbars="none"
android:paddingTop="27dp"
android:gravity="end|bottom" android:gravity="end|bottom"
app:layout_constraintBottom_toTopOf="@+id/total" app:layout_constraintBottom_toTopOf="@+id/total"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -59,7 +60,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autoSizeMaxTextSize="40sp" android:autoSizeMaxTextSize="40sp"
android:autoSizeMinTextSize="24sp" android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp" android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform" android:autoSizeTextType="uniform"
android:gravity="end|bottom" android:gravity="end|bottom"
@@ -85,7 +86,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/displayContainer"> app:layout_constraintTop_toBottomOf="@id/displayContainer">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
@@ -95,49 +96,68 @@
android:id="@+id/btnClear" android:id="@+id/btnClear"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_marginHorizontal="3dp"
android:layout_weight="1" android:backgroundTint="?attr/colorSecondaryContainer"
android:text="C" android:text="C"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnPercent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnPercent" android:id="@+id/btnPercent"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:backgroundTint="?attr/colorSecondaryContainer"
android:layout_weight="1"
android:text="%" android:text="%"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> android:layout_marginHorizontal="3dp"
app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnDivide"
app:layout_constraintStart_toEndOf="@+id/btnClear"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnDivide" android:id="@+id/btnDivide"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:backgroundTint="?attr/colorSecondaryContainer"
android:layout_weight="1"
android:text="÷" android:text="÷"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> android:layout_marginHorizontal="3dp"
app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/cut"
app:layout_constraintStart_toEndOf="@+id/btnPercent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton <ImageButton
android:id="@+id/cut" android:id="@+id/cut"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:layout_marginVertical="8dp"
android:background="@drawable/gradient_bg" android:background="@drawable/gradient_bg"
android:scaleType="centerInside"
android:src="@drawable/backspace" android:src="@drawable/backspace"
android:scaleType="centerInside"/> android:layout_marginHorizontal="3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnDivide"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
@@ -147,10 +167,16 @@
android:id="@+id/btn7" android:id="@+id/btn7"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="7" android:text="7"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn8"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp" />
@@ -158,21 +184,32 @@
android:id="@+id/btn8" android:id="@+id/btn8"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="8" android:text="8"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn9"
app:layout_constraintStart_toEndOf="@+id/btn7"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn9" android:id="@+id/btn9"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="9" android:text="9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnMultiply"
app:layout_constraintStart_toEndOf="@+id/btn8"
app:layout_constraintTop_toTopOf="parent"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp" />
@@ -180,18 +217,22 @@
android:id="@+id/btnMultiply" android:id="@+id/btnMultiply"
style="@style/CustomMaterialButton" style="@style/CustomMaterialButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1" android:layout_weight="1"
android:textColor="@color/white" android:layout_marginHorizontal="3dp"
android:text="×" android:text="×"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn9"
app:layout_constraintTop_toTopOf="parent"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
@@ -201,51 +242,71 @@
android:id="@+id/btn4" android:id="@+id/btn4"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_marginHorizontal="3dp"
android:layout_weight="1" android:layout_weight="1"
android:text="4" android:text="4"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn5" android:id="@+id/btn5"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_marginHorizontal="3dp"
android:layout_weight="1" android:layout_weight="1"
android:text="5" android:text="5"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn6"
app:layout_constraintStart_toEndOf="@+id/btn4"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn6" android:id="@+id/btn6"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_marginHorizontal="3dp"
android:layout_weight="1" android:layout_weight="1"
android:text="6" android:text="6"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnMinus"
app:layout_constraintStart_toEndOf="@+id/btn5"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnMinus" android:id="@+id/btnMinus"
style="@style/CustomMaterialButton" style="@style/CustomMaterialButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="4dp" android:layout_marginHorizontal="3dp"
android:layout_marginVertical="8dp"
android:layout_weight="1" android:layout_weight="1"
android:textColor="@color/white" android:textColor="@color/white"
android:text="-" android:text="-"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn6"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
@@ -255,92 +316,144 @@
android:id="@+id/btn1" android:id="@+id/btn1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="1" android:text="1"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn2"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn2" android:id="@+id/btn2"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="2" android:text="2"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn3"
app:layout_constraintStart_toEndOf="@+id/btn1"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn3" android:id="@+id/btn3"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:text="3" android:text="3"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnPlus"
app:layout_constraintStart_toEndOf="@+id/btn2"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnPlus" android:id="@+id/btnPlus"
style="@style/CustomMaterialButton" style="@style/CustomMaterialButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1" android:layout_weight="1"
android:textColor="@color/white" android:textColor="@color/white"
android:layout_marginHorizontal="3dp"
android:text="+" android:text="+"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn3"
app:layout_constraintTop_toTopOf="parent"
/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn00"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="3dp"
android:text="00"
android:layout_weight="1"
android:textSize="30sp"
app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btn0"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn0" android:id="@+id/btn0"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_weight="1"
android:layout_weight="2" android:layout_marginHorizontal="3dp"
android:text="0" android:text="0"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnDot"
app:layout_constraintStart_toEndOf="@+id/btn00"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnDot" android:id="@+id/btnDot"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_margin="4dp" android:layout_marginHorizontal="3dp"
android:layout_weight="1"
android:text="." android:text="."
android:layout_weight="1"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toStartOf="@+id/btnEquals"
app:layout_constraintStart_toEndOf="@+id/btn0"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnEquals" android:id="@+id/btnEquals"
style="@style/CustomMaterialButton" style="@style/CustomMaterialButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1" android:layout_weight="1"
android:textColor="@color/white" android:layout_marginHorizontal="3dp"
android:text="=" android:text="="
android:textColor="@color/white"
android:textSize="30sp" android:textSize="30sp"
app:cornerRadius="15dp" /> app:cornerRadius="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnDot"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>

View File

@@ -81,7 +81,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardCornerRadius="10dp" app:cardCornerRadius="10dp"
app:cardElevation="5dp"> android:background="#00000000"
android:backgroundTint="#00000000"
app:cardElevation="0dp">
<ImageView <ImageView
android:id="@+id/appLogo" android:id="@+id/appLogo"
android:layout_width="48dp" android:layout_width="48dp"
@@ -254,6 +256,23 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/restrict_screenshots_in_hidden_section" android:text="@string/restrict_screenshots_in_hidden_section"
android:textAppearance="?attr/textAppearanceBodyLarge" /> android:textAppearance="?attr/textAppearanceBodyLarge" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/encryptionSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/encrypt_file_when_hiding"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/customKeyStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/set_custom_encryption_key"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>

View File

@@ -40,16 +40,12 @@
android:id="@+id/folderName"/> android:id="@+id/folderName"/>
<androidx.appcompat.widget.AppCompatImageButton <com.google.android.material.button.MaterialButton
android:id="@+id/menuButton" android:id="@+id/menuButton"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:src="@drawable/ic_more" app:icon="@drawable/ic_more"
app:tint="?attr/colorPrimary" style="@style/Widget.Material3.Button.IconButton"/>
android:scaleType="fitCenter"
android:padding="7dp"
android:visibility="gone"
android:background="#00000000"/>
</LinearLayout> </LinearLayout>
@@ -79,7 +75,7 @@
<ImageView <ImageView
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/ic_no_items" /> android:src="@drawable/ic_file_no_item" />
<TextView <TextView
android:id="@+id/noItemsTxt" android:id="@+id/noItemsTxt"

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="Enter encryption key"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/keyInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Confirm encryption key"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/confirmKeyInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Note: Make sure to remember your key. If you lose it, you won't be able to decrypt your files."
android:textSize="12sp"
android:textColor="?android:textColorSecondary" />
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<CheckBox
android:id="@+id/checkboxImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Image"
android:padding="8dp" />
<CheckBox
android:id="@+id/checkboxVideo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Video"
android:padding="8dp" />
<CheckBox
android:id="@+id/checkboxAudio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Audio"
android:padding="8dp" />
</LinearLayout>

View File

@@ -10,8 +10,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="4dp" android:layout_margin="4dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp">
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,24 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView 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:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="12dp" android:layout_margin="3dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <LinearLayout
android:id="@+id/folderIcon"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/ic_folder"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/folderName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:padding="12dp"
android:textSize="18sp"/> android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:id="@+id/folderIcon"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/ic_folder"
app:tint="?attr/colorPrimary" />
</LinearLayout> <TextView
android:id="@+id/folderName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="18sp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -15,13 +15,13 @@
<androidx.cardview.widget.CardView <com.google.android.material.card.MaterialCardView
android:id="@+id/cardView" android:id="@+id/cardView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:cardCornerRadius="10dp" app:cardCornerRadius="10dp"
android:layout_margin="5dp" android:layout_margin="5dp"
app:cardElevation="3dp" android:backgroundTint="#404040"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -37,6 +37,11 @@
android:layout_gravity="center" android:layout_gravity="center"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/add_image" /> android:src="@drawable/add_image" />
<View
android:id="@+id/shade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bottom_shade"/>
<LinearLayout <LinearLayout
android:id="@+id/selectedLayer" android:id="@+id/selectedLayer"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -49,7 +54,21 @@
</androidx.cardview.widget.CardView> </com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/fileNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingHorizontal="13dp"
android:paddingVertical="6dp"
android:textColor="#fff"
android:textSize="10sp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@+id/cardView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<ImageView <ImageView
@@ -60,6 +79,13 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_play_circle" android:src="@drawable/ic_play_circle"
android:layout_gravity="center"/> android:layout_gravity="center"/>
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
android:src="@drawable/encrypted"
android:layout_gravity="end"
android:id="@+id/encrypted"/>
<ImageView <ImageView
android:id="@+id/selected" android:id="@+id/selected"
android:layout_width="20dp" android:layout_width="20dp"
@@ -70,16 +96,7 @@
android:src="@drawable/selected" android:src="@drawable/selected"
android:background="@drawable/gradient_bg" android:background="@drawable/gradient_bg"
android:layout_gravity="end"/> android:layout_gravity="end"/>
</FrameLayout> </FrameLayout>
<TextView
android:id="@+id/fileNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="5dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout> </LinearLayout>

View File

@@ -42,7 +42,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
app:cardCornerRadius="15dp" app:cardCornerRadius="15dp"
app:cardElevation="10dp"
android:layout_margin="20dp"> android:layout_margin="20dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -11,6 +11,7 @@
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item> <item name="android:windowLightStatusBar">false</item>
<item name="colorSecondaryContainer">#481A4324</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item> <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
</style> </style>

View File

@@ -24,7 +24,7 @@
<string name="preview_videos">Preview Videos</string> <string name="preview_videos">Preview Videos</string>
<string name="preview_audios">Preview Audios</string> <string name="preview_audios">Preview Audios</string>
<string name="preview_documents">Preview Documents</string> <string name="preview_documents">Preview Documents</string>
<string name="delete_file">Delete File</string> <string name="delete_file">Delete File!</string>
<string name="are_you_sure_to_delete_this_file_permanently">Are you sure to Delete this file permanently?</string> <string name="are_you_sure_to_delete_this_file_permanently">Are you sure to Delete this file permanently?</string>
<string name="delete_permanently">Delete Permanently</string> <string name="delete_permanently">Delete Permanently</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
@@ -64,7 +64,7 @@
<string name="unknown_file">Unknown File</string> <string name="unknown_file">Unknown File</string>
<string name="details"> DETAILS</string> <string name="details"> DETAILS</string>
<string name="audio_hidded_successfully">Audios hidden successfully</string> <string name="audio_hidded_successfully">Audios hidden successfully</string>
<string name="no_items_available_add_one_by_clicking_on_the_plus_button">No Items Available, Add one by clicking on the</string> <string name="no_items_available_add_one_by_clicking_on_the_plus_button">No Files Available, Add one by clicking on the \'+\' button</string>
<string name="now_enter_button">Now Enter \'=\' button</string> <string name="now_enter_button">Now Enter \'=\' button</string>
<string name="enter_123456">Enter 123456</string> <string name="enter_123456">Enter 123456</string>
<string name="create_folder">Create Folder</string> <string name="create_folder">Create Folder</string>
@@ -79,6 +79,7 @@
<string name="error_loading_files">Error loading files</string> <string name="error_loading_files">Error loading files</string>
<string name="delete_items">Delete Folder</string> <string name="delete_items">Delete Folder</string>
<string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected Folders?</string> <string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected Folders?</string>
<string name="folder_will_be_deleted_permanently">Note: If you delete the folder(S) all the files available inside the folder will be deleted !</string>
<string name="items_deleted_successfully">Items deleted successfully</string> <string name="items_deleted_successfully">Items deleted successfully</string>
<string name="folder_deleted_successfully">Folder deleted successfully</string> <string name="folder_deleted_successfully">Folder deleted successfully</string>
<string name="some_items_could_not_be_deleted">Some items could not be deleted</string> <string name="some_items_could_not_be_deleted">Some items could not be deleted</string>
@@ -107,8 +108,8 @@
<string name="are_you_sure_you_want_to_un_hide_selected_files">Are you sure you want to unhide the selected files?</string> <string name="are_you_sure_you_want_to_un_hide_selected_files">Are you sure you want to unhide the selected files?</string>
<string name="files_unhidden_successfully">Files unhidden successfully</string> <string name="files_unhidden_successfully">Files unhidden successfully</string>
<string name="some_files_could_not_be_unhidden">Some files could not be unhidden</string> <string name="some_files_could_not_be_unhidden">Some files could not be unhidden</string>
<string name="copy_to_another_folder">Copy to Another Folder</string> <string name="copy_to_another_folder">Copy</string>
<string name="move_to_another_folder">Move to Another Folder</string> <string name="move_to_another_folder">Move</string>
<string name="select_destination_folder">Select Destination Folder</string> <string name="select_destination_folder">Select Destination Folder</string>
<string name="no_folders_available">No other folders available</string> <string name="no_folders_available">No other folders available</string>
<string name="change_password">Change Password</string> <string name="change_password">Change Password</string>
@@ -120,10 +121,59 @@
<string name="app_settings">App Settings</string> <string name="app_settings">App Settings</string>
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string> <string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
<string name="security_settings">Security Settings</string> <string name="security_settings">Security Settings</string>
<string name="version">Version 1.3</string> <string name="version">Version 1.4.0</string>
<string name="app_details">App Details</string> <string name="app_details">App Details</string>
<string name="setup_password">Setup Password</string> <string name="setup_password">Setup Password</string>
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string> <string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
<string name="security_answer">Security Answer</string> <string name="security_answer">Security Answer</string>
<string name="save_password">Save Password</string> <string name="save_password">Save Password</string>
<string name="there_is_no_folders_available_create_one_by_clicking_on_the_add_folder_button_showing_in_the_bottom">There is no Folders Available, create one by clicking on the \'Add Folder\' Button showing in the bottom.</string>
<string name="storage_permission">Storage Permission</string>
<string name="to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission">To ensure the app works properly and allows you to easily hide or un-hide your private files, please grant storage access permission.\n</string>
<string name="for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission">For devices running Android 11 or higher, you\'ll need to grant the \'All Files Access\' permission.</string>
<string name="grant_permission">Grant Permission</string>
<string name="later">Later</string>
<string name="storage_permission_is_required_for_the_app_to_function_properly">Storage permission is required for the app to function properly</string>
<string name="you_can_grant_permission_later_from_settings">You can grant permission later from Settings</string>
<string name="permission_granted">Permission granted</string>
<string name="permission_denied">Permission denied</string>
<string name="github_profile">https://github.com/binondi</string>
<string name="calculator_hide_files">%1$s/calculator-hide-files</string>
<string name="would_you_like_to_set_a_specific_theme_mode">Would you like to set a specific theme mode?</string>
<string name="no">No</string>
<string name="could_not_open_url">Could not open URL</string>
<string name="files_copied_successfully">Files copied successfully</string>
<string name="some_files_could_not_be_copied">Some files could not be copied</string>
<string name="files_moved_successfully">Files moved successfully</string>
<string name="some_files_could_not_be_moved">Some files could not be moved</string>
<string name="ok">OK</string>
<string name="if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app">If you turn on/off this option, dynamic theme changes will be visible after you reopen the app.</string>
<string name="attention">Attention!</string>
<string name="are_you_sure_to_delete_selected_files_permanently">Are you sure to delete selected files permanently ?</string>
<string name="encrypt_file_when_hiding">Encrypt File When Hiding</string>
<string name="encrypt_file">Encrypt File</string>
<string name="decrypt_file">Decrypt File</string>
<string name="encrypt_files">Encrypt Files</string>
<string name="decrypt_files">Decrypt Files</string>
<string name="encrypt">Encrypt</string>
<string name="decrypt">Decrypt</string>
<string name="encryption_disclaimer">Warning: This will encrypt the selected files. Please use a custom encryption key from Settings, you can only decrypt the encrypted files with the same key so if you use default key and uninstall the app without decrypting the file you cannot decrypt the file later!. Do you want to continue?</string>
<string name="decryption_disclaimer">Warning: This will decrypt the selected files. The encrypted files will be deleted after successful decryption. Make sure to do this. Do you want to continue?</string>
<string name="custom_encryption_key_cleared">Custom encryption key cleared</string>
<string name="failed_to_set_custom_key">Failed to set custom key</string>
<string name="custom_key_set_successfully">Custom key set successfully</string>
<string name="keys_do_not_match">Keys do not match</string>
<string name="key_cannot_be_empty">Key cannot be empty</string>
<string name="set_custom_encryption_key">Set Custom Encryption Key</string>
<string name="set">Set</string>
<string name="delete_key">Delete Key</string>
<string name="enter_folder_name_to_create">Enter Folder Name To Create</string>
<string name="please_select_exactly_one_folder_to_edit">Please select exactly one folder to edit</string>
<string name="invalid_folder_name">Invalid folder name</string>
<string name="folder_with_this_name_already_exists">Folder with this name already exists</string>
<string name="failed_to_rename_folder">Failed to rename folder</string>
<string name="select_file_type">Select File Type!</string>
<string name="please_select_the_type_of_file_to_decrypt">Please select the type of file to decrypt</string>
<string name="file_no_longer_exists">File no longer exists</string>
<string name="something_went_wrong">Something went wrong !</string>
</resources> </resources>

View File

@@ -12,6 +12,7 @@
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="colorControlNormal">#483CFF61</item> <item name="colorControlNormal">#483CFF61</item>
<item name="colorSecondaryContainer">#48BDFFCB</item>
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item> <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>

View File

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

View File

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