59 Commits

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

View File

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

2
.idea/kotlinc.xml generated
View File

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

View File

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

View File

@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
}
android {
@@ -10,9 +11,10 @@ android {
defaultConfig {
applicationId = "devs.org.calculator"
minSdk = 26
//noinspection OldTargetApi
targetSdk = 34
versionCode = 4
versionName = "1.3"
versionCode = 7
versionName = "1.4.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -68,6 +70,7 @@ dependencies {
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.gridlayout)
implementation(libs.androidx.lifecycle.process)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -80,4 +83,11 @@ dependencies {
implementation(libs.androidx.viewpager)
implementation(libs.zoomage)
implementation(libs.lottie)
implementation(libs.androidx.swiperefreshlayout)
// Room dependencies
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
//noinspection KaptUsageInsteadOfKsp
kapt(libs.androidx.room.compiler)
}

View File

@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "devs.org.calculator",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 6,
"versionName": "1.4.1",
"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
android:name="androidx.core.content.FileProvider"
android:authorities="devs.org.calculator.fileprovider"
android:authorities="devs.org.calculator.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<meta-data
android:name="android.app.icon"
android:resource="@mipmap/ic_launcher_monochrome" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

View File

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

View File

@@ -5,14 +5,16 @@ import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
@@ -36,21 +38,24 @@ class HiddenActivity : AppCompatActivity() {
private var folderAdapter: FolderAdapter? = null
private var listFolderAdapter: ListFolderAdapter? = null
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
private const val TAG = "HiddenActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHiddenBinding.inflate(layoutInflater)
setContentView(binding.root)
fileManager = FileManager(this, this)
folderManager = FolderManager(this)
folderManager = FolderManager()
dialogUtil = DialogUtil(this)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupInitialUIState()
setupClickListeners()
@@ -100,22 +105,26 @@ class HiddenActivity : AppCompatActivity() {
}
binding.folderOrientation.setOnClickListener {
// Switch between grid mode and list mode
val currentIsList = PrefsUtil(this).getBoolean("isList", false)
val newIsList = !currentIsList
if (newIsList) {
// Switch to list view
showListUI()
PrefsUtil(this).setBoolean("isList", true)
binding.folderOrientation.setImageResource(R.drawable.ic_grid)
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
} else {
// Switch to grid view
showGridUI()
PrefsUtil(this).setBoolean("isList", false)
binding.folderOrientation.setImageResource(R.drawable.ic_list)
binding.folderOrientation.setIconResource(R.drawable.ic_list)
}
}
binding.random.setOnClickListener {
val folders = folderManager.getFoldersInDirectory(hiddenDir)
val randomIndex = (Math.random() * folders.size).toInt()
val folder = folders[randomIndex]
startActivity(Intent(this,ViewFolderActivity::class.java).putExtra("folder",folder.toString()))
}
}
private fun showGridUI() {
@@ -141,11 +150,10 @@ class HiddenActivity : AppCompatActivity() {
showEmptyState()
}
} else {
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
showEmptyState()
}
} catch (e: Exception) {
Log.e(TAG, "Error listing folders: ${e.message}")
} catch (_: Exception) {
showEmptyState()
}
}
@@ -164,26 +172,25 @@ class HiddenActivity : AppCompatActivity() {
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
MaterialAlertDialogBuilder(this)
.setTitle("Enter Folder Name To Create")
.setTitle(getString(R.string.enter_folder_name_to_create))
.setView(dialogView)
.setPositiveButton("Create") { dialog, _ ->
.setPositiveButton(getString(R.string.create)) { dialog, _ ->
val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty()) {
try {
folderManager.createFolder(hiddenDir, newName)
refreshCurrentView()
} catch (e: Exception) {
Log.e(TAG, "Error creating folder: ${e.message}")
} catch (_: Exception) {
Toast.makeText(
this@HiddenActivity,
"Failed to create folder",
getString(R.string.failed_to_create_folder),
Toast.LENGTH_SHORT
).show()
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.show()
@@ -195,7 +202,6 @@ class HiddenActivity : AppCompatActivity() {
}
private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
@@ -220,11 +226,9 @@ class HiddenActivity : AppCompatActivity() {
showEmptyState()
}
} else {
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
showEmptyState()
}
} catch (e: Exception) {
Log.e(TAG, "Error listing folders: ${e.message}")
} catch (_: Exception) {
showEmptyState()
}
}
@@ -232,8 +236,6 @@ class HiddenActivity : AppCompatActivity() {
private fun showFolderList(folders: List<File>) {
binding.noItems.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
// Clear the existing adapter to avoid conflicts
listFolderAdapter = null
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
@@ -247,14 +249,13 @@ class HiddenActivity : AppCompatActivity() {
onSelectionModeChanged = { isSelectionMode ->
handleFolderSelectionModeChange(isSelectionMode)
},
onSelectionCountChanged = { selectedCount ->
onSelectionCountChanged = { _ ->
updateEditButtonVisibility()
}
)
binding.recyclerView.adapter = folderAdapter
folderAdapter?.submitList(folders)
// Ensure proper icon state for folder view
if (folderAdapter?.isInSelectionMode() != true) {
showFolderViewIcons()
}
@@ -262,8 +263,6 @@ class HiddenActivity : AppCompatActivity() {
private fun showFolderListStyle(folders: List<File>) {
binding.noItems.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
// Clear the existing adapter to avoid conflicts
folderAdapter = null
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
@@ -277,14 +276,13 @@ class HiddenActivity : AppCompatActivity() {
onSelectionModeChanged = { isSelectionMode ->
handleFolderSelectionModeChange(isSelectionMode)
},
onSelectionCountChanged = { selectedCount ->
onSelectionCountChanged = { _ ->
updateEditButtonVisibility()
}
)
binding.recyclerView.adapter = listFolderAdapter
listFolderAdapter?.submitList(folders)
// Ensure proper icon state for folder view
if (listFolderAdapter?.isInSelectionMode() != true) {
showFolderViewIcons()
}
@@ -312,10 +310,10 @@ class HiddenActivity : AppCompatActivity() {
private fun refreshCurrentView() {
val isList = PrefsUtil(this).getBoolean("isList", false)
if (isList) {
binding.folderOrientation.setImageResource(R.drawable.ic_grid)
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
listFoldersInHiddenDirectoryListStyle()
} else {
binding.folderOrientation.setImageResource(R.drawable.ic_list)
binding.folderOrientation.setIconResource(R.drawable.ic_list)
listFoldersInHiddenDirectory()
}
}
@@ -335,7 +333,7 @@ class HiddenActivity : AppCompatActivity() {
if (selectedFolders.isNotEmpty()) {
dialogUtil.showMaterialDialog(
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.cancel),
object : DialogUtil.DialogCallback {
@@ -344,11 +342,11 @@ class HiddenActivity : AppCompatActivity() {
}
override fun onNegativeButtonClicked() {
// Do nothing
}
override fun onNaturalButtonClicked() {
// Do nothing
}
}
)
@@ -360,7 +358,6 @@ class HiddenActivity : AppCompatActivity() {
selectedFolders.forEach { folder ->
if (!folderManager.deleteFolder(folder)) {
allDeleted = false
Log.e(TAG, "Failed to delete folder: ${folder.name}")
}
}
@@ -372,26 +369,20 @@ class HiddenActivity : AppCompatActivity() {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Clear selection from both adapters
folderAdapter?.clearSelection()
listFolderAdapter?.clearSelection()
// This will trigger the selection mode change callback and show proper icons
exitFolderSelectionMode()
// Refresh the current view based on orientation
refreshCurrentView()
}
private fun handleBackPress() {
// Check if folder adapters are in selection mode
if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
return
}
// Handle navigation back
if (currentFolder != null) {
navigateBackToFolders()
} else {
@@ -402,26 +393,20 @@ class HiddenActivity : AppCompatActivity() {
private fun navigateBackToFolders() {
currentFolder = null
// Clean up file adapter
refreshCurrentView()
binding.folderName.text = getString(R.string.hidden_space)
// Set proper icons for folder view
showFolderViewIcons()
}
override fun onDestroy() {
super.onDestroy()
// Remove any pending callbacks
mainHandler.removeCallbacksAndMessages(null)
}
//visibility related code
private fun showFolderViewIcons() {
binding.random.visibility = View.VISIBLE
binding.folderOrientation.visibility = View.VISIBLE
binding.settings.visibility = View.VISIBLE
binding.delete.visibility = View.GONE
@@ -429,21 +414,19 @@ class HiddenActivity : AppCompatActivity() {
binding.menuButton.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE
binding.edit.visibility = View.GONE
// Ensure FABs are properly managed
if (currentFolder == null) {
binding.addFolder.visibility = View.VISIBLE
}
}
private fun showFolderSelectionIcons() {
binding.random.visibility = View.GONE
binding.folderOrientation.visibility = View.GONE
binding.settings.visibility = View.GONE
binding.delete.visibility = View.VISIBLE
binding.deleteSelected.visibility = View.VISIBLE
binding.menuButton.visibility = View.GONE
binding.addFolder.visibility = View.GONE
// Update edit button visibility based on current selection count
updateEditButtonVisibility()
}
private fun exitFolderSelectionMode() {
@@ -456,7 +439,6 @@ class HiddenActivity : AppCompatActivity() {
} else {
enterFolderSelectionMode()
}
// Always update edit button visibility when selection mode changes
updateEditButtonVisibility()
}
@@ -468,7 +450,8 @@ class HiddenActivity : AppCompatActivity() {
}
if (selectedFolders.size != 1) {
Toast.makeText(this, "Please select exactly one folder to edit", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.please_select_exactly_one_folder_to_edit), Toast.LENGTH_SHORT).show()
return
}
@@ -483,20 +466,21 @@ class HiddenActivity : AppCompatActivity() {
inputEditText.selectAll()
MaterialAlertDialogBuilder(this)
.setTitle("Rename Folder")
.setTitle(getString(R.string.rename_folder))
.setView(dialogView)
.setPositiveButton("Rename") { dialog, _ ->
.setPositiveButton(getString(R.string.rename)) { dialog, _ ->
val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != folder.name) {
if (isValidFolderName(newName)) {
renameFolder(folder, newName)
} else {
Toast.makeText(this, "Invalid folder name", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.invalid_folder_name), Toast.LENGTH_SHORT).show()
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
.show()
@@ -515,21 +499,19 @@ class HiddenActivity : AppCompatActivity() {
if (parentDir != null) {
val newFolder = File(parentDir, newName)
if (newFolder.exists()) {
Toast.makeText(this, "Folder with this name already exists", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.folder_with_this_name_already_exists), Toast.LENGTH_SHORT).show()
return
}
if (oldFolder.renameTo(newFolder)) {
// Clear selection from both adapters
folderAdapter?.clearSelection()
listFolderAdapter?.clearSelection()
// Exit selection mode
exitFolderSelectionMode()
refreshCurrentView()
} else {
Toast.makeText(this, "Failed to rename folder", Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.failed_to_rename_folder), Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -9,6 +9,7 @@ import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
@@ -22,6 +23,11 @@ import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder
import java.util.regex.Pattern
import androidx.core.content.edit
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.utils.StoragePermissionUtil
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
private lateinit var binding: ActivityMainBinding
@@ -34,50 +40,72 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
private val dialogUtil = DialogUtil(this)
private val fileManager = FileManager(this, this)
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
private lateinit var storagePermissionUtil: StoragePermissionUtil
private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, 0, systemBars.right, systemBars.bottom)
insets
}
permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
storagePermissionUtil.handlePermissionResult(permissions)
}
// Initialize StoragePermissionUtil
storagePermissionUtil = StoragePermissionUtil(this)
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(result)
}
if (sp.getBoolean("isFirst", true)){
binding.display.text = getString(R.string.enter_123456)
}
// Ask permission
if(!Environment.isExternalStorageManager()) {
if (!storagePermissionUtil.hasStoragePermission()) {
dialogUtil.showMaterialDialog(
"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.storage_permission),
getString(R.string.to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission) +
"\n" +
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.",
"Grant",
"Later",
getString(R.string.for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission),
getString(R.string.grant_permission),
getString(R.string.later),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
fileManager.askPermission(this@MainActivity)
storagePermissionUtil.requestStoragePermission(permissionLauncher) {
Toast.makeText(this@MainActivity, getString(R.string.permission_granted), Toast.LENGTH_SHORT).show()
}
}
override fun onNegativeButtonClicked() {
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()
}
override fun onNaturalButtonClicked() {
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()
}
}
)
})
}
setupNumberButton(binding.btn0, "0")
setupNumberButton(binding.btn00, "00")
setupNumberButton(binding.btn1, "1")
setupNumberButton(binding.btn2, "2")
setupNumberButton(binding.btn3, "3")
@@ -99,6 +127,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
binding.cut.setOnClickListener { cutNumbers() }
}
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
@@ -107,10 +136,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
contentResolver.takePersistableUriPermission(uri, takeFlags)
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")
}
}
@@ -173,41 +200,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 {
val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%")
val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%")
var processedExpression = expression
// Replace standalone percentages (like "50%") with their decimal form (0.5)
val matcher = percentagePattern.matcher(processedExpression)
while (matcher.find()) {
val fullMatch = matcher.group(0)
val number = matcher.group(1)
// Check if it's a standalone percentage or part of an operation
val start = matcher.start()
if (start == 0 || !isOperator(processedExpression[start-1].toString())) {
val percentageValue = number.toDouble() / 100
processedExpression = processedExpression.replace(fullMatch, percentageValue.toString())
val percentageValue = number!!.toDouble() / 100
processedExpression = processedExpression.replace(fullMatch!!.toString(), percentageValue.toString())
}
}
// Handle operator-percentage combinations (like "100-20%")
val opMatcher = operatorPercentPattern.matcher(processedExpression)
val sb = StringBuilder(processedExpression)
// We need to process matches from right to left to maintain indices
val matches = mutableListOf<Triple<Int, Int, String>>()
while (opMatcher.find()) {
@@ -219,15 +229,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
matches.add(Triple(start, end, "$operator$percentValue%"))
}
// Process matches from right to left
for (match in matches.reversed()) {
val (start, end, fullMatch) = match
// Find the number before this operator
var leftNumberEnd = start
var leftNumberStart = start - 1
// Skip parentheses and move to the actual number
if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') {
var openParens = 1
leftNumberStart--
@@ -238,7 +244,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
leftNumberStart--
}
// Now we need to find the start of the expression
if (leftNumberStart >= 0) {
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) {
leftNumberStart--
@@ -248,26 +253,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
leftNumberStart = 0
}
} else {
// For simple numbers, just find the start of the number
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) {
leftNumberStart--
}
leftNumberStart++
}
if (leftNumberStart < leftNumberEnd) {
val leftPart = sb.substring(leftNumberStart, leftNumberEnd)
if (leftNumberStart < start) {
val leftPart = sb.substring(leftNumberStart, start)
try {
// Extract the numerical values
val baseNumber = evaluateExpression(leftPart)
val operator = fullMatch.substring(0, 1)
val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble()
// Calculate the percentage of the base number
val percentValue = baseNumber * (percentNumber / 100)
// Calculate the new value based on the operator
val newValue = when (operator) {
"+" -> baseNumber + percentValue
"-" -> baseNumber - percentValue
@@ -276,7 +279,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
else -> baseNumber
}
// Replace the entire expression "number operator percent%" with the result
sb.replace(leftNumberStart, end, newValue.toString())
} catch (e: Exception) {
Log.e("Calculator", "Error processing percentage expression: $e")
@@ -298,15 +300,16 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
private fun evaluateExpression(expression: String): Double {
return try {
ExpressionBuilder(expression).build().evaluate()
} catch (e: Exception) {
} catch (_: Exception) {
expression.toDouble()
}
}
@SuppressLint("DefaultLocale")
private fun calculateResult() {
if (currentExpression == "123456") {
val intent = Intent(this, SetupPasswordActivity::class.java)
sp.edit().putBoolean("isFirst", false).apply()
sp.edit { putBoolean("isFirst", false) }
intent.putExtra("password", currentExpression)
startActivity(intent)
clearDisplay()
@@ -358,15 +361,17 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
}
try {
if (currentExpression.isEmpty() ||
(isOperator(currentExpression.last().toString()) && currentExpression.last() != '%')) {
if (currentExpression.isEmpty()) {
binding.total.text = ""
return
}
// Process the expression for preview calculation
var processedExpression = currentExpression.replace("×", "*")
if (isOperator(processedExpression.last().toString())) {
processedExpression = processedExpression.substring(0, processedExpression.length - 1)
}
if (processedExpression.contains("%")) {
processedExpression = preprocessExpression(processedExpression)
}
@@ -382,7 +387,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
binding.total.text = getString(R.string.now_enter_button)
}else binding.total.text = formattedResult
} catch (e: Exception) {
} catch (_: Exception) {
binding.total.text = ""
}
}
@@ -395,7 +400,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
val lastChar = currentExpression.last()
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
// Update flags based on what was removed
if (lastChar == '%') {
lastWasPercent = false
} else if (isOperator(lastChar.toString())) {
@@ -411,27 +415,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
}
override fun onPositiveButtonClicked() {
// Handle positive button click for both DialogUtil and DialogActionsCallback
fileManager.askPermission(this)
}
override fun onNegativeButtonClicked() {
// Handle negative button click
Toast.makeText(this, "Storage permission is required for the app to function properly", Toast.LENGTH_LONG).show()
Toast.makeText(this, getString(R.string.storage_permission_is_required_for_the_app_to_function_properly), Toast.LENGTH_LONG).show()
}
override fun onNaturalButtonClicked() {
// Handle neutral button click
Toast.makeText(this, "You can grant permission later from Settings", Toast.LENGTH_LONG).show()
Toast.makeText(this, getString(R.string.you_can_grant_permission_later_from_settings), Toast.LENGTH_LONG).show()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 6767) {
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 {
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,27 @@ package devs.org.calculator.activities
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import devs.org.calculator.R
import devs.org.calculator.adapters.ImagePreviewAdapter
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ActivityPreviewBinding
import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.launch
import java.io.File
@@ -18,43 +30,56 @@ class PreviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityPreviewBinding
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 folder: String
private lateinit var filetype: FileManager.FileType
private lateinit var adapter: ImagePreviewAdapter
private lateinit var fileManager: FileManager
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())
}
private var slideshowRunning = false
private val slideshowDelay = 2000L
private val slideshowRunner = object : Runnable {
override fun run() {
if (slideshowRunning) {
if (currentPosition + 1 >= files.size) {
currentPosition = 0
} else {
currentPosition += 1
}
binding.viewPager.setCurrentItem(currentPosition, true)
mainHandler.postDelayed(this, slideshowDelay)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPreviewBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
fileManager = FileManager(this, this)
currentPosition = intent.getIntExtra("position", 0)
type = intent.getStringExtra("type").toString()
folder = intent.getStringExtra("folder").toString()
type = intent.getStringExtra("type") ?: "IMAGE"
folder = intent.getStringExtra("folder") ?: ""
setupFileType()
files = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
loadFiles()
setupImagePreview()
clickListeners()
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
}
})
binding.back.setOnClickListener {
finish()
}
setupClickListeners()
setupPageChangeCallback()
}
override fun onResume() {
@@ -62,8 +87,17 @@ class PreviewActivity : AppCompatActivity() {
setupFlagSecure()
}
override fun onPause() {
super.onPause()
adapter.releaseAllResources()
}
override fun onDestroy() {
super.onDestroy()
adapter.releaseAllResources()
}
private fun setupFlagSecure() {
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
@@ -78,14 +112,17 @@ class PreviewActivity : AppCompatActivity() {
filetype = FileManager.FileType.IMAGE
binding.title.text = getString(R.string.preview_images)
}
"VIDEO" -> {
filetype = FileManager.FileType.VIDEO
binding.title.text = getString(R.string.preview_videos)
}
"AUDIO" -> {
filetype = FileManager.FileType.AUDIO
binding.title.text = getString(R.string.preview_audios)
}
else -> {
filetype = FileManager.FileType.DOCUMENT
binding.title.text = getString(R.string.preview_documents)
@@ -93,108 +130,276 @@ 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() {
if (files.isEmpty()) {
finish()
return
}
adapter = ImagePreviewAdapter(this, this)
adapter.images = files
adapter.onSlideshowShouldStop = {
if (slideshowRunning) {
stopSlideshow()
}
}
binding.viewPager.adapter = adapter
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()
if (currentPosition < files.size) {
binding.viewPager.setCurrentItem(currentPosition, false)
}
updateFileInfo()
}
override fun onDestroy() {
super.onDestroy()
if (filetype == FileManager.FileType.AUDIO) {
(binding.viewPager.adapter as? ImagePreviewAdapter)?.let { adapter ->
adapter.currentMediaPlayer?.release()
adapter.currentMediaPlayer = null
private fun setupPageChangeCallback() {
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentPosition = position
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 {
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem])
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
}
}
)
}
handleDeleteFile()
}
binding.unHide.setOnClickListener {
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem])
if (fileUri != null) {
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 {
FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri)
removeFileFromList(binding.viewPager.currentItem)
}
}
override fun onNegativeButtonClicked() {
// Handle negative button click
}
override fun onNaturalButtonClicked() {
// Handle neutral button click
}
}
)
performFileUnHiding()
}
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
adapter.onItemScrolledAway(currentPosition)
currentPosition = position
}
})
if (filetype != FileManager.FileType.IMAGE) {
binding.startSlideshow.visibility = View.GONE
}
binding.startSlideshow.setOnClickListener {
startSlideshow()
}
}
private fun startSlideshow() {
if (!slideshowRunning) {
slideshowRunning = true
toggleToolbars()
mainHandler.postDelayed(slideshowRunner, slideshowDelay)
adapter.setImageZoomable(false)
}
}
private fun stopSlideshow() {
slideshowRunning = false
toggleToolbars()
mainHandler.removeCallbacks(slideshowRunner)
adapter.setImageZoomable(true)
}
private fun toggleToolbars() {
val visibility = if (slideshowRunning) View.GONE else View.VISIBLE
binding.toolbar.visibility = visibility
binding.footer.visibility = visibility
}
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() {}
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) {
val updatedFiles = files.toMutableList().apply { removeAt(position) }
files = updatedFiles
adapter.images = updatedFiles // Update adapter with the new list
// Update the ViewPager's position
if (updatedFiles.isEmpty()) finish()
if (position < 0 || position >= files.size) return
adapter.releaseAllResources()
files.removeAt(position)
adapter.images = files
if (files.isEmpty()) {
finish()
return
}
currentPosition = if (position >= files.size) {
files.size - 1
} else {
position
}
binding.viewPager.setCurrentItem(currentPosition, false)
updateFileInfo()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
finish()
return true
}
}
}

View File

@@ -0,0 +1,43 @@
package devs.org.calculator.activities
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
abstract class SecureBaseActivity : AppCompatActivity() {
class AppLifecycleObserver : DefaultLifecycleObserver {
companion object {
var wasInBackground = false
}
override fun onStart(owner: LifecycleOwner) {
wasInBackground = true
}
override fun onStop(owner: LifecycleOwner) {
wasInBackground = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
}
override fun onResume() {
super.onResume()
if (AppLifecycleObserver.wasInBackground) {
AppLifecycleObserver.wasInBackground = false
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
finish()
}
}
}

View File

@@ -1,36 +1,45 @@
package devs.org.calculator.activities
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import devs.org.calculator.R
import devs.org.calculator.databinding.ActivitySettingsBinding
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
private lateinit var prefs: SharedPreferences
private val DEV_GITHUB_URL = "https://github.com/binondi"
private val GITHUB_URL = "$DEV_GITHUB_URL/calculator-hide-files"
private lateinit var prefs: PrefsUtil
private var DEV_GITHUB_URL = ""
private var GITHUB_URL = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
prefs = PrefsUtil(this)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
DEV_GITHUB_URL = getString(R.string.github_profile)
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
setupUI()
loadSettings()
setupListeners()
@@ -42,6 +51,7 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun loadSettings() {
binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
@@ -55,8 +65,11 @@ class SettingsActivity : AppCompatActivity() {
else -> binding.systemThemeRadio.isChecked = true
}
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
binding.customKeyStatus.isChecked = isUsingCustomKey
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
updateThemeModeVisibility()
}
@@ -72,11 +85,19 @@ class SettingsActivity : AppCompatActivity() {
}
binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("dynamic_theme", isChecked).apply()
prefs.setBoolean("dynamic_theme", isChecked)
if (!isChecked) {
showThemeModeDialog()
}else{
showThemeModeDialog()
if (!prefs.getBoolean("isAppReopened",false)){
DynamicColors.applyToActivityIfAvailable(this)
}
}
}
binding.encryptionSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.setBoolean("encryption", isChecked)
}
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
@@ -97,7 +118,7 @@ class SettingsActivity : AppCompatActivity() {
}
binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("screenshot_restriction", isChecked).apply()
prefs.setBoolean("screenshot_restriction", isChecked)
if (isChecked) {
enableScreenshotRestriction()
} else {
@@ -105,7 +126,11 @@ class SettingsActivity : AppCompatActivity() {
}
}
binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean("showFileName", isChecked).apply()
prefs.setBoolean("showFileName", isChecked)
}
binding.customKeyStatus.setOnClickListener {
showCustomKeyDialog()
}
}
@@ -115,20 +140,16 @@ class SettingsActivity : AppCompatActivity() {
private fun showThemeModeDialog() {
MaterialAlertDialogBuilder(this)
.setTitle("Theme Mode")
.setMessage("Would you like to set a specific theme mode?")
.setPositiveButton("Yes") { _, _ ->
binding.themeModeSwitch.isChecked = true
}
.setNegativeButton("No") { _, _ ->
binding.systemThemeRadio.isChecked = true
applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
.setTitle(getString(R.string.attention))
.setMessage(getString(R.string.if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app))
.setPositiveButton(getString(R.string.ok)) { _, _ ->
}
.show()
}
private fun applyThemeMode(themeMode: Int) {
prefs.edit().putInt("theme_mode", themeMode).apply()
prefs.setInt("theme_mode", themeMode)
AppCompatDelegate.setDefaultNightMode(themeMode)
}
@@ -145,10 +166,64 @@ class SettingsActivity : AppCompatActivity() {
private fun openUrl(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
} 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

@@ -5,7 +5,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import devs.org.calculator.databinding.ActivitySetupPasswordBinding
@@ -18,13 +22,19 @@ class SetupPasswordActivity : AppCompatActivity() {
private lateinit var binding2: ActivityChangePasswordBinding
private lateinit var prefsUtil: PrefsUtil
private var hasPassword = false
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySetupPasswordBinding.inflate(layoutInflater)
binding2 = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
prefsUtil = PrefsUtil(this)
hasPassword = prefsUtil.hasPassword()
@@ -72,8 +82,6 @@ class SetupPasswordActivity : AppCompatActivity() {
}
binding.btnResetPassword.setOnClickListener {
// Implement password reset logic
// Could use security questions or email verification
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
else Toast.makeText(this,
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()

View File

@@ -2,7 +2,6 @@ package devs.org.calculator.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.os.Environment
@@ -20,20 +19,31 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderSelectionAdapter
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.databinding.ActivityViewFolderBinding
import devs.org.calculator.databinding.ProccessingDialogBinding
import devs.org.calculator.utils.DialogUtil
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.FolderManager
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.utils.SecurityUtils
import kotlinx.coroutines.launch
import java.io.File
import android.widget.CheckBox
import android.widget.CompoundButton
import android.app.AlertDialog
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import devs.org.calculator.adapters.FileAdapter
class ViewFolderActivity : AppCompatActivity() {
@@ -51,12 +61,12 @@ class ViewFolderActivity : AppCompatActivity() {
private var currentFolder: File? = null
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
private lateinit var prefs: SharedPreferences
private var customDialog: androidx.appcompat.app.AlertDialog? = null
private var dialogShowTime: Long = 0
private val MINIMUM_DIALOG_DURATION = 1200L
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -64,6 +74,12 @@ class ViewFolderActivity : AppCompatActivity() {
binding = ActivityViewFolderBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupAnimations()
initialize()
setupClickListeners()
@@ -81,9 +97,9 @@ class ViewFolderActivity : AppCompatActivity() {
private fun initialize() {
fileManager = FileManager(this, this)
folderManager = FolderManager(this)
folderManager = FolderManager()
dialogUtil = DialogUtil(this)
prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
}
private fun setupActivityResultLaunchers() {
@@ -135,16 +151,19 @@ class ViewFolderActivity : AppCompatActivity() {
customDialog?.dismiss()
customDialog = null
updateFilesToAdapter()
refreshCurrentFolder()
}, remainingTime)
} else {
customDialog?.dismiss()
customDialog = null
refreshCurrentFolder()
updateFilesToAdapter()
}
}
private fun updateFilesToAdapter() {
openFolder(currentFolder!!)
val files = folderManager.getFilesInFolder(currentFolder!!)
fileAdapter?.submitList(files)
}
@@ -162,8 +181,46 @@ class ViewFolderActivity : AppCompatActivity() {
object : FileProcessCallback {
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
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({
dismissCustomDialog()
val files = folderManager.getFilesInFolder(targetFolder)
if (files.isNotEmpty()) {
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
if (fileAdapter == null) {
showFileList(files, targetFolder)
} else {
fileAdapter?.submitList(files.toMutableList())
}
}
}, 1000)
}
}
@@ -188,23 +245,27 @@ class ViewFolderActivity : AppCompatActivity() {
).show()
dismissCustomDialog()
}
e.printStackTrace()
}
}
}
private fun refreshCurrentView() {
if (currentFolder != null) {
refreshCurrentFolder()
}
}
override fun onResume() {
super.onResume()
refreshCurrentFolder()
setupFlagSecure()
}
private fun openFolder(folder: File) {
// Ensure folder exists and has .nomedia file
private fun setupFlagSecure() {
if (prefs.getBoolean("screenshot_restriction", true)) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
fun openFolder(folder: File) {
if (!folder.exists()) {
folder.mkdirs()
File(folder, ".nomedia").createNewFile()
@@ -218,34 +279,36 @@ class ViewFolderActivity : AppCompatActivity() {
} else {
showEmptyState()
}
binding.swipeLayout.isRefreshing = false
}
private fun showEmptyState() {
binding.noItems.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
binding.swipeLayout.visibility = View.GONE
}
private fun showFileList(files: List<File>, folder: File) {
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
// Clean up previous adapter
fileAdapter?.cleanup()
fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
onFolderLongClick = { isSelected ->
handleFileSelectionModeChange(isSelected)
}).apply {
setFileOperationCallback(object : FileAdapter.FileOperationCallback {
setFilesOperationCallback(object : FileAdapter.FilesOperationCallback {
override fun onFileDeleted(file: File) {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onFileRenamed(oldFile: File, newFile: File) {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onRefreshNeeded() {
refreshCurrentFolder()
updateFilesToAdapter()
}
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
@@ -261,9 +324,13 @@ class ViewFolderActivity : AppCompatActivity() {
}
binding.recyclerView.adapter = fileAdapter
binding.recyclerView.visibility = View.VISIBLE
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
binding.selectAllButton.setOnClickListener {
fileAdapter?.toggleSelectAll()
}
binding.menuButton.setOnClickListener {
fileAdapter?.let { adapter ->
showFileOptionsMenu(adapter.getSelectedItems())
@@ -306,24 +373,232 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileOptionsMenu(selectedFiles: List<File>) {
if (selectedFiles.isEmpty()) return
val options = arrayOf(
getString(R.string.un_hide),
getString(R.string.delete),
getString(R.string.copy_to_another_folder),
getString(R.string.move_to_another_folder)
)
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.file_options))
.setItems(options) { _, which ->
when (which) {
0 -> unhideSelectedFiles(selectedFiles)
1 -> deleteSelectedFiles(selectedFiles)
2 -> copyToAnotherFolder(selectedFiles)
3 -> moveToAnotherFolder(selectedFiles)
lifecycleScope.launch {
var hasEncryptedFiles = false
var hasDecryptedFiles = false
var hasEncFilesWithoutMetadata = false
for (file in selectedFiles) {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
if (hiddenFile?.isEncrypted == true) {
hasEncryptedFiles = true
} else {
hasEncFilesWithoutMetadata = true
}
} else {
hasDecryptedFiles = true
}
}
.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
val decryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} 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()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
e.printStackTrace()
}
}
mainHandler.post {
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()
}
}
if (successCount > 0) {
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
}
private fun moveToAnotherFolder(selectedFiles: List<File>) {
@@ -342,15 +617,12 @@ class ViewFolderActivity : AppCompatActivity() {
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
performFileUnhiding(selectedFiles)
}
override fun onNegativeButtonClicked() {
// Do nothing
}
override fun onNegativeButtonClicked() {}
override fun onNaturalButtonClicked() {
// Do nothing
}
override fun onNaturalButtonClicked() {}
}
)
}
@@ -359,8 +631,8 @@ class ViewFolderActivity : AppCompatActivity() {
private fun deleteSelectedFiles(selectedFiles: List<File>) {
dialogUtil.showMaterialDialog(
getString(R.string.delete_items),
getString(R.string.are_you_sure_you_want_to_delete_selected_items),
getString(R.string.delete_file),
getString(R.string.are_you_sure_to_delete_selected_files_permanently),
getString(R.string.delete),
getString(R.string.cancel),
object : DialogUtil.DialogCallback {
@@ -368,13 +640,9 @@ class ViewFolderActivity : AppCompatActivity() {
performFileDeletion(selectedFiles)
}
override fun onNegativeButtonClicked() {
// Do nothing
}
override fun onNegativeButtonClicked() {}
override fun onNaturalButtonClicked() {
// Do nothing
}
override fun onNaturalButtonClicked() {}
}
)
}
@@ -383,20 +651,40 @@ class ViewFolderActivity : AppCompatActivity() {
private fun refreshCurrentFolder() {
currentFolder?.let { folder ->
val files = folderManager.getFilesInFolder(folder)
if (files.isNotEmpty()) {
binding.recyclerView.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
fileAdapter?.submitList(files.toMutableList())
fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) {
showFileSelectionIcons()
} else {
showFileViewIcons()
lifecycleScope.launch {
try {
val files = folderManager.getFilesInFolder(folder)
mainHandler.post {
if (files.isNotEmpty()) {
binding.swipeLayout.visibility = View.VISIBLE
binding.noItems.visibility = View.GONE
val currentFiles = fileAdapter?.currentList ?: emptyList()
val hasChanges = files.size != currentFiles.size ||
files.any { newFile ->
currentFiles.none { it.absolutePath == newFile.absolutePath }
}
if (hasChanges) {
fileAdapter?.submitList(files.toMutableList())
}
fileAdapter?.let { adapter ->
if (adapter.isInSelectionMode()) {
showFileSelectionIcons()
} else {
showFileViewIcons()
}
}
} else {
showEmptyState()
}
}
} catch (_: Exception) {
mainHandler.post {
showEmptyState()
}
}
} else {
showEmptyState()
}
}
}
@@ -408,6 +696,9 @@ class ViewFolderActivity : AppCompatActivity() {
binding.back.setOnClickListener {
finish()
}
binding.swipeLayout.setOnRefreshListener {
openFolder(currentFolder!!)
}
binding.addImage.setOnClickListener { openFilePicker("image/*") }
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
@@ -475,6 +766,7 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileViewIcons() {
binding.menuButton.visibility = View.GONE
binding.selectAllButton.visibility = View.GONE
binding.fabExpend.visibility = View.VISIBLE
binding.addImage.visibility = View.INVISIBLE
binding.addVideo.visibility = View.INVISIBLE
@@ -486,6 +778,7 @@ class ViewFolderActivity : AppCompatActivity() {
private fun showFileSelectionIcons() {
binding.menuButton.visibility = View.VISIBLE
binding.selectAllButton.visibility = View.VISIBLE
binding.fabExpend.visibility = View.GONE
binding.addImage.visibility = View.INVISIBLE
binding.addVideo.visibility = View.INVISIBLE
@@ -497,21 +790,65 @@ class ViewFolderActivity : AppCompatActivity() {
private fun performFileUnhiding(selectedFiles: List<File>) {
lifecycleScope.launch {
var allUnhidden = true
val unhiddenFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result == null) {
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, decryptedFile)
if (fileUri != null) {
val result = fileManager.copyFileToNormalDir(fileUri)
if (result != null) {
hiddenFile.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
file.delete()
decryptedFile.delete()
unhiddenFiles.add(file)
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
decryptedFile.delete()
allUnhidden = false
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
allUnhidden = false
}
} 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()
unhiddenFiles.add(file)
} else {
allUnhidden = false
}
} else {
allUnhidden = false
}
}
} catch (e: Exception) {
allUnhidden = false
e.printStackTrace()
}
}
@@ -523,33 +860,44 @@ class ViewFolderActivity : AppCompatActivity() {
}
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
// Fixed: Ensure proper order of operations
fileAdapter?.exitSelectionMode()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
}
private fun performFileDeletion(selectedFiles: List<File>) {
var allDeleted = true
selectedFiles.forEach { file ->
if (!file.delete()) {
allDeleted = false
lifecycleScope.launch {
var allDeleted = true
val deletedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
}
if (file.delete()) {
deletedFiles.add(file)
} else {
allDeleted = false
}
} catch (_: 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()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
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>) {
@@ -559,47 +907,86 @@ class ViewFolderActivity : AppCompatActivity() {
}
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
var allCopied = true
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
file.copyTo(newFile, overwrite = true)
} catch (e: Exception) {
allCopied = false
lifecycleScope.launch {
var allCopied = true
val copiedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
file.copyTo(newFile, overwrite = true)
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
)
)
}
copiedFiles.add(file)
} catch (e: Exception) {
allCopied = false
e.printStackTrace()
}
}
mainHandler.post {
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
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) {
var allMoved = true
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
file.copyTo(newFile, overwrite = true)
file.delete()
} catch (e: Exception) {
allMoved = false
lifecycleScope.launch {
var allMoved = true
val movedFiles = mutableListOf<File>()
selectedFiles.forEach { file ->
try {
val newFile = File(destinationFolder, file.name)
file.copyTo(newFile, overwrite = true)
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
hiddenFile?.let {
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = newFile.absolutePath,
encryptedFileName = it.encryptedFileName,
isEncrypted = it.isEncrypted
)
}
if (file.delete()) {
movedFiles.add(file)
} else {
allMoved = false
}
} catch (e: Exception) {
allMoved = false
e.printStackTrace()
}
}
mainHandler.post {
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
refreshCurrentFolder()
fileAdapter?.exitSelectionMode()
}
}
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()
}
@SuppressLint("InflateParams")
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
val folders = folderManager.getFoldersInDirectory(hiddenDir)
.filter { it != currentFolder } // Exclude current folder
.filter { it != currentFolder }
if (folders.isEmpty()) {
Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,10 @@ import java.io.File
class FileDiffCallback : DiffUtil.ItemCallback<File>() {
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
}
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
// Compare all relevant properties that might change and affect the UI
return oldItem.name == newItem.name &&
oldItem.length() == newItem.length() &&
oldItem.lastModified() == newItem.lastModified() &&
@@ -22,8 +19,6 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
}
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>()
if (oldItem.name != newItem.name) {
@@ -42,6 +37,10 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
changes.add("EXISTENCE_CHANGED")
}
return if (changes.isNotEmpty()) changes else null
if (oldItem.absolutePath != newItem.absolutePath) {
changes.add("FILE_CHANGED")
}
return changes.ifEmpty { null }
}
}

View File

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

View File

@@ -5,19 +5,25 @@ import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import android.widget.SeekBar
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import devs.org.calculator.R
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileRepository
import devs.org.calculator.databinding.ViewpagerItemsBinding
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
import kotlinx.coroutines.launch
import java.io.File
import devs.org.calculator.R
class ImagePreviewAdapter(
private val context: Context,
@@ -25,16 +31,19 @@ class ImagePreviewAdapter(
) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() {
private val differ = AsyncListDiffer(this, FileDiffCallback())
var currentMediaPlayer: MediaPlayer? = null
var isMediaPlayerPrepared = false
var currentViewHolder: ImageViewHolder? = null
private var currentPlayingPosition = -1
private var isPlaying = false
private var currentViewHolder: ImageViewHolder? = null
private val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
private var canZoomOnImages: Boolean = true
var images: List<File>
get() = differ.currentList
set(value) = differ.submitList(value)
var onSlideshowShouldStop: () -> Unit = {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val binding = ViewpagerItemsBinding.inflate(LayoutInflater.from(context), parent, false)
return ImageViewHolder(binding)
@@ -42,110 +51,239 @@ class ImagePreviewAdapter(
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = images[position]
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
holder.bind(imageUrl,fileType)
currentViewHolder = holder
val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
stopAndResetCurrentAudio()
currentMediaPlayer?.let {
if (it.isPlaying) it.pause()
it.seekTo(0)
}
currentMediaPlayer = null
isMediaPlayerPrepared = false
if (currentMediaPlayer?.isPlaying == true) {
currentMediaPlayer?.stop()
currentMediaPlayer?.release()
}
currentMediaPlayer = null
holder.bind(imageUrl, position,fileType)
}
override fun getItemCount(): Int = images.size
fun setImageZoomable(zoomable: Boolean) {
currentViewHolder?.setImageViewCanZoom(zoomable)
canZoomOnImages = zoomable
}
private fun stopAndResetCurrentAudio() {
currentViewHolder?.stopAndResetAudio()
currentPlayingPosition = -1
currentViewHolder = null
}
inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) {
private var mediaPlayer: MediaPlayer? = null
private var seekHandler = Handler(Looper.getMainLooper())
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) {
when (fileType) {
FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE
fun bind(file: File, position: Int, decryptedFileType: FileManager.FileType) {
currentPosition = position
val videoUri = Uri.fromFile(file)
binding.videoView.setVideoURI(videoUri)
binding.videoView.start()
val mediaController = MediaController(context)
mediaController.setAnchorView(binding.videoView)
binding.videoView.setMediaController(mediaController)
mediaController.setPrevNextListeners(
{
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
},
{
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
playVideoAtPosition(prevPosition)
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) {
tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
if (tempDecryptedFile != null && tempDecryptedFile!!.exists() && tempDecryptedFile!!.length() > 0) {
displayFile(tempDecryptedFile!!, fileType, true)
} else {
Log.e("ImagePreviewAdapter", "Failed to get decrypted preview file for: ${file.absolutePath}")
showEncryptedError()
}
} else {
displayFile(file, decryptedFileType, false)
}
)
binding.videoView.setOnCompletionListener {
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
} else {
displayFile(file, decryptedFileType, false)
}
}
FileManager.FileType.IMAGE -> {
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
Glide.with(context)
.load(file)
.into(binding.imageView)
}
FileManager.FileType.AUDIO -> {
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name
setupAudioPlayer(file)
setupSeekBar()
setupPlaybackControls()
}
else -> {
binding.imageView.visibility = View.VISIBLE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.GONE
}
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error in bind: ${e.message}")
displayFile(file, decryptedFileType, false)
}
}
private fun displayFile(file: File, fileType: FileManager.FileType, isEncrypted: Boolean = false) {
try {
val uri = if (isEncrypted) {
getUriForPreviewFile(context, file)
} else {
Uri.fromFile(file)
}
if (uri == null) {
Log.e("ImagePreviewAdapter", "Failed to get URI for file: ${file.absolutePath}")
showEncryptedError()
return
}
when (fileType) {
FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.VISIBLE
binding.videoView.setVideoURI(uri)
binding.videoView.start()
val mediaController = MediaController(context)
mediaController.setAnchorView(binding.videoView)
binding.videoView.setMediaController(mediaController)
mediaController.setPrevNextListeners(
{
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
},
{
val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1
playVideoAtPosition(prevPosition)
}
)
binding.videoView.setOnCompletionListener {
val nextPosition = (adapterPosition + 1) % images.size
playVideoAtPosition(nextPosition)
}
}
FileManager.FileType.IMAGE -> {
binding.imageView.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioBg.visibility = View.GONE
binding.imageView.isZoomable = canZoomOnImages
binding.imageView.doubleTapToZoom = canZoomOnImages
binding.imageView.setOnClickListener {
onSlideshowShouldStop()
}
Glide.with(context)
.load(uri)
.error(R.drawable.encrypted)
.into(binding.imageView)
}
FileManager.FileType.AUDIO -> {
val audioFile: File? = if (isEncrypted) {
getFileFromUri(context, uri)
} else {
file
}
if (audioFile == null) {
Log.e("ImagePreviewAdapter", "Failed to get audio file from URI")
showEncryptedError()
return
}
binding.imageView.visibility = View.GONE
binding.audioBg.visibility = View.VISIBLE
binding.videoView.visibility = View.GONE
binding.audioTitle.text = file.name
setupAudioPlayer(audioFile)
setupPlaybackControls()
}
else -> {
binding.imageView.visibility = View.VISIBLE
binding.audioBg.visibility = View.GONE
binding.videoView.visibility = View.GONE
binding.imageView.isZoomable = canZoomOnImages
binding.imageView.doubleTapToZoom = canZoomOnImages
binding.imageView.setOnClickListener {
onSlideshowShouldStop()
}
Glide.with(context)
.load(uri)
.error(R.drawable.encrypted)
.into(binding.imageView)
}
}
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error displaying file: ${e.message}")
showEncryptedError()
}
}
fun setImageViewCanZoom(zoomEnabled: Boolean) {
binding.imageView.doubleTapToZoom = zoomEnabled
binding.imageView.isZoomable = zoomEnabled
}
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 {
if (it.exists()) {
try {
it.delete()
} catch (e: Exception) {
Log.e("ImagePreviewAdapter", "Error cleaning up temp file: ${e.message}")
}
}
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) {
mediaPlayer = MediaPlayer().apply {
setDataSource(file.absolutePath)
setOnPreparedListener { mp ->
binding.audioSeekBar.valueTo = mp.duration.toFloat()
isMediaPlayerPrepared = true
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(file.absolutePath)
setOnPreparedListener { mp ->
binding.audioSeekBar.valueTo = mp.duration.toFloat()
binding.audioSeekBar.value = 0f
setupSeekBar()
isMediaPlayerPrepared = true
}
setOnCompletionListener {
stopAndResetAudio()
}
setOnErrorListener { _, _, _ ->
releaseMediaPlayer()
true
}
prepareAsync()
}
setOnCompletionListener {
// isPlaying = false
binding.playPause.setImageResource(R.drawable.play)
binding.audioSeekBar.value = 0f
seekHandler.removeCallbacks(seekRunnable!!)
}
prepareAsync()
} catch (e: Exception) {
e.printStackTrace()
releaseMediaPlayer()
}
}
private fun setupSeekBar() {
binding.audioSeekBar.addOnChangeListener { slider, value, fromUser ->
binding.audioSeekBar.addOnChangeListener { _, value, fromUser ->
if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) {
mediaPlayer?.seekTo(value.toInt())
}
@@ -153,15 +291,18 @@ class ImagePreviewAdapter(
seekRunnable = Runnable {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
binding.audioSeekBar.value = mp.currentPosition.toFloat()
seekHandler.postDelayed(seekRunnable!!, 100)
if (mp.isPlaying && isMediaPlayerPrepared) {
try {
binding.audioSeekBar.value = mp.currentPosition.toFloat()
seekHandler.postDelayed(seekRunnable!!, 100)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
private fun setupPlaybackControls() {
binding.playPause.setOnClickListener {
if (isPlaying) {
@@ -173,65 +314,127 @@ class ImagePreviewAdapter(
binding.preview.setOnClickListener {
mediaPlayer?.let { mp ->
val newPosition = mp.currentPosition - 10000
mp.seekTo(maxOf(0, newPosition))
binding.audioSeekBar.value = mp.currentPosition.toFloat()
if (isMediaPlayerPrepared) {
try {
val newPosition = mp.currentPosition - 10000
mp.seekTo(maxOf(0, newPosition))
binding.audioSeekBar.value = mp.currentPosition.toFloat()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
binding.next.setOnClickListener {
mediaPlayer?.let { mp ->
val newPosition = mp.currentPosition + 10000
mp.seekTo(minOf(mp.duration, newPosition))
binding.audioSeekBar.value = mp.currentPosition.toFloat()
if (isMediaPlayerPrepared) {
try {
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() {
mediaPlayer?.let { mp ->
if (currentPlayingPosition != -1 && currentPlayingPosition != adapterPosition) {
currentViewHolder?.pauseAudio()
if (isMediaPlayerPrepared) {
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() {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
mp.pause()
isPlaying = false
binding.playPause.setImageResource(R.drawable.play)
seekHandler.removeCallbacks(seekRunnable!!)
try {
if (mp.isPlaying) {
mp.pause()
isPlaying = false
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() {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
mp.stop()
try {
mediaPlayer?.let { mp ->
if (mp.isPlaying) {
mp.stop()
}
mp.release()
}
mp.release()
} catch (e: Exception) {
e.printStackTrace()
} finally {
mediaPlayer = null
isPlaying = false
seekHandler.removeCallbacks(seekRunnable!!)
isMediaPlayerPrepared = false
seekRunnable?.let { seekHandler.removeCallbacks(it) }
if (currentPlayingPosition == currentPosition) {
currentPlayingPosition = -1
currentViewHolder = null
}
}
}
private fun playVideoAtPosition(position: Int) {
val nextFile = images[position]
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
if (fileType == FileManager.FileType.VIDEO) {
val videoUri = Uri.fromFile(nextFile)
binding.videoView.setVideoURI(videoUri)
binding.videoView.start()
if (position < images.size) {
val nextFile = images[position]
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
if (fileType == FileManager.FileType.VIDEO) {
val videoUri = Uri.fromFile(nextFile)
binding.videoView.setVideoURI(videoUri)
binding.videoView.start()
}
}
}
}
@@ -240,5 +443,14 @@ class ImagePreviewAdapter(
super.onViewRecycled(holder)
holder.releaseMediaPlayer()
}
}
fun onItemScrolledAway(position: Int) {
if (currentPlayingPosition == position) {
stopAndResetCurrentAudio()
}
}
fun releaseAllResources() {
stopAndResetCurrentAudio()
}
}

View File

@@ -1,9 +1,9 @@
package devs.org.calculator.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -22,9 +22,9 @@ class ListFolderAdapter(
private var isSelectionMode = false
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) {
folderNameTextView.text = folder.name
@@ -89,6 +89,7 @@ class ListFolderAdapter(
}
}
@SuppressLint("NotifyDataSetChanged")
fun clearSelection() {
val wasInSelectionMode = isSelectionMode
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

@@ -1,5 +1,6 @@
package devs.org.calculator.utils
import android.Manifest
import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.ActivityNotFoundException
@@ -8,6 +9,8 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
@@ -15,22 +18,26 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.database.AppDatabase
import devs.org.calculator.database.HiddenFileEntity
import devs.org.calculator.database.HiddenFileRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import android.Manifest
import androidx.core.content.FileProvider
import devs.org.calculator.R
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
val intent = Intent()
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
companion object {
const val HIDDEN_DIR = ".CalculatorHide"
@@ -38,8 +45,10 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
const val VIDEOS_DIR = "videos"
const val AUDIO_DIR = "audio"
const val DOCS_DIR = "documents"
const val ENCRYPTED_EXTENSION = ".enc"
}
fun getHiddenDirectory(): File {
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
if (!dir.exists()) {
@@ -47,7 +56,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
if (!created) {
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
}
// Create .nomedia file to hide from media scanners
val nomediaFile = File(dir, ".nomedia")
if (!nomediaFile.exists()) {
nomediaFile.createNewFile()
@@ -56,15 +64,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
return dir
}
fun getFilesInHiddenDir(type: FileType): List<File> {
val hiddenDir = getHiddenDirectory()
val typeDir = File(hiddenDir, type.dirName)
if (!typeDir.exists()) {
typeDir.mkdirs()
File(typeDir, ".nomedia").createNewFile()
}
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
}
fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List<File> {
val typeDir = File(folder)
if (!typeDir.exists()) {
@@ -92,7 +91,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(mimeType) ?: ""
val fileName = "${System.currentTimeMillis()}.${extension}"
val targetFile = File(targetDir, fileName)
var targetFile = File(targetDir, fileName)
// Copy file using DocumentFile
contentResolver.openInputStream(uri)?.use { input ->
@@ -124,7 +123,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
val contentResolver = context.contentResolver
// Get the target directory
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES + "/Restore")
targetDir.mkdirs()
// Create target file
@@ -159,64 +158,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
null
}
}
fun unHideFile(file: File, onSuccess: (() -> Unit)? = null, onError: ((String) -> Unit)? = null) {
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
try {
// Create target directory (Downloads)
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
targetDir.mkdirs()
// Create target file with same name or timestamp
val targetFile = File(targetDir, file.name)
// If file with same name exists, add timestamp
val finalTargetFile = if (targetFile.exists()) {
val nameWithoutExt = file.nameWithoutExtension
val extension = file.extension
File(targetDir, "${nameWithoutExt}_${System.currentTimeMillis()}.${extension}")
} else {
targetFile
}
// Copy file content
file.copyTo(finalTargetFile, overwrite = false)
// Verify copy success
if (finalTargetFile.exists() && finalTargetFile.length() > 0) {
// Delete original hidden file
if (file.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 {
withContext(Dispatchers.Main) {
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 {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Failed to copy file", Toast.LENGTH_SHORT).show()
onError?.invoke("Failed to copy file")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Error unhiding file: ${e.message}", Toast.LENGTH_LONG).show()
onError?.invoke(e.message ?: "Unknown error")
}
e.printStackTrace()
}
}
}
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
withContext(Dispatchers.IO) {
@@ -224,7 +165,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
// First try to delete using DocumentFile
val documentFile = DocumentFile.fromSingleUri(context, photoUri)
if (documentFile?.exists() == true && documentFile.canWrite()) {
val deleted = documentFile.delete()
documentFile.delete()
withContext(Dispatchers.Main) {
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
}
@@ -388,4 +329,142 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
DOCUMENT(DOCS_DIR),
ALL("all")
}
fun performEncryption(selectedFiles: List<File>,onEncryptionEnded :(MutableMap<File, File>)-> Unit) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val encryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) continue
val originalExtension = ".${file.extension.lowercase()}"
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
val encryptedFile = SecurityUtils.changeFileExtension(file, devs.org.calculator.utils.FileManager.ENCRYPTED_EXTENSION)
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
if (encryptedFile.exists()) {
if (hiddenFile == null){
hiddenFileRepository.insertHiddenFile(
HiddenFileEntity(
filePath = encryptedFile.absolutePath,
isEncrypted = true,
encryptedFileName = encryptedFile.name,
fileType = fileType,
fileName = file.name,
originalExtension = originalExtension
)
)
}else{
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = hiddenFile.filePath,
newFilePath = encryptedFile.absolutePath,
encryptedFileName = encryptedFile.name,
isEncrypted = true
)
}
}
if (file.delete()) {
encryptedFiles[file] = encryptedFile
successCount++
} else {
failCount++
}
} else {
failCount++
}
} else {
failCount++
}
} catch (e: Exception) {
failCount++
}
}
Handler(Looper.getMainLooper()).post{
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Files encrypted successfully", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Some files could not be encrypted", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
else -> {
Toast.makeText(context, "Failed to encrypt files", Toast.LENGTH_SHORT).show()
onEncryptionEnded(encryptedFiles)
}
}
}
}
}
fun performDecryption(selectedFiles: List<File>,onDecryptionEnded :(MutableMap<File, File>) -> Unit) {
lifecycleOwner.lifecycleScope.launch {
var successCount = 0
var failCount = 0
val decryptedFiles = mutableMapOf<File, File>()
for (file in selectedFiles) {
try {
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
if (hiddenFile?.isEncrypted == true) {
val originalExtension = hiddenFile.originalExtension
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
if (decryptedFile.exists() && decryptedFile.length() > 0) {
hiddenFile.let {
hiddenFileRepository.updateEncryptionStatus(
filePath = file.absolutePath,
newFilePath = decryptedFile.absolutePath,
encryptedFileName = decryptedFile.name,
isEncrypted = false
)
}
if (file.delete()) {
decryptedFiles[file] = decryptedFile
successCount++
} else {
decryptedFile.delete()
failCount++
}
} else {
decryptedFile.delete()
failCount++
}
} else {
if (decryptedFile.exists()) {
decryptedFile.delete()
}
failCount++
}
}
} catch (e: Exception) {
failCount++
}
}
Handler(Looper.getMainLooper()).post{
when {
successCount > 0 && failCount == 0 -> {
Toast.makeText(context, "Files decrypted successfully", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
successCount > 0 && failCount > 0 -> {
Toast.makeText(context, "Some files could not be decrypted", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
else -> {
Toast.makeText(context, "Failed to decrypt files", Toast.LENGTH_SHORT).show()
onDecryptionEnded(decryptedFiles)
}
}
}
}
}
}

View File

@@ -1,19 +1,14 @@
package devs.org.calculator.utils
import android.content.Context
import android.os.Environment
import java.io.File
class FolderManager(private val context: Context) {
companion object {
const val HIDDEN_DIR = ".CalculatorHide"
}
class FolderManager {
fun createFolder(parentDir: File, folderName: String): Boolean {
val newFolder = File(parentDir, folderName)
return if (!newFolder.exists()) {
newFolder.mkdirs()
// Create .nomedia file to hide from media scanners
File(newFolder, ".nomedia").createNewFile()
true
} else {
@@ -44,7 +39,7 @@ class FolderManager(private val context: Context) {
directory.listFiles()?.filter { it.isDirectory && it.name != ".nomedia" } ?: emptyList()
} else {
emptyList()
}
}.sortedBy { it.name.lowercase() }
}
fun getFilesInFolder(folder: File): List<File> {
@@ -54,20 +49,4 @@ class FolderManager(private val context: Context) {
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.SharedPreferences
import java.security.MessageDigest
import androidx.core.content.edit
class PrefsUtil(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE)
@@ -13,26 +14,33 @@ class PrefsUtil(context: Context) {
fun savePassword(password: String) {
val hashedPassword = hashPassword(password)
prefs.edit()
.putString("password", hashedPassword)
.apply()
prefs.edit {
putString("password", hashedPassword)
}
}
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{
return prefs.getBoolean(key,defValue)
}
fun getInt(key: String, defValue: Int): Int{
return prefs.getInt(key,defValue)
}
fun resetPassword(){
prefs.edit()
.remove("password")
.remove("security_question")
.remove("security_answer")
.apply()
prefs.edit {
remove("password")
.remove("security_question")
.remove("security_answer")
}
}
fun validatePassword(input: String): Boolean {
@@ -40,11 +48,15 @@ class PrefsUtil(context: Context) {
return stored == hashPassword(input)
}
fun getPassword(): String{
return prefs.getString("password", "") ?: ""
}
fun saveSecurityQA(question: String, answer: String) {
prefs.edit()
.putString("security_question", question)
.putString("security_answer", hashPassword(answer))
.apply()
prefs.edit {
putString("security_question", question)
.putString("security_answer", hashPassword(answer))
}
}
fun validateSecurityAnswer(answer: String): Boolean {

View File

@@ -5,60 +5,254 @@ import android.net.Uri
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
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
import android.util.Log
class SecurityUtils {
companion object {
private const val ALGORITHM = "AES"
private const val HIDDEN_FOLDER = "Calculator_Data"
object SecurityUtils {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val KEY_SIZE = 256
val ENCRYPTED_EXTENSION = ".enc"
private fun getSecretKey(context: Context): SecretKey {
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
val useCustomKey = keyStore.getBoolean("use_custom_key", false)
fun validatePassword(input: String, storedHash: String): Boolean {
return input.hashCode().toString() == storedHash
}
fun encryptFile(context: Context, sourceUri: Uri, password: String): File {
val inputStream = context.contentResolver.openInputStream(sourceUri)
val secretKey = generateKey(password)
val cipher = Cipher.getInstance(ALGORITHM)
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)
if (useCustomKey) {
val customKey = keyStore.getString("custom_key", null)
if (customKey != null) {
try {
val messageDigest = java.security.MessageDigest.getInstance("SHA-256")
val keyBytes = messageDigest.digest(customKey.toByteArray())
return SecretKeySpec(keyBytes, ALGORITHM)
} catch (_: Exception) {
}
val finalBlock = cipher.doFinal()
outputStream.write(finalBlock)
}
outputStream.close()
return encryptedFile
}
fun decryptFile(file: File, password: String): ByteArray {
val secretKey = generateKey(password)
val cipher = Cipher.getInstance(ALGORITHM)
cipher.init(Cipher.DECRYPT_MODE, secretKey)
val inputStream = FileInputStream(file)
val bytes = inputStream.readBytes()
inputStream.close()
return cipher.doFinal(bytes)
}
private fun generateKey(password: String): SecretKey {
val keyBytes = password.toByteArray().copyOf(16)
return SecretKeySpec(keyBytes, ALGORITHM)
val encodedKey = keyStore.getString("secret_key", null)
return if (encodedKey != null) {
try {
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
SecretKeySpec(decodedKey, ALGORITHM)
} catch (_: Exception) {
generateAndStoreNewKey(keyStore)
}
} else {
generateAndStoreNewKey(keyStore)
}
}
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()) {
Log.e("SecurityUtils", "Encrypted file does not exist: ${meta.filePath}")
return null
}
val tempDir = File(context.cacheDir, "preview_temp")
if (!tempDir.exists()) {
if (!tempDir.mkdirs()) {
Log.e("SecurityUtils", "Failed to create temp directory")
return null
}
}
// Clean up old preview files
tempDir.listFiles()?.forEach {
if (it.lastModified() < System.currentTimeMillis() - 5 * 60 * 1000) { // 5 minutes
it.delete()
}
}
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
val success = decryptFile(context, encryptedFile, tempFile)
if (success && tempFile.exists() && tempFile.length() > 0) {
return tempFile
} else {
Log.e("SecurityUtils", "Failed to decrypt preview file: ${meta.filePath}")
if (tempFile.exists()) tempFile.delete()
return null
}
} catch (e: Exception) {
Log.e("SecurityUtils", "Error in getDecryptedPreviewFile: ${e.message}")
return null
}
}
fun getUriForPreviewFile(context: Context, file: File): Uri? {
return try {
if (!file.exists() || file.length() == 0L) {
Log.e("SecurityUtils", "Preview file does not exist or is empty: ${file.absolutePath}")
return null
}
FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
file
)
} catch (e: Exception) {
Log.e("SecurityUtils", "Error getting URI for preview file: ${e.message}")
null
}
}
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
return try {
if (!inputFile.exists()) {
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

@@ -7,34 +7,31 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import androidx.core.net.toUri
class StoragePermissionUtil(private val activity: AppCompatActivity) {
private val requestPermissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (permissions.all { it.value }) {
onPermissionGranted?.invoke()
}
}
private var onPermissionGranted: (() -> Unit)? = null
fun requestStoragePermission(onGranted: () -> Unit) {
fun requestStoragePermission(
launcher: ActivityResultLauncher<Array<String>>,
onGranted: () -> Unit
) {
onPermissionGranted = onGranted
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
if (Environment.isExternalStorageManager()) {
onGranted()
} else {
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)
}
@@ -44,7 +41,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
requestPermissionLauncher.launch(permissions)
launcher.launch(permissions)
}
}
}
@@ -65,4 +62,10 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
writePermission == PermissionChecker.PERMISSION_GRANTED
}
}
}
fun handlePermissionResult(permissions: Map<String, Boolean>) {
if (permissions.all { it.value }) {
onPermissionGranted?.invoke()
}
}
}

View File

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

View File

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

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M340,680Q365,680 382.5,662.5Q400,645 400,620Q400,595 382.5,577.5Q365,560 340,560Q315,560 297.5,577.5Q280,595 280,620Q280,645 297.5,662.5Q315,680 340,680ZM340,400Q365,400 382.5,382.5Q400,365 400,340Q400,315 382.5,297.5Q365,280 340,280Q315,280 297.5,297.5Q280,315 280,340Q280,365 297.5,382.5Q315,400 340,400ZM620,680Q645,680 662.5,662.5Q680,645 680,620Q680,595 662.5,577.5Q645,560 620,560Q595,560 577.5,577.5Q560,595 560,620Q560,645 577.5,662.5Q595,680 620,680ZM620,400Q645,400 662.5,382.5Q680,365 680,340Q680,315 662.5,297.5Q645,280 620,280Q595,280 577.5,297.5Q560,315 560,340Q560,365 577.5,382.5Q595,400 620,400ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
</vector>

View File

@@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="50dp"
android:height="50dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/textColor"
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"/>
</vector>
<path
android:fillColor="#FF000000"
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

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

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M268,720L42,494L99,438L269,608L269,608L325,664L268,720ZM494,720L268,494L324,437L494,607L862,239L918,296L494,720ZM494,494L437,438L635,240L692,296L494,494Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@color/svgTintColor"
android:pathData="M10,8v8l5,-4 -5,-4zM19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,5h14v14z"/>
</vector>

View File

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

View File

@@ -39,57 +39,44 @@
android:layout_weight="1"
android:id="@+id/folderName"/>
<androidx.appcompat.widget.AppCompatImageButton
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_edit"
app:tint="?attr/colorPrimary"
android:scaleType="fitCenter"
android:padding="9dp"
android:visibility="gone"
android:background="#00000000"
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_edit"
style="@style/Widget.Material3.Button.IconButton"
android:id="@+id/edit"/>
<androidx.appcompat.widget.AppCompatImageButton
<com.google.android.material.button.MaterialButton
android:id="@+id/delete"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#00000000"
android:padding="9dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_delete"
app:tint="?attr/colorPrimary"
android:visibility="gone" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_delete"
style="@style/Widget.Material3.Button.IconButton" />
<androidx.appcompat.widget.AppCompatImageButton
<com.google.android.material.button.MaterialButton
android:id="@+id/menuButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_more"
app:tint="?attr/colorPrimary"
android:scaleType="fitCenter"
android:padding="9dp"
android:visibility="gone"
android:background="#00000000"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_more"
style="@style/Widget.Material3.Button.IconButton"/>
<androidx.appcompat.widget.AppCompatImageButton
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_list"
android:scaleType="fitCenter"
app:tint="?attr/colorPrimary"
android:background="#00000000"
android:padding="8dp"
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_dice"
style="@style/Widget.Material3.Button.IconButton"
android:id="@+id/random" />
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_list"
style="@style/Widget.Material3.Button.IconButton"
android:id="@+id/folderOrientation"/>
<androidx.appcompat.widget.AppCompatImageButton
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_settings"
android:scaleType="fitCenter"
app:tint="?attr/colorPrimary"
android:background="#00000000"
android:padding="8dp"
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_settings"
style="@style/Widget.Material3.Button.IconButton"
android:id="@+id/settings"/>
</LinearLayout>
@@ -128,7 +115,7 @@
android:layout_marginTop="8dp"
android:gravity="center"
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" />
</LinearLayout>

View File

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

View File

@@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:id="@+id/main"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -13,6 +14,7 @@
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="15dp"
android:paddingRight="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<androidx.appcompat.widget.AppCompatImageButton
@@ -25,11 +27,20 @@
android:id="@+id/back"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:textSize="22sp"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/preview_file"/>
android:text="@string/preview_file"
android:layout_weight="1"/>
<androidx.appcompat.widget.AppCompatImageButton
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_slideshow"
android:scaleType="fitCenter"
android:background="#00000000"
android:padding="8dp"
android:id="@+id/start_slideshow"/>
</LinearLayout>
<androidx.viewpager2.widget.ViewPager2
@@ -43,6 +54,7 @@
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"

View File

@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/main"
android:layout_height="match_parent"
tools:context=".activities.SettingsActivity">
@@ -81,7 +82,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="10dp"
app:cardElevation="5dp">
android:background="#00000000"
android:backgroundTint="#00000000"
app:cardElevation="0dp">
<ImageView
android:id="@+id/appLogo"
android:layout_width="48dp"
@@ -254,6 +257,23 @@
android:layout_marginTop="16dp"
android:text="@string/restrict_screenshots_in_hidden_section"
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>
</com.google.android.material.card.MaterialCardView>

View File

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

View File

@@ -39,30 +39,40 @@
android:layout_weight="1"
android:id="@+id/folderName"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/menuButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_more"
app:tint="?attr/colorPrimary"
android:scaleType="fitCenter"
android:padding="7dp"
<com.google.android.material.button.MaterialButton
android:id="@+id/selectAllButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_select_all"
android:visibility="gone"
android:background="#00000000"/>
style="@style/Widget.Material3.Button.IconButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/menuButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_more"
android:visibility="gone"
style="@style/Widget.Material3.Button.IconButton"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/swipeLayout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolBar">
app:layout_constraintTop_toBottomOf="@+id/toolBar"
android:layout_height="0dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp">
</androidx.recyclerview.widget.RecyclerView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.recyclerview.widget.RecyclerView>
<LinearLayout
android:id="@+id/noItems"
@@ -79,7 +89,7 @@
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_no_items" />
android:src="@drawable/ic_file_no_item" />
<TextView
android:id="@+id/noItemsTxt"

View File

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

View File

@@ -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_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
app:cardCornerRadius="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@@ -1,24 +1,33 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:layout_margin="3dp"
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" />
<TextView
android:id="@+id/folderName"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="18sp"/>
android:padding="12dp"
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:layout_width="match_parent"
android:layout_height="0dp"
app:cardCornerRadius="10dp"
android:layout_margin="5dp"
app:cardElevation="3dp"
android:backgroundTint="#404040"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
@@ -35,8 +35,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/add_image" />
android:scaleType="centerCrop"/>
<View
android:id="@+id/shade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bottom_shade"/>
<LinearLayout
android:id="@+id/selectedLayer"
android:layout_width="match_parent"
@@ -49,7 +53,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>
<ImageView
@@ -60,6 +78,13 @@
android:scaleType="fitCenter"
android:src="@drawable/ic_play_circle"
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
android:id="@+id/selected"
android:layout_width="20dp"
@@ -70,16 +95,7 @@
android:src="@drawable/selected"
android:background="@drawable/gradient_bg"
android:layout_gravity="end"/>
</FrameLayout>
<TextView
android:id="@+id/fileNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="5dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>

View File

@@ -42,7 +42,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
app:cardCornerRadius="15dp"
app:cardElevation="10dp"
android:layout_margin="20dp">
<LinearLayout
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:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
<item name="colorSecondaryContainer">#481A4324</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
</style>

View File

@@ -1,5 +1,6 @@
<resources>
<string name="app_name">Calculator</string>
<string name="app_name" translatable="false">Calculator</string>
<string name="version" translatable="false">Version 1.4.1</string>
<string name="invalid_message">Invalid Value Entered</string>
<string name="add_image">Add Image</string>
<string name="add_audio">Add Audio</string>
@@ -24,7 +25,7 @@
<string name="preview_videos">Preview Videos</string>
<string name="preview_audios">Preview Audios</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="delete_permanently">Delete Permanently</string>
<string name="cancel">Cancel</string>
@@ -64,7 +65,7 @@
<string name="unknown_file">Unknown File</string>
<string name="details"> DETAILS</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="enter_123456">Enter 123456</string>
<string name="create_folder">Create Folder</string>
@@ -79,6 +80,7 @@
<string name="error_loading_files">Error loading files</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="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="folder_deleted_successfully">Folder deleted successfully</string>
<string name="some_items_could_not_be_deleted">Some items could not be deleted</string>
@@ -107,8 +109,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="files_unhidden_successfully">Files unhidden successfully</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="move_to_another_folder">Move to Another Folder</string>
<string name="copy_to_another_folder">Copy</string>
<string name="move_to_another_folder">Move</string>
<string name="select_destination_folder">Select Destination Folder</string>
<string name="no_folders_available">No other folders available</string>
<string name="change_password">Change Password</string>
@@ -120,10 +122,58 @@
<string name="app_settings">App Settings</string>
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
<string name="security_settings">Security Settings</string>
<string name="version">Version 1.3</string>
<string name="app_details">App Details</string>
<string name="setup_password">Setup Password</string>
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
<string name="security_answer">Security Answer</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>

View File

@@ -12,6 +12,7 @@
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="colorControlNormal">#483CFF61</item>
<item name="colorSecondaryContainer">#48BDFFCB</item>
<item name="android:windowLightStatusBar">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"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Allow access to external files -->
<external-path name="external_files" path="." />
<!-- Allow access to hidden directories -->
<external-files-path name="hidden_files" path=".CalculatorHide/" />
<cache-path name="cache_files" path="." />
</paths>

View File

@@ -1,27 +1,34 @@
[versions]
agp = "8.9.1"
documentfile = "1.0.1"
agp = "8.11.1"
documentfile = "1.1.0"
exp4j = "0.4.8"
glide = "4.16.0"
kotlin = "1.9.24"
coreKtx = "1.15.0"
kotlin = "2.0.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
appcompat = "1.7.1"
lottie = "6.2.0"
material = "1.12.0"
activity = "1.9.3"
constraintlayout = "2.2.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
materialColorUtilities = "1.3.0"
gridlayout = "1.0.0"
gridlayout = "1.1.0"
photoview = "2.3.0"
roomRuntime = "2.7.1"
swiperefreshlayout = "1.2.0-beta01"
viewpager = "1.1.0"
zoomage = "1.3.1"
lifecycleProcess = "2.9.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
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-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
@@ -37,6 +44,7 @@ material-color-utilities = { module = "com.google.android.material:material-colo
androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" }
photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoview" }
zoomage = { module = "com.jsibbold:zoomage", version.ref = "zoomage" }
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

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

BIN
media/Screenshot_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
media/Screenshot_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
media/Screenshot_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
media/Screenshot_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
media/Screenshot_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
media/Screenshot_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

BIN
media/Screenshot_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

BIN
media/Screenshot_8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

BIN
media/Screenshot_9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
media/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

BIN
media/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
media/payment_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB