Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf192b6ab9 | ||
|
|
191368bdf8 | ||
|
|
5aafc7e411 | ||
|
|
1f81cccd96 | ||
|
|
eea0f56379 | ||
|
|
e90e3c438f | ||
|
|
480a90b64c | ||
|
|
90a252f227 | ||
|
|
08cdd747ca | ||
|
|
2d10c2f941 | ||
|
|
d840732305 | ||
|
|
7da5885931 | ||
|
|
082dbf17aa | ||
|
|
093c419f34 | ||
|
|
8649ec9ac2 | ||
|
|
527f377ebd | ||
|
|
e1627c426f | ||
|
|
092219d487 | ||
|
|
7803ed2589 | ||
|
|
e40b59c460 | ||
|
|
c25ac613c2 | ||
|
|
bfc339c101 | ||
|
|
657b674ff3 | ||
|
|
ea7c6ea18a | ||
|
|
c97f38ae53 | ||
|
|
0ba60f2bae |
1
.idea/appInsightsSettings.xml
generated
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="AppInsightsSettings">
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="selectedTabId" value="Firebase Crashlytics" />
|
||||||
<option name="tabSettings">
|
<option name="tabSettings">
|
||||||
<map>
|
<map>
|
||||||
<entry key="Firebase Crashlytics">
|
<entry key="Firebase Crashlytics">
|
||||||
|
|||||||
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.24" />
|
<option name="version" value="2.0.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
26
README.md
@@ -49,10 +49,21 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
|||||||
## 🖼️ Screenshots
|
## 🖼️ Screenshots
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="24%">
|
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
|
||||||
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="24%">
|
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="32%">
|
||||||
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hide File App - Passcode Protection" width="24%">
|
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hidle File App - Passcode Protection" width="32%">
|
||||||
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="24%">
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
|
<img src="app/src/main/assets/Screenshot_5.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
|
<img src="app/src/main/assets/Screenshot_6.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="app/src/main/assets/Screenshot_7.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
|
<img src="app/src/main/assets/Screenshot_8.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
|
<img src="app/src/main/assets/Screenshot_9.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -68,7 +79,9 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
|||||||
|
|
||||||
3. **Manage Hidden Files**
|
3. **Manage Hidden Files**
|
||||||
- Add, remove, and restore hidden files.
|
- Add, remove, and restore hidden files.
|
||||||
- Files stay protected even after closing the app.
|
- Files stay protected even after closing the app.
|
||||||
|
- Create Hidden Folders.
|
||||||
|
- Move & Copy Hidden Files Between Hidden Folders.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -78,7 +91,8 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
|||||||
|
|
||||||
### 🔹 Prerequisites
|
### 🔹 Prerequisites
|
||||||
- **Android 6.0 or higher**
|
- **Android 6.0 or higher**
|
||||||
- **Storage permissions enabled**
|
- **Storage permissions enabled**
|
||||||
|
- **Need Manage All Files Permissons For Android 11 and Higher Versions.**
|
||||||
|
|
||||||
### 🔹 Installation Steps
|
### 🔹 Installation Steps
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
id("kotlin-kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -11,8 +12,8 @@ android {
|
|||||||
applicationId = "devs.org.calculator"
|
applicationId = "devs.org.calculator"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 4
|
versionCode = 5
|
||||||
versionName = "1.3"
|
versionName = "1.4.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -80,4 +81,9 @@ dependencies {
|
|||||||
implementation(libs.androidx.viewpager)
|
implementation(libs.androidx.viewpager)
|
||||||
implementation(libs.zoomage)
|
implementation(libs.zoomage)
|
||||||
implementation(libs.lottie)
|
implementation(libs.lottie)
|
||||||
|
|
||||||
|
// Room dependencies
|
||||||
|
implementation(libs.androidx.room.runtime)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
kapt(libs.androidx.room.compiler)
|
||||||
}
|
}
|
||||||
37
app/release/output-metadata.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "devs.org.calculator",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 4,
|
||||||
|
"versionName": "1.3",
|
||||||
|
"outputFile": "app-release.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File",
|
||||||
|
"baselineProfiles": [
|
||||||
|
{
|
||||||
|
"minApi": 28,
|
||||||
|
"maxApi": 30,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/1/app-release.dm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minApi": 31,
|
||||||
|
"maxApi": 2147483647,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/0/app-release.dm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minSdkVersionForDexing": 26
|
||||||
|
}
|
||||||
@@ -59,13 +59,17 @@
|
|||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="devs.org.calculator.fileprovider"
|
android:authorities="devs.org.calculator.provider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.icon"
|
||||||
|
android:resource="@mipmap/ic_launcher_monochrome" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 557 KiB |
BIN
app/src/main/assets/Screenshot_9.jpg
Normal file
|
After Width: | Height: | Size: 228 KiB |
@@ -3,19 +3,14 @@ package devs.org.calculator
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
|
|
||||||
class CalculatorApp : Application() {
|
class CalculatorApp : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
val prefs = PrefsUtil(this)
|
||||||
// Initialize theme settings
|
|
||||||
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
|
||||||
|
|
||||||
// Apply saved theme mode
|
|
||||||
val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
AppCompatDelegate.setDefaultNightMode(themeMode)
|
AppCompatDelegate.setDefaultNightMode(themeMode)
|
||||||
|
|
||||||
// Apply dynamic colors only if dynamic theme is enabled
|
|
||||||
if (prefs.getBoolean("dynamic_theme", true)) {
|
if (prefs.getBoolean("dynamic_theme", true)) {
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
@@ -36,22 +35,20 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
private var folderAdapter: FolderAdapter? = null
|
private var folderAdapter: FolderAdapter? = null
|
||||||
private var listFolderAdapter: ListFolderAdapter? = null
|
private var listFolderAdapter: ListFolderAdapter? = null
|
||||||
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||||
|
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "HiddenActivity"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityHiddenBinding.inflate(layoutInflater)
|
binding = ActivityHiddenBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
fileManager = FileManager(this, this)
|
fileManager = FileManager(this, this)
|
||||||
folderManager = FolderManager(this)
|
folderManager = FolderManager()
|
||||||
dialogUtil = DialogUtil(this)
|
dialogUtil = DialogUtil(this)
|
||||||
|
|
||||||
|
|
||||||
setupInitialUIState()
|
setupInitialUIState()
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
setupBackPressedHandler()
|
setupBackPressedHandler()
|
||||||
@@ -100,20 +97,17 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.folderOrientation.setOnClickListener {
|
binding.folderOrientation.setOnClickListener {
|
||||||
// Switch between grid mode and list mode
|
|
||||||
val currentIsList = PrefsUtil(this).getBoolean("isList", false)
|
val currentIsList = PrefsUtil(this).getBoolean("isList", false)
|
||||||
val newIsList = !currentIsList
|
val newIsList = !currentIsList
|
||||||
|
|
||||||
if (newIsList) {
|
if (newIsList) {
|
||||||
// Switch to list view
|
|
||||||
showListUI()
|
showListUI()
|
||||||
PrefsUtil(this).setBoolean("isList", true)
|
PrefsUtil(this).setBoolean("isList", true)
|
||||||
binding.folderOrientation.setImageResource(R.drawable.ic_grid)
|
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
|
||||||
} else {
|
} else {
|
||||||
// Switch to grid view
|
|
||||||
showGridUI()
|
showGridUI()
|
||||||
PrefsUtil(this).setBoolean("isList", false)
|
PrefsUtil(this).setBoolean("isList", false)
|
||||||
binding.folderOrientation.setImageResource(R.drawable.ic_list)
|
binding.folderOrientation.setIconResource(R.drawable.ic_list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,11 +135,10 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
|
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Log.e(TAG, "Error listing folders: ${e.message}")
|
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,26 +157,25 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle("Enter Folder Name To Create")
|
.setTitle(getString(R.string.enter_folder_name_to_create))
|
||||||
.setView(dialogView)
|
.setView(dialogView)
|
||||||
.setPositiveButton("Create") { dialog, _ ->
|
.setPositiveButton(getString(R.string.create)) { dialog, _ ->
|
||||||
val newName = inputEditText.text.toString().trim()
|
val newName = inputEditText.text.toString().trim()
|
||||||
if (newName.isNotEmpty()) {
|
if (newName.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
folderManager.createFolder(hiddenDir, newName)
|
folderManager.createFolder(hiddenDir, newName)
|
||||||
refreshCurrentView()
|
refreshCurrentView()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Log.e(TAG, "Error creating folder: ${e.message}")
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
this@HiddenActivity,
|
this@HiddenActivity,
|
||||||
"Failed to create folder",
|
getString(R.string.failed_to_create_folder),
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||||
dialog.cancel()
|
dialog.cancel()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -195,7 +187,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupFlagSecure() {
|
private fun setupFlagSecure() {
|
||||||
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
|
||||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||||
window.setFlags(
|
window.setFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
@@ -220,11 +211,9 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
|
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Log.e(TAG, "Error listing folders: ${e.message}")
|
|
||||||
showEmptyState()
|
showEmptyState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,8 +221,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
private fun showFolderList(folders: List<File>) {
|
private fun showFolderList(folders: List<File>) {
|
||||||
binding.noItems.visibility = View.GONE
|
binding.noItems.visibility = View.GONE
|
||||||
binding.recyclerView.visibility = View.VISIBLE
|
binding.recyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
// Clear the existing adapter to avoid conflicts
|
|
||||||
listFolderAdapter = null
|
listFolderAdapter = null
|
||||||
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
|
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
|
||||||
@@ -247,14 +234,13 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
onSelectionModeChanged = { isSelectionMode ->
|
onSelectionModeChanged = { isSelectionMode ->
|
||||||
handleFolderSelectionModeChange(isSelectionMode)
|
handleFolderSelectionModeChange(isSelectionMode)
|
||||||
},
|
},
|
||||||
onSelectionCountChanged = { selectedCount ->
|
onSelectionCountChanged = { _ ->
|
||||||
updateEditButtonVisibility()
|
updateEditButtonVisibility()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
binding.recyclerView.adapter = folderAdapter
|
binding.recyclerView.adapter = folderAdapter
|
||||||
folderAdapter?.submitList(folders)
|
folderAdapter?.submitList(folders)
|
||||||
|
|
||||||
// Ensure proper icon state for folder view
|
|
||||||
if (folderAdapter?.isInSelectionMode() != true) {
|
if (folderAdapter?.isInSelectionMode() != true) {
|
||||||
showFolderViewIcons()
|
showFolderViewIcons()
|
||||||
}
|
}
|
||||||
@@ -262,8 +248,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
private fun showFolderListStyle(folders: List<File>) {
|
private fun showFolderListStyle(folders: List<File>) {
|
||||||
binding.noItems.visibility = View.GONE
|
binding.noItems.visibility = View.GONE
|
||||||
binding.recyclerView.visibility = View.VISIBLE
|
binding.recyclerView.visibility = View.VISIBLE
|
||||||
|
|
||||||
// Clear the existing adapter to avoid conflicts
|
|
||||||
folderAdapter = null
|
folderAdapter = null
|
||||||
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
|
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
|
||||||
@@ -277,14 +261,13 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
onSelectionModeChanged = { isSelectionMode ->
|
onSelectionModeChanged = { isSelectionMode ->
|
||||||
handleFolderSelectionModeChange(isSelectionMode)
|
handleFolderSelectionModeChange(isSelectionMode)
|
||||||
},
|
},
|
||||||
onSelectionCountChanged = { selectedCount ->
|
onSelectionCountChanged = { _ ->
|
||||||
updateEditButtonVisibility()
|
updateEditButtonVisibility()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
binding.recyclerView.adapter = listFolderAdapter
|
binding.recyclerView.adapter = listFolderAdapter
|
||||||
listFolderAdapter?.submitList(folders)
|
listFolderAdapter?.submitList(folders)
|
||||||
|
|
||||||
// Ensure proper icon state for folder view
|
|
||||||
if (listFolderAdapter?.isInSelectionMode() != true) {
|
if (listFolderAdapter?.isInSelectionMode() != true) {
|
||||||
showFolderViewIcons()
|
showFolderViewIcons()
|
||||||
}
|
}
|
||||||
@@ -312,10 +295,10 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
private fun refreshCurrentView() {
|
private fun refreshCurrentView() {
|
||||||
val isList = PrefsUtil(this).getBoolean("isList", false)
|
val isList = PrefsUtil(this).getBoolean("isList", false)
|
||||||
if (isList) {
|
if (isList) {
|
||||||
binding.folderOrientation.setImageResource(R.drawable.ic_grid)
|
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
|
||||||
listFoldersInHiddenDirectoryListStyle()
|
listFoldersInHiddenDirectoryListStyle()
|
||||||
} else {
|
} else {
|
||||||
binding.folderOrientation.setImageResource(R.drawable.ic_list)
|
binding.folderOrientation.setIconResource(R.drawable.ic_list)
|
||||||
listFoldersInHiddenDirectory()
|
listFoldersInHiddenDirectory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +318,7 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
if (selectedFolders.isNotEmpty()) {
|
if (selectedFolders.isNotEmpty()) {
|
||||||
dialogUtil.showMaterialDialog(
|
dialogUtil.showMaterialDialog(
|
||||||
getString(R.string.delete_items),
|
getString(R.string.delete_items),
|
||||||
getString(R.string.are_you_sure_you_want_to_delete_selected_items),
|
"${getString(R.string.are_you_sure_you_want_to_delete_selected_items)}\n ${getString(R.string.folder_will_be_deleted_permanently)}" ,
|
||||||
getString(R.string.delete),
|
getString(R.string.delete),
|
||||||
getString(R.string.cancel),
|
getString(R.string.cancel),
|
||||||
object : DialogUtil.DialogCallback {
|
object : DialogUtil.DialogCallback {
|
||||||
@@ -344,11 +327,11 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
override fun onNegativeButtonClicked() {
|
||||||
// Do nothing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
override fun onNaturalButtonClicked() {
|
||||||
// Do nothing
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -360,7 +343,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
selectedFolders.forEach { folder ->
|
selectedFolders.forEach { folder ->
|
||||||
if (!folderManager.deleteFolder(folder)) {
|
if (!folderManager.deleteFolder(folder)) {
|
||||||
allDeleted = false
|
allDeleted = false
|
||||||
Log.e(TAG, "Failed to delete folder: ${folder.name}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,26 +354,20 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
// Clear selection from both adapters
|
|
||||||
folderAdapter?.clearSelection()
|
folderAdapter?.clearSelection()
|
||||||
listFolderAdapter?.clearSelection()
|
listFolderAdapter?.clearSelection()
|
||||||
|
|
||||||
// This will trigger the selection mode change callback and show proper icons
|
|
||||||
exitFolderSelectionMode()
|
exitFolderSelectionMode()
|
||||||
|
|
||||||
// Refresh the current view based on orientation
|
|
||||||
refreshCurrentView()
|
refreshCurrentView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBackPress() {
|
private fun handleBackPress() {
|
||||||
|
|
||||||
|
|
||||||
// Check if folder adapters are in selection mode
|
|
||||||
if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
|
if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation back
|
|
||||||
if (currentFolder != null) {
|
if (currentFolder != null) {
|
||||||
navigateBackToFolders()
|
navigateBackToFolders()
|
||||||
} else {
|
} else {
|
||||||
@@ -402,25 +378,18 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
private fun navigateBackToFolders() {
|
private fun navigateBackToFolders() {
|
||||||
currentFolder = null
|
currentFolder = null
|
||||||
|
|
||||||
// Clean up file adapter
|
|
||||||
|
|
||||||
refreshCurrentView()
|
refreshCurrentView()
|
||||||
|
|
||||||
binding.folderName.text = getString(R.string.hidden_space)
|
binding.folderName.text = getString(R.string.hidden_space)
|
||||||
|
|
||||||
// Set proper icons for folder view
|
|
||||||
showFolderViewIcons()
|
showFolderViewIcons()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
|
||||||
// Remove any pending callbacks
|
|
||||||
mainHandler.removeCallbacksAndMessages(null)
|
mainHandler.removeCallbacksAndMessages(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
//visibility related code
|
|
||||||
private fun showFolderViewIcons() {
|
private fun showFolderViewIcons() {
|
||||||
binding.folderOrientation.visibility = View.VISIBLE
|
binding.folderOrientation.visibility = View.VISIBLE
|
||||||
binding.settings.visibility = View.VISIBLE
|
binding.settings.visibility = View.VISIBLE
|
||||||
@@ -429,7 +398,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
binding.menuButton.visibility = View.GONE
|
binding.menuButton.visibility = View.GONE
|
||||||
binding.addFolder.visibility = View.VISIBLE
|
binding.addFolder.visibility = View.VISIBLE
|
||||||
binding.edit.visibility = View.GONE
|
binding.edit.visibility = View.GONE
|
||||||
// Ensure FABs are properly managed
|
|
||||||
if (currentFolder == null) {
|
if (currentFolder == null) {
|
||||||
|
|
||||||
binding.addFolder.visibility = View.VISIBLE
|
binding.addFolder.visibility = View.VISIBLE
|
||||||
@@ -442,8 +410,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
binding.deleteSelected.visibility = View.VISIBLE
|
binding.deleteSelected.visibility = View.VISIBLE
|
||||||
binding.menuButton.visibility = View.GONE
|
binding.menuButton.visibility = View.GONE
|
||||||
binding.addFolder.visibility = View.GONE
|
binding.addFolder.visibility = View.GONE
|
||||||
|
|
||||||
// Update edit button visibility based on current selection count
|
|
||||||
updateEditButtonVisibility()
|
updateEditButtonVisibility()
|
||||||
}
|
}
|
||||||
private fun exitFolderSelectionMode() {
|
private fun exitFolderSelectionMode() {
|
||||||
@@ -456,7 +422,6 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
enterFolderSelectionMode()
|
enterFolderSelectionMode()
|
||||||
}
|
}
|
||||||
// Always update edit button visibility when selection mode changes
|
|
||||||
updateEditButtonVisibility()
|
updateEditButtonVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,7 +433,8 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFolders.size != 1) {
|
if (selectedFolders.size != 1) {
|
||||||
Toast.makeText(this, "Please select exactly one folder to edit", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this,
|
||||||
|
getString(R.string.please_select_exactly_one_folder_to_edit), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,20 +449,21 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
inputEditText.selectAll()
|
inputEditText.selectAll()
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle("Rename Folder")
|
.setTitle(getString(R.string.rename_folder))
|
||||||
.setView(dialogView)
|
.setView(dialogView)
|
||||||
.setPositiveButton("Rename") { dialog, _ ->
|
.setPositiveButton(getString(R.string.rename)) { dialog, _ ->
|
||||||
val newName = inputEditText.text.toString().trim()
|
val newName = inputEditText.text.toString().trim()
|
||||||
if (newName.isNotEmpty() && newName != folder.name) {
|
if (newName.isNotEmpty() && newName != folder.name) {
|
||||||
if (isValidFolderName(newName)) {
|
if (isValidFolderName(newName)) {
|
||||||
renameFolder(folder, newName)
|
renameFolder(folder, newName)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "Invalid folder name", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this,
|
||||||
|
getString(R.string.invalid_folder_name), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||||
dialog.cancel()
|
dialog.cancel()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -515,21 +482,19 @@ class HiddenActivity : AppCompatActivity() {
|
|||||||
if (parentDir != null) {
|
if (parentDir != null) {
|
||||||
val newFolder = File(parentDir, newName)
|
val newFolder = File(parentDir, newName)
|
||||||
if (newFolder.exists()) {
|
if (newFolder.exists()) {
|
||||||
Toast.makeText(this, "Folder with this name already exists", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this,
|
||||||
|
getString(R.string.folder_with_this_name_already_exists), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldFolder.renameTo(newFolder)) {
|
if (oldFolder.renameTo(newFolder)) {
|
||||||
// Clear selection from both adapters
|
|
||||||
folderAdapter?.clearSelection()
|
folderAdapter?.clearSelection()
|
||||||
listFolderAdapter?.clearSelection()
|
listFolderAdapter?.clearSelection()
|
||||||
|
|
||||||
// Exit selection mode
|
|
||||||
exitFolderSelectionMode()
|
exitFolderSelectionMode()
|
||||||
|
|
||||||
refreshCurrentView()
|
refreshCurrentView()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "Failed to rename folder", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.failed_to_rename_folder), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.os.Bundle
|
|||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -22,6 +23,7 @@ import devs.org.calculator.utils.FileManager
|
|||||||
import devs.org.calculator.utils.PrefsUtil
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
import net.objecthunter.exp4j.ExpressionBuilder
|
import net.objecthunter.exp4j.ExpressionBuilder
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
|
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -40,7 +42,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
enableEdgeToEdge()
|
||||||
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
handleActivityResult(result)
|
handleActivityResult(result)
|
||||||
}
|
}
|
||||||
@@ -49,15 +51,14 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
binding.display.text = getString(R.string.enter_123456)
|
binding.display.text = getString(R.string.enter_123456)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask permission
|
|
||||||
if(!Environment.isExternalStorageManager()) {
|
if(!Environment.isExternalStorageManager()) {
|
||||||
dialogUtil.showMaterialDialog(
|
dialogUtil.showMaterialDialog(
|
||||||
"Storage Permission",
|
getString(R.string.storage_permission),
|
||||||
"To ensure the app works properly and allows you to easily hide or un-hide your private files, please grant storage access permission.\n" +
|
getString(R.string.to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission) +
|
||||||
"\n" +
|
"\n" +
|
||||||
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.",
|
getString(R.string.for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission),
|
||||||
"Grant",
|
getString(R.string.grant_permission),
|
||||||
"Later",
|
getString(R.string.later),
|
||||||
object : DialogUtil.DialogCallback {
|
object : DialogUtil.DialogCallback {
|
||||||
override fun onPositiveButtonClicked() {
|
override fun onPositiveButtonClicked() {
|
||||||
fileManager.askPermission(this@MainActivity)
|
fileManager.askPermission(this@MainActivity)
|
||||||
@@ -65,19 +66,20 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
override fun onNegativeButtonClicked() {
|
||||||
Toast.makeText(this@MainActivity,
|
Toast.makeText(this@MainActivity,
|
||||||
"Storage permission is required for the app to function properly",
|
getString(R.string.storage_permission_is_required_for_the_app_to_function_properly),
|
||||||
Toast.LENGTH_LONG).show()
|
Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
override fun onNaturalButtonClicked() {
|
||||||
Toast.makeText(this@MainActivity,
|
Toast.makeText(this@MainActivity,
|
||||||
"You can grant permission later from Settings",
|
getString(R.string.you_can_grant_permission_later_from_settings),
|
||||||
Toast.LENGTH_LONG).show()
|
Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setupNumberButton(binding.btn0, "0")
|
setupNumberButton(binding.btn0, "0")
|
||||||
|
setupNumberButton(binding.btn00, "00")
|
||||||
setupNumberButton(binding.btn1, "1")
|
setupNumberButton(binding.btn1, "1")
|
||||||
setupNumberButton(binding.btn2, "2")
|
setupNumberButton(binding.btn2, "2")
|
||||||
setupNumberButton(binding.btn3, "3")
|
setupNumberButton(binding.btn3, "3")
|
||||||
@@ -99,6 +101,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
binding.cut.setOnClickListener { cutNumbers() }
|
binding.cut.setOnClickListener { cutNumbers() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
|
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
|
||||||
if (result.resultCode == RESULT_OK) {
|
if (result.resultCode == RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { uri ->
|
||||||
@@ -107,10 +110,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE)
|
val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE)
|
||||||
preferences.edit().putString("filestorageuri", uri.toString()).apply()
|
preferences.edit { putString("filestorageuri", uri.toString()) }
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.e("FileUtility", "Error occurred or operation cancelled: $result")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearDisplay() {
|
private fun clearDisplay() {
|
||||||
currentExpression = "0"
|
currentExpression = ""
|
||||||
binding.total.text = ""
|
binding.total.text = ""
|
||||||
lastWasOperator = false
|
lastWasOperator = false
|
||||||
lastWasPercent = false
|
lastWasPercent = false
|
||||||
@@ -173,41 +174,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculatePercentage() {
|
|
||||||
try {
|
|
||||||
val value = currentExpression.toDouble()
|
|
||||||
currentExpression = (value / 100).toString()
|
|
||||||
updateDisplay()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
binding.display.text = getString(R.string.invalid_message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun preprocessExpression(expression: String): String {
|
private fun preprocessExpression(expression: String): String {
|
||||||
val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%")
|
val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%")
|
||||||
val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%")
|
val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%")
|
||||||
|
|
||||||
var processedExpression = expression
|
var processedExpression = expression
|
||||||
|
|
||||||
// Replace standalone percentages (like "50%") with their decimal form (0.5)
|
|
||||||
val matcher = percentagePattern.matcher(processedExpression)
|
val matcher = percentagePattern.matcher(processedExpression)
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
val fullMatch = matcher.group(0)
|
val fullMatch = matcher.group(0)
|
||||||
val number = matcher.group(1)
|
val number = matcher.group(1)
|
||||||
|
|
||||||
// Check if it's a standalone percentage or part of an operation
|
|
||||||
val start = matcher.start()
|
val start = matcher.start()
|
||||||
if (start == 0 || !isOperator(processedExpression[start-1].toString())) {
|
if (start == 0 || !isOperator(processedExpression[start-1].toString())) {
|
||||||
val percentageValue = number.toDouble() / 100
|
val percentageValue = number!!.toDouble() / 100
|
||||||
processedExpression = processedExpression.replace(fullMatch, percentageValue.toString())
|
processedExpression = processedExpression.replace(fullMatch!!.toString(), percentageValue.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle operator-percentage combinations (like "100-20%")
|
|
||||||
val opMatcher = operatorPercentPattern.matcher(processedExpression)
|
val opMatcher = operatorPercentPattern.matcher(processedExpression)
|
||||||
val sb = StringBuilder(processedExpression)
|
val sb = StringBuilder(processedExpression)
|
||||||
|
|
||||||
// We need to process matches from right to left to maintain indices
|
|
||||||
val matches = mutableListOf<Triple<Int, Int, String>>()
|
val matches = mutableListOf<Triple<Int, Int, String>>()
|
||||||
|
|
||||||
while (opMatcher.find()) {
|
while (opMatcher.find()) {
|
||||||
@@ -219,15 +203,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
matches.add(Triple(start, end, "$operator$percentValue%"))
|
matches.add(Triple(start, end, "$operator$percentValue%"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process matches from right to left
|
|
||||||
for (match in matches.reversed()) {
|
for (match in matches.reversed()) {
|
||||||
val (start, end, fullMatch) = match
|
val (start, end, fullMatch) = match
|
||||||
|
|
||||||
// Find the number before this operator
|
|
||||||
var leftNumberEnd = start
|
|
||||||
var leftNumberStart = start - 1
|
var leftNumberStart = start - 1
|
||||||
|
|
||||||
// Skip parentheses and move to the actual number
|
|
||||||
if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') {
|
if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') {
|
||||||
var openParens = 1
|
var openParens = 1
|
||||||
leftNumberStart--
|
leftNumberStart--
|
||||||
@@ -238,7 +218,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
leftNumberStart--
|
leftNumberStart--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we need to find the start of the expression
|
|
||||||
if (leftNumberStart >= 0) {
|
if (leftNumberStart >= 0) {
|
||||||
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) {
|
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) {
|
||||||
leftNumberStart--
|
leftNumberStart--
|
||||||
@@ -248,26 +227,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
leftNumberStart = 0
|
leftNumberStart = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For simple numbers, just find the start of the number
|
|
||||||
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) {
|
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) {
|
||||||
leftNumberStart--
|
leftNumberStart--
|
||||||
}
|
}
|
||||||
leftNumberStart++
|
leftNumberStart++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftNumberStart < leftNumberEnd) {
|
if (leftNumberStart < start) {
|
||||||
val leftPart = sb.substring(leftNumberStart, leftNumberEnd)
|
val leftPart = sb.substring(leftNumberStart, start)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the numerical values
|
|
||||||
val baseNumber = evaluateExpression(leftPart)
|
val baseNumber = evaluateExpression(leftPart)
|
||||||
val operator = fullMatch.substring(0, 1)
|
val operator = fullMatch.substring(0, 1)
|
||||||
val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble()
|
val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble()
|
||||||
|
|
||||||
// Calculate the percentage of the base number
|
|
||||||
val percentValue = baseNumber * (percentNumber / 100)
|
val percentValue = baseNumber * (percentNumber / 100)
|
||||||
|
|
||||||
// Calculate the new value based on the operator
|
|
||||||
val newValue = when (operator) {
|
val newValue = when (operator) {
|
||||||
"+" -> baseNumber + percentValue
|
"+" -> baseNumber + percentValue
|
||||||
"-" -> baseNumber - percentValue
|
"-" -> baseNumber - percentValue
|
||||||
@@ -276,7 +253,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
else -> baseNumber
|
else -> baseNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the entire expression "number operator percent%" with the result
|
|
||||||
sb.replace(leftNumberStart, end, newValue.toString())
|
sb.replace(leftNumberStart, end, newValue.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Calculator", "Error processing percentage expression: $e")
|
Log.e("Calculator", "Error processing percentage expression: $e")
|
||||||
@@ -298,15 +274,16 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
private fun evaluateExpression(expression: String): Double {
|
private fun evaluateExpression(expression: String): Double {
|
||||||
return try {
|
return try {
|
||||||
ExpressionBuilder(expression).build().evaluate()
|
ExpressionBuilder(expression).build().evaluate()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
expression.toDouble()
|
expression.toDouble()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
private fun calculateResult() {
|
private fun calculateResult() {
|
||||||
if (currentExpression == "123456") {
|
if (currentExpression == "123456") {
|
||||||
val intent = Intent(this, SetupPasswordActivity::class.java)
|
val intent = Intent(this, SetupPasswordActivity::class.java)
|
||||||
sp.edit().putBoolean("isFirst", false).apply()
|
sp.edit { putBoolean("isFirst", false) }
|
||||||
intent.putExtra("password", currentExpression)
|
intent.putExtra("password", currentExpression)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
clearDisplay()
|
clearDisplay()
|
||||||
@@ -363,8 +340,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
binding.total.text = ""
|
binding.total.text = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the expression for preview calculation
|
|
||||||
var processedExpression = currentExpression.replace("×", "*")
|
var processedExpression = currentExpression.replace("×", "*")
|
||||||
|
|
||||||
if (processedExpression.contains("%")) {
|
if (processedExpression.contains("%")) {
|
||||||
@@ -382,7 +357,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
|
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
|
||||||
binding.total.text = getString(R.string.now_enter_button)
|
binding.total.text = getString(R.string.now_enter_button)
|
||||||
}else binding.total.text = formattedResult
|
}else binding.total.text = formattedResult
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
binding.total.text = ""
|
binding.total.text = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,7 +370,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
val lastChar = currentExpression.last()
|
val lastChar = currentExpression.last()
|
||||||
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
|
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
|
||||||
|
|
||||||
// Update flags based on what was removed
|
|
||||||
if (lastChar == '%') {
|
if (lastChar == '%') {
|
||||||
lastWasPercent = false
|
lastWasPercent = false
|
||||||
} else if (isOperator(lastChar.toString())) {
|
} else if (isOperator(lastChar.toString())) {
|
||||||
@@ -411,27 +385,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.Dial
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositiveButtonClicked() {
|
override fun onPositiveButtonClicked() {
|
||||||
// Handle positive button click for both DialogUtil and DialogActionsCallback
|
|
||||||
fileManager.askPermission(this)
|
fileManager.askPermission(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
override fun onNegativeButtonClicked() {
|
||||||
// Handle negative button click
|
Toast.makeText(this, getString(R.string.storage_permission_is_required_for_the_app_to_function_properly), Toast.LENGTH_LONG).show()
|
||||||
Toast.makeText(this, "Storage permission is required for the app to function properly", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
override fun onNaturalButtonClicked() {
|
||||||
// Handle neutral button click
|
Toast.makeText(this, getString(R.string.you_can_grant_permission_later_from_settings), Toast.LENGTH_LONG).show()
|
||||||
Toast.makeText(this, "You can grant permission later from Settings", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
if (requestCode == 6767) {
|
if (requestCode == 6767) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.permission_granted), Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,22 @@ package devs.org.calculator.activities
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
import devs.org.calculator.adapters.ImagePreviewAdapter
|
import devs.org.calculator.adapters.ImagePreviewAdapter
|
||||||
|
import devs.org.calculator.database.AppDatabase
|
||||||
|
import devs.org.calculator.database.HiddenFileRepository
|
||||||
import devs.org.calculator.databinding.ActivityPreviewBinding
|
import devs.org.calculator.databinding.ActivityPreviewBinding
|
||||||
import devs.org.calculator.utils.DialogUtil
|
import devs.org.calculator.utils.DialogUtil
|
||||||
import devs.org.calculator.utils.FileManager
|
import devs.org.calculator.utils.FileManager
|
||||||
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
|
import devs.org.calculator.utils.SecurityUtils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -18,43 +25,35 @@ class PreviewActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var binding: ActivityPreviewBinding
|
private lateinit var binding: ActivityPreviewBinding
|
||||||
private var currentPosition: Int = 0
|
private var currentPosition: Int = 0
|
||||||
private lateinit var files: List<File>
|
private var files: MutableList<File> = mutableListOf()
|
||||||
private lateinit var type: String
|
private lateinit var type: String
|
||||||
private lateinit var folder: String
|
private lateinit var folder: String
|
||||||
private lateinit var filetype: FileManager.FileType
|
private lateinit var filetype: FileManager.FileType
|
||||||
private lateinit var adapter: ImagePreviewAdapter
|
private lateinit var adapter: ImagePreviewAdapter
|
||||||
private lateinit var fileManager: FileManager
|
private lateinit var fileManager: FileManager
|
||||||
private val dialogUtil = DialogUtil(this)
|
private val dialogUtil = DialogUtil(this)
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val prefs: PrefsUtil by lazy { PrefsUtil(this) }
|
||||||
|
private val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
|
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityPreviewBinding.inflate(layoutInflater)
|
binding = ActivityPreviewBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|
||||||
fileManager = FileManager(this, this)
|
fileManager = FileManager(this, this)
|
||||||
|
|
||||||
currentPosition = intent.getIntExtra("position", 0)
|
currentPosition = intent.getIntExtra("position", 0)
|
||||||
type = intent.getStringExtra("type").toString()
|
type = intent.getStringExtra("type") ?: "IMAGE"
|
||||||
folder = intent.getStringExtra("folder").toString()
|
folder = intent.getStringExtra("folder") ?: ""
|
||||||
|
|
||||||
setupFileType()
|
setupFileType()
|
||||||
files = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
|
loadFiles()
|
||||||
|
|
||||||
setupImagePreview()
|
setupImagePreview()
|
||||||
clickListeners()
|
setupClickListeners()
|
||||||
|
setupPageChangeCallback()
|
||||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
super.onPageSelected(position)
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.back.setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -62,8 +61,17 @@ class PreviewActivity : AppCompatActivity() {
|
|||||||
setupFlagSecure()
|
setupFlagSecure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
adapter.releaseAllResources()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
adapter.releaseAllResources()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupFlagSecure() {
|
private fun setupFlagSecure() {
|
||||||
val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
|
||||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||||
window.setFlags(
|
window.setFlags(
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
@@ -78,14 +86,17 @@ class PreviewActivity : AppCompatActivity() {
|
|||||||
filetype = FileManager.FileType.IMAGE
|
filetype = FileManager.FileType.IMAGE
|
||||||
binding.title.text = getString(R.string.preview_images)
|
binding.title.text = getString(R.string.preview_images)
|
||||||
}
|
}
|
||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
filetype = FileManager.FileType.VIDEO
|
filetype = FileManager.FileType.VIDEO
|
||||||
binding.title.text = getString(R.string.preview_videos)
|
binding.title.text = getString(R.string.preview_videos)
|
||||||
}
|
}
|
||||||
|
|
||||||
"AUDIO" -> {
|
"AUDIO" -> {
|
||||||
filetype = FileManager.FileType.AUDIO
|
filetype = FileManager.FileType.AUDIO
|
||||||
binding.title.text = getString(R.string.preview_audios)
|
binding.title.text = getString(R.string.preview_audios)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
filetype = FileManager.FileType.DOCUMENT
|
filetype = FileManager.FileType.DOCUMENT
|
||||||
binding.title.text = getString(R.string.preview_documents)
|
binding.title.text = getString(R.string.preview_documents)
|
||||||
@@ -93,108 +104,243 @@ class PreviewActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadFiles() {
|
||||||
|
try {
|
||||||
|
val filesList = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
|
||||||
|
files = filesList.toMutableList()
|
||||||
|
|
||||||
|
if (currentPosition >= files.size) {
|
||||||
|
currentPosition = 0
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
files = mutableListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupImagePreview() {
|
private fun setupImagePreview() {
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
adapter = ImagePreviewAdapter(this, this)
|
adapter = ImagePreviewAdapter(this, this)
|
||||||
adapter.images = files
|
adapter.images = files
|
||||||
binding.viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
|
if (currentPosition < files.size) {
|
||||||
binding.viewPager.setCurrentItem(currentPosition, false)
|
binding.viewPager.setCurrentItem(currentPosition, false)
|
||||||
|
|
||||||
val fileUri = Uri.fromFile(files[currentPosition])
|
|
||||||
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
if (filetype == FileManager.FileType.AUDIO) {
|
|
||||||
(binding.viewPager.adapter as? ImagePreviewAdapter)?.currentMediaPlayer?.pause()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFileInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
private fun setupPageChangeCallback() {
|
||||||
super.onDestroy()
|
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
if (filetype == FileManager.FileType.AUDIO) {
|
override fun onPageSelected(position: Int) {
|
||||||
(binding.viewPager.adapter as? ImagePreviewAdapter)?.let { adapter ->
|
super.onPageSelected(position)
|
||||||
adapter.currentMediaPlayer?.release()
|
currentPosition = position
|
||||||
adapter.currentMediaPlayer = null
|
updateFileInfo()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFileInfo() {
|
||||||
|
if (files.isNotEmpty() && currentPosition < files.size) {
|
||||||
|
val fileUri = Uri.fromFile(files[currentPosition])
|
||||||
|
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri) ?: "Unknown"
|
||||||
|
//For Now File Name not Needed, i am keeping it for later use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
binding.back.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
private fun clickListeners() {
|
|
||||||
binding.delete.setOnClickListener {
|
binding.delete.setOnClickListener {
|
||||||
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem])
|
handleDeleteFile()
|
||||||
if (fileUri != null) {
|
|
||||||
dialogUtil.showMaterialDialog(
|
|
||||||
getString(R.string.delete_file),
|
|
||||||
getString(R.string.are_you_sure_to_delete_this_file_permanently),
|
|
||||||
getString(R.string.delete_permanently),
|
|
||||||
getString(R.string.cancel),
|
|
||||||
object : DialogUtil.DialogCallback {
|
|
||||||
override fun onPositiveButtonClicked() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
FileManager(this@PreviewActivity, this@PreviewActivity).deletePhotoFromExternalStorage(fileUri)
|
|
||||||
removeFileFromList(binding.viewPager.currentItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
|
||||||
// Handle negative button click
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
|
||||||
// Handle neutral button click
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.unHide.setOnClickListener {
|
binding.unHide.setOnClickListener {
|
||||||
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem])
|
performFileUnHiding()
|
||||||
if (fileUri != null) {
|
}
|
||||||
dialogUtil.showMaterialDialog(
|
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
getString(R.string.un_hide_file),
|
override fun onPageSelected(position: Int) {
|
||||||
getString(R.string.are_you_sure_you_want_to_un_hide_this_file),
|
super.onPageSelected(position)
|
||||||
getString(R.string.un_hide),
|
adapter.onItemScrolledAway(currentPosition)
|
||||||
getString(R.string.cancel),
|
currentPosition = position
|
||||||
object : DialogUtil.DialogCallback {
|
}
|
||||||
override fun onPositiveButtonClicked() {
|
})
|
||||||
lifecycleScope.launch {
|
}
|
||||||
FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri)
|
|
||||||
removeFileFromList(binding.viewPager.currentItem)
|
private fun handleDeleteFile() {
|
||||||
|
if (files.isEmpty() || currentPosition >= files.size) return
|
||||||
|
|
||||||
|
val currentFile = files[currentPosition]
|
||||||
|
val fileUri = FileManager.FileManager().getContentUriImage(this, currentFile)
|
||||||
|
|
||||||
|
if (fileUri != null) {
|
||||||
|
dialogUtil.showMaterialDialog(
|
||||||
|
getString(R.string.delete_file),
|
||||||
|
getString(R.string.are_you_sure_to_delete_this_file_permanently),
|
||||||
|
getString(R.string.delete_permanently),
|
||||||
|
getString(R.string.cancel),
|
||||||
|
object : DialogUtil.DialogCallback {
|
||||||
|
override fun onPositiveButtonClicked() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val hiddenFile =
|
||||||
|
hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
|
||||||
|
hiddenFile?.let {
|
||||||
|
hiddenFileRepository.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileManager.deletePhotoFromExternalStorage(fileUri)
|
||||||
|
removeFileFromList(currentPosition)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
|
||||||
// Handle negative button click
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
|
||||||
// Handle neutral button click
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
override fun onNegativeButtonClicked() {}
|
||||||
|
|
||||||
|
override fun onNaturalButtonClicked() {}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun performFileUnHiding() {
|
||||||
|
if (files.isEmpty() || currentPosition >= files.size) return
|
||||||
|
val file = files[currentPosition]
|
||||||
|
|
||||||
|
dialogUtil.showMaterialDialog(
|
||||||
|
getString(R.string.un_hide_file),
|
||||||
|
getString(R.string.are_you_sure_you_want_to_un_hide_this_file),
|
||||||
|
getString(R.string.un_hide),
|
||||||
|
getString(R.string.cancel),
|
||||||
|
object : DialogUtil.DialogCallback {
|
||||||
|
override fun onPositiveButtonClicked() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
var allUnhidden = true
|
||||||
|
try {
|
||||||
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
|
if (hiddenFile != null){
|
||||||
|
if (hiddenFile.isEncrypted) {
|
||||||
|
val originalExtension = hiddenFile.originalExtension
|
||||||
|
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||||
|
|
||||||
|
if (SecurityUtils.decryptFile(this@PreviewActivity, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
val fileUri = FileManager.FileManager()
|
||||||
|
.getContentUriImage(this@PreviewActivity, decryptedFile)
|
||||||
|
if (fileUri != null) {
|
||||||
|
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||||
|
if (result != null) {
|
||||||
|
hiddenFile.let {
|
||||||
|
hiddenFileRepository.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
decryptedFile.delete()
|
||||||
|
removeFileFromList(currentPosition)
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
|
||||||
|
if (fileUri != null) {
|
||||||
|
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||||
|
if (result != null) {
|
||||||
|
hiddenFile.let {
|
||||||
|
hiddenFileRepository.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
removeFileFromList(currentPosition)
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
|
||||||
|
if (fileUri != null) {
|
||||||
|
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||||
|
if (result != null) {
|
||||||
|
file.delete()
|
||||||
|
removeFileFromList(currentPosition)
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
val message = if (allUnhidden) {
|
||||||
|
getString(R.string.file_unhidden_successfully)
|
||||||
|
} else {
|
||||||
|
getString(R.string.something_went_wrong)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(this@PreviewActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNegativeButtonClicked() {}
|
||||||
|
|
||||||
|
override fun onNaturalButtonClicked() {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeFileFromList(position: Int) {
|
private fun removeFileFromList(position: Int) {
|
||||||
val updatedFiles = files.toMutableList().apply { removeAt(position) }
|
if (position < 0 || position >= files.size) return
|
||||||
files = updatedFiles
|
adapter.releaseAllResources()
|
||||||
adapter.images = updatedFiles // Update adapter with the new list
|
files.removeAt(position)
|
||||||
|
adapter.images = files
|
||||||
// Update the ViewPager's position
|
if (files.isEmpty()) {
|
||||||
if (updatedFiles.isEmpty()) finish()
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPosition = if (position >= files.size) {
|
||||||
|
files.size - 1
|
||||||
|
} else {
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.viewPager.setCurrentItem(currentPosition, false)
|
||||||
|
updateFileInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
onBackPressed()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
package devs.org.calculator.activities
|
package devs.org.calculator.activities
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.enableEdgeToEdge
|
import android.widget.EditText
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.WindowCompat
|
import com.google.android.material.color.DynamicColors
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
import devs.org.calculator.databinding.ActivitySettingsBinding
|
import devs.org.calculator.databinding.ActivitySettingsBinding
|
||||||
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
|
import devs.org.calculator.utils.SecurityUtils
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
private lateinit var binding: ActivitySettingsBinding
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var prefs: PrefsUtil
|
||||||
private val DEV_GITHUB_URL = "https://github.com/binondi"
|
private var DEV_GITHUB_URL = ""
|
||||||
private val GITHUB_URL = "$DEV_GITHUB_URL/calculator-hide-files"
|
private var GITHUB_URL = ""
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
prefs = PrefsUtil(this)
|
||||||
prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
DEV_GITHUB_URL = getString(R.string.github_profile)
|
||||||
|
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
|
||||||
setupUI()
|
setupUI()
|
||||||
loadSettings()
|
loadSettings()
|
||||||
setupListeners()
|
setupListeners()
|
||||||
@@ -42,6 +42,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
|
|
||||||
binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
|
binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
|
||||||
@@ -55,8 +56,11 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
else -> binding.systemThemeRadio.isChecked = true
|
else -> binding.systemThemeRadio.isChecked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
|
||||||
|
binding.customKeyStatus.isChecked = isUsingCustomKey
|
||||||
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
|
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
|
||||||
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
||||||
|
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
|
||||||
|
|
||||||
updateThemeModeVisibility()
|
updateThemeModeVisibility()
|
||||||
}
|
}
|
||||||
@@ -72,11 +76,19 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
prefs.edit().putBoolean("dynamic_theme", isChecked).apply()
|
prefs.setBoolean("dynamic_theme", isChecked)
|
||||||
if (!isChecked) {
|
if (!isChecked) {
|
||||||
showThemeModeDialog()
|
showThemeModeDialog()
|
||||||
|
}else{
|
||||||
|
showThemeModeDialog()
|
||||||
|
if (!prefs.getBoolean("isAppReopened",false)){
|
||||||
|
DynamicColors.applyToActivityIfAvailable(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.encryptionSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
prefs.setBoolean("encryption", isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
@@ -97,7 +109,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
|
binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
prefs.edit().putBoolean("screenshot_restriction", isChecked).apply()
|
prefs.setBoolean("screenshot_restriction", isChecked)
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
enableScreenshotRestriction()
|
enableScreenshotRestriction()
|
||||||
} else {
|
} else {
|
||||||
@@ -105,7 +117,11 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
|
binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
|
||||||
prefs.edit().putBoolean("showFileName", isChecked).apply()
|
prefs.setBoolean("showFileName", isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.customKeyStatus.setOnClickListener {
|
||||||
|
showCustomKeyDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,20 +131,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun showThemeModeDialog() {
|
private fun showThemeModeDialog() {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle("Theme Mode")
|
.setTitle(getString(R.string.attention))
|
||||||
.setMessage("Would you like to set a specific theme mode?")
|
.setMessage(getString(R.string.if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app))
|
||||||
.setPositiveButton("Yes") { _, _ ->
|
.setPositiveButton(getString(R.string.ok)) { _, _ ->
|
||||||
binding.themeModeSwitch.isChecked = true
|
|
||||||
}
|
|
||||||
.setNegativeButton("No") { _, _ ->
|
|
||||||
binding.systemThemeRadio.isChecked = true
|
|
||||||
applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyThemeMode(themeMode: Int) {
|
private fun applyThemeMode(themeMode: Int) {
|
||||||
prefs.edit().putInt("theme_mode", themeMode).apply()
|
prefs.setInt("theme_mode", themeMode)
|
||||||
AppCompatDelegate.setDefaultNightMode(themeMode)
|
AppCompatDelegate.setDefaultNightMode(themeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +157,64 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun openUrl(url: String) {
|
private fun openUrl(url: String) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Snackbar.make(binding.root, "Could not open URL", Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root,
|
||||||
|
getString(R.string.could_not_open_url), Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showCustomKeyDialog() {
|
||||||
|
val dialogView = layoutInflater.inflate(R.layout.dialog_custom_key, null)
|
||||||
|
val keyInput = dialogView.findViewById<EditText>(R.id.keyInput)
|
||||||
|
val confirmKeyInput = dialogView.findViewById<EditText>(R.id.confirmKeyInput)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(getString(R.string.set_custom_encryption_key))
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton(getString(R.string.set)) { dialog, _ ->
|
||||||
|
val key = keyInput.text.toString()
|
||||||
|
val confirmKey = confirmKeyInput.text.toString()
|
||||||
|
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
Toast.makeText(this, getString(R.string.key_cannot_be_empty), Toast.LENGTH_SHORT).show()
|
||||||
|
updateUI()
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key != confirmKey) {
|
||||||
|
Toast.makeText(this, getString(R.string.keys_do_not_match), Toast.LENGTH_SHORT).show()
|
||||||
|
updateUI()
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SecurityUtils.setCustomKey(this, key)) {
|
||||||
|
Toast.makeText(this,
|
||||||
|
getString(R.string.custom_key_set_successfully), Toast.LENGTH_SHORT).show()
|
||||||
|
updateUI()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this,
|
||||||
|
getString(R.string.failed_to_set_custom_key), Toast.LENGTH_SHORT).show()
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(getString(R.string.cancel)){ _, _ ->
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
.setNeutralButton(getString(R.string.delete_key)) { _, _ ->
|
||||||
|
SecurityUtils.clearCustomKey(this)
|
||||||
|
Toast.makeText(this,
|
||||||
|
getString(R.string.custom_encryption_key_cleared), Toast.LENGTH_SHORT).show()
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUI() {
|
||||||
|
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
||||||
|
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
|
||||||
|
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
|
||||||
|
binding.customKeyStatus.isChecked = isUsingCustomKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import devs.org.calculator.databinding.ActivitySetupPasswordBinding
|
import devs.org.calculator.databinding.ActivitySetupPasswordBinding
|
||||||
@@ -18,6 +19,7 @@ class SetupPasswordActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding2: ActivityChangePasswordBinding
|
private lateinit var binding2: ActivityChangePasswordBinding
|
||||||
private lateinit var prefsUtil: PrefsUtil
|
private lateinit var prefsUtil: PrefsUtil
|
||||||
private var hasPassword = false
|
private var hasPassword = false
|
||||||
|
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -72,8 +74,6 @@ class SetupPasswordActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnResetPassword.setOnClickListener {
|
binding.btnResetPassword.setOnClickListener {
|
||||||
// Implement password reset logic
|
|
||||||
// Could use security questions or email verification
|
|
||||||
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
|
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
|
||||||
else Toast.makeText(this,
|
else Toast.makeText(this,
|
||||||
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()
|
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package devs.org.calculator.activities
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
@@ -20,20 +20,27 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
import devs.org.calculator.adapters.FileAdapter
|
import devs.org.calculator.adapters.FileAdapter
|
||||||
import devs.org.calculator.adapters.FolderSelectionAdapter
|
import devs.org.calculator.adapters.FolderSelectionAdapter
|
||||||
import devs.org.calculator.callbacks.FileProcessCallback
|
import devs.org.calculator.callbacks.FileProcessCallback
|
||||||
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
import devs.org.calculator.databinding.ActivityViewFolderBinding
|
import devs.org.calculator.databinding.ActivityViewFolderBinding
|
||||||
import devs.org.calculator.databinding.ProccessingDialogBinding
|
import devs.org.calculator.databinding.ProccessingDialogBinding
|
||||||
import devs.org.calculator.utils.DialogUtil
|
import devs.org.calculator.utils.DialogUtil
|
||||||
import devs.org.calculator.utils.FileManager
|
import devs.org.calculator.utils.FileManager
|
||||||
|
import devs.org.calculator.utils.FileManager.Companion.ENCRYPTED_EXTENSION
|
||||||
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
|
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
|
||||||
import devs.org.calculator.utils.FolderManager
|
import devs.org.calculator.utils.FolderManager
|
||||||
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
|
import devs.org.calculator.utils.SecurityUtils
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.app.AlertDialog
|
||||||
|
|
||||||
class ViewFolderActivity : AppCompatActivity() {
|
class ViewFolderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -51,12 +58,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private var currentFolder: File? = null
|
private var currentFolder: File? = null
|
||||||
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||||
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
|
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
|
||||||
private lateinit var prefs: SharedPreferences
|
|
||||||
|
|
||||||
private var customDialog: androidx.appcompat.app.AlertDialog? = null
|
private var customDialog: androidx.appcompat.app.AlertDialog? = null
|
||||||
|
|
||||||
private var dialogShowTime: Long = 0
|
private var dialogShowTime: Long = 0
|
||||||
private val MINIMUM_DIALOG_DURATION = 1200L
|
private val MINIMUM_DIALOG_DURATION = 1200L
|
||||||
|
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -81,9 +88,9 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
fileManager = FileManager(this, this)
|
fileManager = FileManager(this, this)
|
||||||
folderManager = FolderManager(this)
|
folderManager = FolderManager()
|
||||||
dialogUtil = DialogUtil(this)
|
dialogUtil = DialogUtil(this)
|
||||||
prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupActivityResultLaunchers() {
|
private fun setupActivityResultLaunchers() {
|
||||||
@@ -162,6 +169,34 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
object : FileProcessCallback {
|
object : FileProcessCallback {
|
||||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
|
copiedFiles.forEach { file ->
|
||||||
|
val fileType = fileManager.getFileType(file)
|
||||||
|
var finalFile = file
|
||||||
|
val extension = ".${file.extension}"
|
||||||
|
val isEncrypted = prefs.getBoolean("encryption",false)
|
||||||
|
|
||||||
|
if (isEncrypted) {
|
||||||
|
val encryptedFile = SecurityUtils.changeFileExtension(file, ENCRYPTED_EXTENSION)
|
||||||
|
if (SecurityUtils.encryptFile(this@ViewFolderActivity, file, encryptedFile)) {
|
||||||
|
finalFile = encryptedFile
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||||
|
HiddenFileEntity(
|
||||||
|
filePath = finalFile.absolutePath,
|
||||||
|
fileName = file.name,
|
||||||
|
fileType = fileType,
|
||||||
|
originalExtension = extension,
|
||||||
|
isEncrypted = isEncrypted,
|
||||||
|
encryptedFileName = finalFile.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mainHandler.postDelayed({
|
mainHandler.postDelayed({
|
||||||
dismissCustomDialog()
|
dismissCustomDialog()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -192,19 +227,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshCurrentView() {
|
|
||||||
if (currentFolder != null) {
|
|
||||||
refreshCurrentFolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openFolder(folder: File) {
|
private fun openFolder(folder: File) {
|
||||||
// Ensure folder exists and has .nomedia file
|
|
||||||
if (!folder.exists()) {
|
if (!folder.exists()) {
|
||||||
folder.mkdirs()
|
folder.mkdirs()
|
||||||
File(folder, ".nomedia").createNewFile()
|
File(folder, ".nomedia").createNewFile()
|
||||||
@@ -227,8 +255,6 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun showFileList(files: List<File>, folder: File) {
|
private fun showFileList(files: List<File>, folder: File) {
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
|
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||||
|
|
||||||
// Clean up previous adapter
|
|
||||||
fileAdapter?.cleanup()
|
fileAdapter?.cleanup()
|
||||||
|
|
||||||
fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
|
fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
|
||||||
@@ -306,24 +332,244 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
private fun showFileOptionsMenu(selectedFiles: List<File>) {
|
private fun showFileOptionsMenu(selectedFiles: List<File>) {
|
||||||
if (selectedFiles.isEmpty()) return
|
if (selectedFiles.isEmpty()) return
|
||||||
|
|
||||||
val options = arrayOf(
|
lifecycleScope.launch {
|
||||||
getString(R.string.un_hide),
|
var hasEncryptedFiles = false
|
||||||
getString(R.string.delete),
|
var hasDecryptedFiles = false
|
||||||
getString(R.string.copy_to_another_folder),
|
var hasEncFilesWithoutMetadata = false
|
||||||
getString(R.string.move_to_another_folder)
|
|
||||||
)
|
for (file in selectedFiles) {
|
||||||
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(getString(R.string.file_options))
|
if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
|
||||||
.setItems(options) { _, which ->
|
if (hiddenFile?.isEncrypted == true) {
|
||||||
when (which) {
|
hasEncryptedFiles = true
|
||||||
0 -> unhideSelectedFiles(selectedFiles)
|
} else {
|
||||||
1 -> deleteSelectedFiles(selectedFiles)
|
hasEncFilesWithoutMetadata = true
|
||||||
2 -> copyToAnotherFolder(selectedFiles)
|
}
|
||||||
3 -> moveToAnotherFolder(selectedFiles)
|
} else {
|
||||||
|
hasDecryptedFiles = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.show()
|
|
||||||
|
val options = mutableListOf(
|
||||||
|
getString(R.string.un_hide),
|
||||||
|
getString(R.string.delete),
|
||||||
|
getString(R.string.copy_to_another_folder),
|
||||||
|
getString(R.string.move_to_another_folder)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasDecryptedFiles) {
|
||||||
|
options.add(getString(R.string.encrypt_file))
|
||||||
|
}
|
||||||
|
if (hasEncryptedFiles || hasEncFilesWithoutMetadata) {
|
||||||
|
options.add(getString(R.string.decrypt_file))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(this@ViewFolderActivity)
|
||||||
|
.setTitle(getString(R.string.file_options))
|
||||||
|
.setItems(options.toTypedArray()) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> unhideSelectedFiles(selectedFiles)
|
||||||
|
1 -> deleteSelectedFiles(selectedFiles)
|
||||||
|
2 -> copyToAnotherFolder(selectedFiles)
|
||||||
|
3 -> moveToAnotherFolder(selectedFiles)
|
||||||
|
else -> {
|
||||||
|
val option = options[which]
|
||||||
|
when (option) {
|
||||||
|
getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles()
|
||||||
|
getString(R.string.decrypt_file) -> {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val filesWithoutMetadata = selectedFiles.filter { file ->
|
||||||
|
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
||||||
|
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesWithoutMetadata.isNotEmpty()) {
|
||||||
|
showDecryptionTypeDialog(filesWithoutMetadata)
|
||||||
|
} else {
|
||||||
|
fileAdapter?.decryptSelectedFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDecryptionTypeDialog(selectedFiles: List<File>) {
|
||||||
|
val dialogView = layoutInflater.inflate(R.layout.dialog_file_type_selection, null)
|
||||||
|
val imageCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxImage)
|
||||||
|
val videoCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxVideo)
|
||||||
|
val audioCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxAudio)
|
||||||
|
val checkboxes = listOf(imageCheckbox, videoCheckbox, audioCheckbox)
|
||||||
|
checkboxes.forEach { checkbox ->
|
||||||
|
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
checkboxes.filter { it != checkbox }.forEach { it.isChecked = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(this)
|
||||||
|
.setTitle(getString(R.string.select_file_type))
|
||||||
|
.setMessage(getString(R.string.please_select_the_type_of_file_to_decrypt))
|
||||||
|
.setView(dialogView)
|
||||||
|
.setNegativeButton(getString(R.string.cancel), null)
|
||||||
|
.setPositiveButton(getString(R.string.decrypt), null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
positiveButton.isEnabled = false
|
||||||
|
val checkboxListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
|
positiveButton.isEnabled = checkboxes.any { it.isChecked }
|
||||||
|
}
|
||||||
|
checkboxes.forEach { it.setOnCheckedChangeListener(checkboxListener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
checkboxes.forEach { it.setOnCheckedChangeListener(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
val selectedType = when {
|
||||||
|
imageCheckbox.isChecked -> FileManager.FileType.IMAGE
|
||||||
|
videoCheckbox.isChecked -> FileManager.FileType.VIDEO
|
||||||
|
audioCheckbox.isChecked -> FileManager.FileType.AUDIO
|
||||||
|
else -> return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
performDecryptionWithType(selectedFiles, selectedType)
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performDecryptionWithType(selectedFiles: List<File>, fileType: FileManager.FileType) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
var successCount = 0
|
||||||
|
var failCount = 0
|
||||||
|
|
||||||
|
for (file in selectedFiles) {
|
||||||
|
try {
|
||||||
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
|
||||||
|
|
||||||
|
if (hiddenFile?.isEncrypted == true) {
|
||||||
|
|
||||||
|
val originalExtension = hiddenFile.originalExtension
|
||||||
|
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||||
|
|
||||||
|
|
||||||
|
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
|
||||||
|
hiddenFile.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
newFilePath = decryptedFile.absolutePath,
|
||||||
|
encryptedFileName = decryptedFile.name,
|
||||||
|
isEncrypted = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (file.delete()) {
|
||||||
|
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
|
||||||
|
|
||||||
|
val extension = when (fileType) {
|
||||||
|
FileManager.FileType.IMAGE -> ".jpg"
|
||||||
|
FileManager.FileType.VIDEO -> ".mp4"
|
||||||
|
FileManager.FileType.AUDIO -> ".mp3"
|
||||||
|
else -> ".txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||||
|
|
||||||
|
|
||||||
|
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
|
||||||
|
|
||||||
|
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||||
|
HiddenFileEntity(
|
||||||
|
filePath = decryptedFile.absolutePath,
|
||||||
|
fileName = decryptedFile.name,
|
||||||
|
encryptedFileName = file.name,
|
||||||
|
fileType = fileType,
|
||||||
|
originalExtension = extension,
|
||||||
|
isEncrypted = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (file.delete()) {
|
||||||
|
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
|
when {
|
||||||
|
successCount > 0 && failCount == 0 -> {
|
||||||
|
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
successCount > 0 && failCount > 0 -> {
|
||||||
|
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
failCount > 0 -> {
|
||||||
|
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCurrentFolder()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveToAnotherFolder(selectedFiles: List<File>) {
|
private fun moveToAnotherFolder(selectedFiles: List<File>) {
|
||||||
@@ -342,15 +588,12 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
object : DialogUtil.DialogCallback {
|
object : DialogUtil.DialogCallback {
|
||||||
override fun onPositiveButtonClicked() {
|
override fun onPositiveButtonClicked() {
|
||||||
performFileUnhiding(selectedFiles)
|
performFileUnhiding(selectedFiles)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
override fun onNegativeButtonClicked() {}
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
override fun onNaturalButtonClicked() {}
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -359,8 +602,8 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun deleteSelectedFiles(selectedFiles: List<File>) {
|
private fun deleteSelectedFiles(selectedFiles: List<File>) {
|
||||||
dialogUtil.showMaterialDialog(
|
dialogUtil.showMaterialDialog(
|
||||||
getString(R.string.delete_items),
|
getString(R.string.delete_file),
|
||||||
getString(R.string.are_you_sure_you_want_to_delete_selected_items),
|
getString(R.string.are_you_sure_to_delete_selected_files_permanently),
|
||||||
getString(R.string.delete),
|
getString(R.string.delete),
|
||||||
getString(R.string.cancel),
|
getString(R.string.cancel),
|
||||||
object : DialogUtil.DialogCallback {
|
object : DialogUtil.DialogCallback {
|
||||||
@@ -368,13 +611,9 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
performFileDeletion(selectedFiles)
|
performFileDeletion(selectedFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNegativeButtonClicked() {
|
override fun onNegativeButtonClicked() {}
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNaturalButtonClicked() {
|
override fun onNaturalButtonClicked() {}
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -383,20 +622,32 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun refreshCurrentFolder() {
|
private fun refreshCurrentFolder() {
|
||||||
currentFolder?.let { folder ->
|
currentFolder?.let { folder ->
|
||||||
val files = folderManager.getFilesInFolder(folder)
|
lifecycleScope.launch {
|
||||||
if (files.isNotEmpty()) {
|
try {
|
||||||
binding.recyclerView.visibility = View.VISIBLE
|
val files = folderManager.getFilesInFolder(folder)
|
||||||
binding.noItems.visibility = View.GONE
|
mainHandler.post {
|
||||||
fileAdapter?.submitList(files.toMutableList())
|
if (files.isNotEmpty()) {
|
||||||
fileAdapter?.let { adapter ->
|
binding.recyclerView.visibility = View.VISIBLE
|
||||||
if (adapter.isInSelectionMode()) {
|
binding.noItems.visibility = View.GONE
|
||||||
showFileSelectionIcons()
|
|
||||||
} else {
|
fileAdapter?.submitList(files.toMutableList())
|
||||||
showFileViewIcons()
|
fileAdapter?.let { adapter ->
|
||||||
|
if (adapter.isInSelectionMode()) {
|
||||||
|
showFileSelectionIcons()
|
||||||
|
} else {
|
||||||
|
showFileViewIcons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showEmptyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
showEmptyState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
showEmptyState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,18 +750,59 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
var allUnhidden = true
|
var allUnhidden = true
|
||||||
selectedFiles.forEach { file ->
|
selectedFiles.forEach { file ->
|
||||||
try {
|
try {
|
||||||
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
|
||||||
if (fileUri != null) {
|
if (hiddenFile?.isEncrypted == true) {
|
||||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
val originalExtension = hiddenFile.originalExtension
|
||||||
if (result == null) {
|
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||||
|
|
||||||
|
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, decryptedFile)
|
||||||
|
if (fileUri != null) {
|
||||||
|
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||||
|
if (result != null) {
|
||||||
|
hiddenFile.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
decryptedFile.delete()
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
allUnhidden = false
|
allUnhidden = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allUnhidden = false
|
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
|
||||||
|
if (fileUri != null) {
|
||||||
|
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||||
|
if (result != null) {
|
||||||
|
hiddenFile?.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allUnhidden = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
||||||
allUnhidden = false
|
allUnhidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,8 +815,6 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
// Fixed: Ensure proper order of operations
|
|
||||||
fileAdapter?.exitSelectionMode()
|
fileAdapter?.exitSelectionMode()
|
||||||
refreshCurrentFolder()
|
refreshCurrentFolder()
|
||||||
}
|
}
|
||||||
@@ -532,24 +822,35 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun performFileDeletion(selectedFiles: List<File>) {
|
private fun performFileDeletion(selectedFiles: List<File>) {
|
||||||
var allDeleted = true
|
lifecycleScope.launch {
|
||||||
selectedFiles.forEach { file ->
|
var allDeleted = true
|
||||||
if (!file.delete()) {
|
selectedFiles.forEach { file ->
|
||||||
allDeleted = false
|
try {
|
||||||
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
hiddenFile?.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||||
|
}
|
||||||
|
if (!file.delete()) {
|
||||||
|
allDeleted = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
allDeleted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
val message = if (allDeleted) {
|
||||||
|
getString(R.string.files_deleted_successfully)
|
||||||
|
} else {
|
||||||
|
getString(R.string.some_items_could_not_be_deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
|
refreshCurrentFolder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = if (allDeleted) {
|
|
||||||
getString(R.string.files_deleted_successfully)
|
|
||||||
} else {
|
|
||||||
getString(R.string.some_items_could_not_be_deleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
// Fixed: Ensure proper order of operations
|
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyToAnotherFolder(selectedFiles: List<File>) {
|
private fun copyToAnotherFolder(selectedFiles: List<File>) {
|
||||||
@@ -559,47 +860,78 @@ class ViewFolderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||||
var allCopied = true
|
lifecycleScope.launch {
|
||||||
selectedFiles.forEach { file ->
|
var allCopied = true
|
||||||
try {
|
selectedFiles.forEach { file ->
|
||||||
val newFile = File(destinationFolder, file.name)
|
try {
|
||||||
file.copyTo(newFile, overwrite = true)
|
val newFile = File(destinationFolder, file.name)
|
||||||
} catch (e: Exception) {
|
file.copyTo(newFile, overwrite = true)
|
||||||
allCopied = false
|
|
||||||
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
hiddenFile?.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||||
|
HiddenFileEntity(
|
||||||
|
filePath = newFile.absolutePath,
|
||||||
|
fileName = it.fileName,
|
||||||
|
fileType = it.fileType,
|
||||||
|
originalExtension = it.originalExtension,
|
||||||
|
isEncrypted = it.isEncrypted,
|
||||||
|
encryptedFileName = it.encryptedFileName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
allCopied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
|
||||||
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
|
refreshCurrentFolder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = if (allCopied) "Files copied successfully" else "Some files could not be copied"
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
// Fixed: Ensure proper order of operations
|
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||||
var allMoved = true
|
lifecycleScope.launch {
|
||||||
selectedFiles.forEach { file ->
|
var allMoved = true
|
||||||
try {
|
selectedFiles.forEach { file ->
|
||||||
val newFile = File(destinationFolder, file.name)
|
try {
|
||||||
file.copyTo(newFile, overwrite = true)
|
val newFile = File(destinationFolder, file.name)
|
||||||
file.delete()
|
file.copyTo(newFile, overwrite = true)
|
||||||
} catch (e: Exception) {
|
|
||||||
allMoved = false
|
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||||
|
hiddenFile?.let {
|
||||||
|
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
newFilePath = newFile.absolutePath,
|
||||||
|
encryptedFileName = it.encryptedFileName,
|
||||||
|
isEncrypted = it.isEncrypted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
allMoved = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
|
||||||
|
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||||
|
fileAdapter?.exitSelectionMode()
|
||||||
|
refreshCurrentFolder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = if (allMoved) "Files moved successfully" else "Some files could not be moved"
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
|
||||||
|
|
||||||
// Fixed: Ensure proper order of operations
|
|
||||||
fileAdapter?.exitSelectionMode()
|
|
||||||
refreshCurrentFolder()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
||||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||||
.filter { it != currentFolder } // Exclude current folder
|
.filter { it != currentFolder }
|
||||||
|
|
||||||
if (folders.isEmpty()) {
|
if (folders.isEmpty()) {
|
||||||
Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@@ -22,7 +23,14 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
import devs.org.calculator.activities.PreviewActivity
|
import devs.org.calculator.activities.PreviewActivity
|
||||||
|
import devs.org.calculator.database.AppDatabase
|
||||||
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
|
import devs.org.calculator.database.HiddenFileRepository
|
||||||
import devs.org.calculator.utils.FileManager
|
import devs.org.calculator.utils.FileManager
|
||||||
|
import devs.org.calculator.utils.SecurityUtils
|
||||||
|
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||||
|
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -38,18 +46,15 @@ class FileAdapter(
|
|||||||
private val selectedItems = mutableSetOf<Int>()
|
private val selectedItems = mutableSetOf<Int>()
|
||||||
private var isSelectionMode = false
|
private var isSelectionMode = false
|
||||||
|
|
||||||
// Use WeakReference to prevent memory leaks
|
|
||||||
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
|
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
|
||||||
|
|
||||||
// Background executor for file operations
|
|
||||||
private val fileExecutor = Executors.newSingleThreadExecutor()
|
private val fileExecutor = Executors.newSingleThreadExecutor()
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
companion object {
|
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
private const val TAG = "FileAdapter"
|
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback interface for handling file operations and selection changes
|
|
||||||
interface FileOperationCallback {
|
interface FileOperationCallback {
|
||||||
fun onFileDeleted(file: File)
|
fun onFileDeleted(file: File)
|
||||||
fun onFileRenamed(oldFile: File, newFile: File)
|
fun onFileRenamed(oldFile: File, newFile: File)
|
||||||
@@ -67,29 +72,42 @@ class FileAdapter(
|
|||||||
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
|
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
|
||||||
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
|
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
|
||||||
val selectedLayer: View = view.findViewById(R.id.selectedLayer)
|
val selectedLayer: View = view.findViewById(R.id.selectedLayer)
|
||||||
|
val shade: View = view.findViewById(R.id.shade)
|
||||||
val selected: ImageView = view.findViewById(R.id.selected)
|
val selected: ImageView = view.findViewById(R.id.selected)
|
||||||
|
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
|
||||||
|
|
||||||
fun bind(file: File) {
|
fun bind(file: File) {
|
||||||
val fileType = FileManager(context, lifecycleOwner).getFileType(file)
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
setupFileDisplay(file, fileType)
|
try {
|
||||||
setupClickListeners(file, fileType)
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
|
val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
|
||||||
|
else {
|
||||||
|
FileManager(context, lifecycleOwner).getFileType(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
|
||||||
|
setupClickListeners(file, fileType)
|
||||||
|
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||||
|
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Update selection state
|
val position = adapterPosition
|
||||||
val position = adapterPosition
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
val isSelected = selectedItems.contains(position)
|
||||||
val isSelected = selectedItems.contains(position)
|
updateSelectionUI(isSelected)
|
||||||
updateSelectionUI(isSelected)
|
}
|
||||||
|
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun bind(file: File, payloads: List<Any>) {
|
fun bind(file: File, payloads: List<Any>) {
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
bind(file)
|
bind(file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle partial updates based on payload
|
|
||||||
val changes = payloads.firstOrNull() as? List<String>
|
val changes = payloads.firstOrNull() as? List<String>
|
||||||
changes?.forEach { change ->
|
changes?.forEach { change ->
|
||||||
when (change) {
|
when (change) {
|
||||||
@@ -97,14 +115,13 @@ class FileAdapter(
|
|||||||
fileNameTextView.text = file.name
|
fileNameTextView.text = file.name
|
||||||
}
|
}
|
||||||
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
|
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
|
||||||
// Could update file info if displayed
|
|
||||||
}
|
}
|
||||||
"SELECTION_CHANGED" -> {
|
"SELECTION_CHANGED" -> {
|
||||||
val position = adapterPosition
|
val position = adapterPosition
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
val isSelected = selectedItems.contains(position)
|
val isSelected = selectedItems.contains(position)
|
||||||
updateSelectionUI(isSelected)
|
updateSelectionUI(isSelected)
|
||||||
// Notify activity about selection change
|
|
||||||
notifySelectionModeChange()
|
notifySelectionModeChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,37 +134,98 @@ class FileAdapter(
|
|||||||
selected.visibility = if (isSelected) View.VISIBLE else View.GONE
|
selected.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupFileDisplay(file: File, fileType: FileManager.FileType) {
|
private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
|
||||||
fileNameTextView.text = file.name
|
fileNameTextView.text = metadata?.fileName ?: file.name
|
||||||
|
|
||||||
when (fileType) {
|
when (fileType) {
|
||||||
FileManager.FileType.IMAGE -> {
|
FileManager.FileType.IMAGE -> {
|
||||||
playIcon.visibility = View.GONE
|
playIcon.visibility = View.GONE
|
||||||
Glide.with(context)
|
if (isEncrypted) {
|
||||||
.load(file)
|
try {
|
||||||
.centerCrop()
|
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
.error(R.drawable.ic_document)
|
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||||
.placeholder(R.drawable.ic_document)
|
if (uri != null) {
|
||||||
.into(imageView)
|
Glide.with(context)
|
||||||
|
.load(uri)
|
||||||
|
.centerCrop()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.error(R.drawable.encrypted)
|
||||||
|
.into(imageView)
|
||||||
|
imageView.setPadding(0, 0, 0, 0)
|
||||||
|
} else {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(file)
|
||||||
|
.centerCrop()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.error(R.drawable.ic_image)
|
||||||
|
.into(imageView)
|
||||||
|
imageView.setPadding(0, 0, 0, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FileManager.FileType.VIDEO -> {
|
FileManager.FileType.VIDEO -> {
|
||||||
playIcon.visibility = View.VISIBLE
|
playIcon.visibility = View.VISIBLE
|
||||||
Glide.with(context)
|
if (isEncrypted) {
|
||||||
.load(file)
|
try {
|
||||||
.centerCrop()
|
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
.error(R.drawable.ic_document)
|
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||||
.placeholder(R.drawable.ic_document)
|
if (uri != null) {
|
||||||
.into(imageView)
|
Glide.with(context)
|
||||||
|
.load(uri)
|
||||||
|
.centerCrop()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.error(R.drawable.encrypted)
|
||||||
|
.into(imageView)
|
||||||
|
imageView.setPadding(0, 0, 0, 0)
|
||||||
|
} else {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showEncryptedIcon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Glide.with(context)
|
||||||
|
.load(file)
|
||||||
|
.centerCrop()
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.error(R.drawable.ic_video)
|
||||||
|
.into(imageView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FileManager.FileType.AUDIO -> {
|
FileManager.FileType.AUDIO -> {
|
||||||
playIcon.visibility = View.GONE
|
playIcon.visibility = View.GONE
|
||||||
imageView.setImageResource(R.drawable.ic_audio)
|
if (isEncrypted) {
|
||||||
|
imageView.setImageResource(R.drawable.encrypted)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.ic_audio)
|
||||||
|
}
|
||||||
|
imageView.setPadding(50, 50, 50, 50)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
playIcon.visibility = View.GONE
|
playIcon.visibility = View.GONE
|
||||||
imageView.setImageResource(R.drawable.ic_document)
|
if (isEncrypted) {
|
||||||
|
imageView.setImageResource(R.drawable.encrypted)
|
||||||
|
} else {
|
||||||
|
imageView.setImageResource(R.drawable.ic_document)
|
||||||
|
}
|
||||||
|
imageView.setPadding(50, 50, 50, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,33 +256,83 @@ class FileAdapter(
|
|||||||
|
|
||||||
private fun openFile(file: File, fileType: FileManager.FileType) {
|
private fun openFile(file: File, fileType: FileManager.FileType) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Toast.makeText(context, "File no longer exists", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context,
|
||||||
|
context.getString(R.string.file_no_longer_exists), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (fileType) {
|
when (fileType) {
|
||||||
FileManager.FileType.AUDIO -> openAudioFile(file)
|
FileManager.FileType.AUDIO -> openAudioFile(file)
|
||||||
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> openInPreview(fileType)
|
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
|
if (hiddenFile?.isEncrypted == true || file.extension == FileManager.ENCRYPTED_EXTENSION) {
|
||||||
|
if (file.extension == FileManager.ENCRYPTED_EXTENSION && hiddenFile == null) {
|
||||||
|
showDecryptionTypeDialog(file)
|
||||||
|
} else {
|
||||||
|
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
||||||
|
|
||||||
|
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
||||||
|
if (tempFile.exists() && tempFile.length() > 0) {
|
||||||
|
mainHandler.post {
|
||||||
|
val fileTypeString = when (fileType) {
|
||||||
|
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||||
|
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||||
|
else -> "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||||
|
putExtra("type", fileTypeString)
|
||||||
|
putExtra("folder", currentFolder.toString())
|
||||||
|
putExtra("position", adapterPosition)
|
||||||
|
putExtra("isEncrypted", true)
|
||||||
|
putExtra("tempFile", tempFile.absolutePath)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Failed to prepare file for preview", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Failed to decrypt file for preview", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openInPreview(fileType)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
FileManager.FileType.DOCUMENT -> openDocumentFile(file)
|
FileManager.FileType.DOCUMENT -> openDocumentFile(file)
|
||||||
else -> openDocumentFile(file)
|
else -> openDocumentFile(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioFile(file: File) {
|
private fun openAudioFile(file: File) {
|
||||||
|
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||||
try {
|
try {
|
||||||
val uri = FileProvider.getUriForFile(
|
val fileTypeString = when (fileType) {
|
||||||
context,
|
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||||
"${context.packageName}.fileprovider",
|
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||||
file
|
else -> "unknown"
|
||||||
)
|
}
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(uri, "audio/*")
|
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||||
|
putExtra("type", fileTypeString)
|
||||||
putExtra("folder", currentFolder.toString())
|
putExtra("folder", currentFolder.toString())
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
putExtra("position", adapterPosition)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to open audio file: ${e.message}")
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.no_audio_player_found),
|
context.getString(R.string.no_audio_player_found),
|
||||||
@@ -227,7 +355,7 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to open document file: ${e.message}")
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.no_suitable_app_found_to_open_this_document),
|
context.getString(R.string.no_suitable_app_found_to_open_this_document),
|
||||||
@@ -251,102 +379,6 @@ class FileAdapter(
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFileOptionsDialog(file: File) {
|
|
||||||
val options = if (isSelectionMode) {
|
|
||||||
arrayOf(
|
|
||||||
context.getString(R.string.un_hide),
|
|
||||||
context.getString(R.string.delete),
|
|
||||||
context.getString(R.string.copy_to_another_folder),
|
|
||||||
context.getString(R.string.move_to_another_folder)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
arrayOf(
|
|
||||||
context.getString(R.string.un_hide),
|
|
||||||
context.getString(R.string.select_multiple),
|
|
||||||
context.getString(R.string.rename),
|
|
||||||
context.getString(R.string.delete),
|
|
||||||
context.getString(R.string.share)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle(context.getString(R.string.file_options))
|
|
||||||
.setItems(options) { dialog, which ->
|
|
||||||
if (isSelectionMode) {
|
|
||||||
when (which) {
|
|
||||||
0 -> unHideFile(file)
|
|
||||||
1 -> deleteFile(file)
|
|
||||||
2 -> copyToAnotherFolder(file)
|
|
||||||
3 -> moveToAnotherFolder(file)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (which) {
|
|
||||||
0 -> unHideFile(file)
|
|
||||||
1 -> enableSelectMultipleFiles()
|
|
||||||
2 -> renameFile(file)
|
|
||||||
3 -> deleteFile(file)
|
|
||||||
4 -> shareFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableSelectMultipleFiles() {
|
|
||||||
val position = adapterPosition
|
|
||||||
if (position == RecyclerView.NO_POSITION) return
|
|
||||||
|
|
||||||
enterSelectionMode()
|
|
||||||
selectedItems.add(position)
|
|
||||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unHideFile(file: File) {
|
|
||||||
FileManager(context, lifecycleOwner).unHideFile(
|
|
||||||
file = file,
|
|
||||||
onSuccess = {
|
|
||||||
fileOperationCallback?.get()?.onFileDeleted(file)
|
|
||||||
},
|
|
||||||
onError = { errorMessage ->
|
|
||||||
Toast.makeText(context, "Failed to unhide: $errorMessage", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteFile(file: File) {
|
|
||||||
// Show confirmation dialog first
|
|
||||||
MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle("Delete File")
|
|
||||||
.setMessage("Are you sure you want to delete ${file.name}?")
|
|
||||||
.setPositiveButton("Delete") { _, _ ->
|
|
||||||
deleteFileAsync(file)
|
|
||||||
}
|
|
||||||
.setNegativeButton("Cancel", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteFileAsync(file: File) {
|
|
||||||
fileExecutor.execute {
|
|
||||||
val success = try {
|
|
||||||
file.delete()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to delete file: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
mainHandler.post {
|
|
||||||
if (success) {
|
|
||||||
fileOperationCallback?.get()?.onFileDeleted(file)
|
|
||||||
Toast.makeText(context, "File deleted", Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingInflatedId")
|
@SuppressLint("MissingInflatedId")
|
||||||
private fun renameFile(file: File) {
|
private fun renameFile(file: File) {
|
||||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
||||||
@@ -398,7 +430,6 @@ class FileAdapter(
|
|||||||
val success = try {
|
val success = try {
|
||||||
file.renameTo(newFile)
|
file.renameTo(newFile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to rename file: ${e.message}")
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,36 +445,6 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareFile(file: File) {
|
|
||||||
try {
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = context.contentResolver.getType(uri) ?: "*/*"
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to share file: ${e.message}")
|
|
||||||
Toast.makeText(context, "Failed to share file", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyToAnotherFolder(file: File) {
|
|
||||||
// This will be handled by the activity
|
|
||||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun moveToAnotherFolder(file: File) {
|
|
||||||
// This will be handled by the activity
|
|
||||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleSelection(position: Int) {
|
private fun toggleSelection(position: Int) {
|
||||||
if (selectedItems.contains(position)) {
|
if (selectedItems.contains(position)) {
|
||||||
@@ -457,6 +458,91 @@ class FileAdapter(
|
|||||||
onSelectionCountChanged(selectedItems.size)
|
onSelectionCountChanged(selectedItems.size)
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showEncryptedIcon() {
|
||||||
|
imageView.setImageResource(R.drawable.encrypted)
|
||||||
|
imageView.setPadding(50, 50, 50, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDecryptionTypeDialog(file: File) {
|
||||||
|
val options = arrayOf("Image", "Video", "Audio")
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle("Select File Type")
|
||||||
|
.setMessage("Please select the type of file to decrypt")
|
||||||
|
.setItems(options) { _, which ->
|
||||||
|
val selectedType = when (which) {
|
||||||
|
0 -> FileManager.FileType.IMAGE
|
||||||
|
1 -> FileManager.FileType.VIDEO
|
||||||
|
2 -> FileManager.FileType.AUDIO
|
||||||
|
else -> FileManager.FileType.DOCUMENT
|
||||||
|
}
|
||||||
|
performDecryptionWithType(file, selectedType)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val extension = when (fileType) {
|
||||||
|
FileManager.FileType.IMAGE -> ".jpg"
|
||||||
|
FileManager.FileType.VIDEO -> ".mp4"
|
||||||
|
FileManager.FileType.AUDIO -> ".mp3"
|
||||||
|
else -> ".txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||||
|
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
hiddenFileRepository.insertHiddenFile(
|
||||||
|
HiddenFileEntity(
|
||||||
|
filePath = decryptedFile.absolutePath,
|
||||||
|
fileName = decryptedFile.name,
|
||||||
|
encryptedFileName = file.name,
|
||||||
|
fileType = fileType,
|
||||||
|
originalExtension = extension,
|
||||||
|
isEncrypted = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
when (fileType) {
|
||||||
|
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
|
||||||
|
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||||
|
putExtra("type", if (fileType == FileManager.FileType.IMAGE) "image" else "video")
|
||||||
|
putExtra("folder", currentFolder.toString())
|
||||||
|
putExtra("position", adapterPosition)
|
||||||
|
putExtra("isEncrypted", false)
|
||||||
|
putExtra("file", decryptedFile.absolutePath)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
FileManager.FileType.AUDIO -> openAudioFile(decryptedFile)
|
||||||
|
else -> openDocumentFile(decryptedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.delete()
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mainHandler.post {
|
||||||
|
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
|
||||||
@@ -489,8 +575,7 @@ class FileAdapter(
|
|||||||
currentList.clear()
|
currentList.clear()
|
||||||
super.submitList(null)
|
super.submitList(null)
|
||||||
} else {
|
} else {
|
||||||
// Create a new list to force update
|
val newList = list.filter { it.name != ".nomedia" }.toMutableList()
|
||||||
val newList = list.toMutableList()
|
|
||||||
super.submitList(newList)
|
super.submitList(newList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,6 +587,7 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun exitSelectionMode() {
|
fun exitSelectionMode() {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
isSelectionMode = false
|
isSelectionMode = false
|
||||||
@@ -528,19 +614,12 @@ class FileAdapter(
|
|||||||
if (!isSelectionMode) {
|
if (!isSelectionMode) {
|
||||||
enterSelectionMode()
|
enterSelectionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
val previouslySelected = selectedItems.toSet()
|
val previouslySelected = selectedItems.toSet()
|
||||||
selectedItems.clear()
|
selectedItems.clear()
|
||||||
|
|
||||||
// Add all positions to selection
|
|
||||||
for (i in 0 until itemCount) {
|
for (i in 0 until itemCount) {
|
||||||
selectedItems.add(i)
|
selectedItems.add(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify callback about selection change
|
|
||||||
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
|
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
|
||||||
|
|
||||||
// Update UI for changed items efficiently
|
|
||||||
updateSelectionItems(selectedItems.toSet(), previouslySelected)
|
updateSelectionItems(selectedItems.toSet(), previouslySelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,128 +650,6 @@ class FileAdapter(
|
|||||||
fun isInSelectionMode(): Boolean = isSelectionMode
|
fun isInSelectionMode(): Boolean = isSelectionMode
|
||||||
|
|
||||||
|
|
||||||
fun deleteSelectedFiles() {
|
|
||||||
val selectedFiles = getSelectedItems()
|
|
||||||
if (selectedFiles.isEmpty()) return
|
|
||||||
|
|
||||||
// Show confirmation dialog
|
|
||||||
MaterialAlertDialogBuilder(context)
|
|
||||||
.setTitle("Delete Files")
|
|
||||||
.setMessage("Are you sure you want to delete ${selectedFiles.size} file(s)?")
|
|
||||||
.setPositiveButton("Delete") { _, _ ->
|
|
||||||
deleteFilesAsync(selectedFiles)
|
|
||||||
}
|
|
||||||
.setNegativeButton("Cancel", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteFilesAsync(selectedFiles: List<File>) {
|
|
||||||
fileExecutor.execute {
|
|
||||||
var deletedCount = 0
|
|
||||||
var failedCount = 0
|
|
||||||
val failedFiles = mutableListOf<String>()
|
|
||||||
|
|
||||||
selectedFiles.forEach { file ->
|
|
||||||
try {
|
|
||||||
if (file.delete()) {
|
|
||||||
deletedCount++
|
|
||||||
mainHandler.post {
|
|
||||||
fileOperationCallback?.get()?.onFileDeleted(file)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
failedCount++
|
|
||||||
failedFiles.add(file.name)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
failedCount++
|
|
||||||
failedFiles.add(file.name)
|
|
||||||
Log.e(TAG, "Failed to delete ${file.name}: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainHandler.post {
|
|
||||||
// Exit selection mode first
|
|
||||||
exitSelectionMode()
|
|
||||||
|
|
||||||
// Show detailed result message
|
|
||||||
when {
|
|
||||||
deletedCount > 0 && failedCount == 0 -> {
|
|
||||||
Toast.makeText(context, "Deleted $deletedCount file(s)", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
deletedCount > 0 && failedCount > 0 -> {
|
|
||||||
Toast.makeText(context,
|
|
||||||
"Deleted $deletedCount file(s), failed to delete $failedCount",
|
|
||||||
Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
failedCount > 0 -> {
|
|
||||||
Toast.makeText(context,
|
|
||||||
"Failed to delete $failedCount file(s)",
|
|
||||||
Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun shareSelectedFiles() {
|
|
||||||
val selectedFiles = getSelectedItems()
|
|
||||||
if (selectedFiles.isEmpty()) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (selectedFiles.size == 1) {
|
|
||||||
// Share single file
|
|
||||||
val file = selectedFiles.first()
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = context.contentResolver.getType(uri) ?: "*/*"
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(shareIntent, context.getString(R.string.share_file))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Share multiple files
|
|
||||||
val uris = selectedFiles.mapNotNull { file ->
|
|
||||||
try {
|
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to get URI for file ${file.name}: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uris.isNotEmpty()) {
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
|
||||||
type = "*/*"
|
|
||||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(shareIntent, "Share ${selectedFiles.size} files")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "No files could be shared", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to share files: ${e.message}")
|
|
||||||
Toast.makeText(context, "Failed to share files", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
exitSelectionMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onBackPressed(): Boolean {
|
fun onBackPressed(): Boolean {
|
||||||
return if (isSelectionMode) {
|
return if (isSelectionMode) {
|
||||||
exitSelectionMode()
|
exitSelectionMode()
|
||||||
@@ -702,25 +659,12 @@ class FileAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshSelectionStates() {
|
|
||||||
if (isSelectionMode) {
|
|
||||||
selectedItems.forEach { position ->
|
|
||||||
if (position < itemCount) {
|
|
||||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure callback is notified
|
|
||||||
notifySelectionModeChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
try {
|
try {
|
||||||
if (!fileExecutor.isShutdown) {
|
if (!fileExecutor.isShutdown) {
|
||||||
fileExecutor.shutdown()
|
fileExecutor.shutdown()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error shutting down executor: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileOperationCallback?.clear()
|
fileOperationCallback?.clear()
|
||||||
@@ -734,4 +678,169 @@ class FileAdapter(
|
|||||||
private fun onSelectionCountChanged(count: Int) {
|
private fun onSelectionCountChanged(count: Int) {
|
||||||
fileOperationCallback?.get()?.onSelectionCountChanged(count)
|
fileOperationCallback?.get()?.onSelectionCountChanged(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun encryptSelectedFiles() {
|
||||||
|
val selectedFiles = getSelectedItems()
|
||||||
|
if (selectedFiles.isEmpty()) return
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(context.getString(R.string.encrypt_files))
|
||||||
|
.setMessage(context.getString(R.string.encryption_disclaimer))
|
||||||
|
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
|
||||||
|
performEncryption(selectedFiles)
|
||||||
|
}
|
||||||
|
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performEncryption(selectedFiles: List<File>) {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
var successCount = 0
|
||||||
|
var failCount = 0
|
||||||
|
val updatedFiles = mutableListOf<File>()
|
||||||
|
|
||||||
|
for (file in selectedFiles) {
|
||||||
|
try {
|
||||||
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
|
if (hiddenFile?.isEncrypted == true) continue
|
||||||
|
val originalExtension = ".${file.extension.lowercase()}"
|
||||||
|
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||||
|
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
|
||||||
|
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
|
||||||
|
if (encryptedFile.exists()) {
|
||||||
|
if (hiddenFile == null){
|
||||||
|
hiddenFileRepository.insertHiddenFile(
|
||||||
|
HiddenFileEntity(
|
||||||
|
filePath = encryptedFile.absolutePath,
|
||||||
|
isEncrypted = true,
|
||||||
|
encryptedFileName = encryptedFile.name,
|
||||||
|
fileType = fileType,
|
||||||
|
fileName = file.name,
|
||||||
|
originalExtension = originalExtension
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
hiddenFile.let {
|
||||||
|
hiddenFileRepository.updateEncryptionStatus(
|
||||||
|
filePath = hiddenFile.filePath,
|
||||||
|
newFilePath = encryptedFile.absolutePath,
|
||||||
|
encryptedFileName = encryptedFile.name,
|
||||||
|
isEncrypted = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file.delete()) {
|
||||||
|
updatedFiles.add(encryptedFile)
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
exitSelectionMode()
|
||||||
|
when {
|
||||||
|
successCount > 0 && failCount == 0 -> {
|
||||||
|
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
successCount > 0 && failCount > 0 -> {
|
||||||
|
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
failCount > 0 -> {
|
||||||
|
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||||
|
submitList(currentFiles)
|
||||||
|
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptSelectedFiles() {
|
||||||
|
val selectedFiles = getSelectedItems()
|
||||||
|
if (selectedFiles.isEmpty()) return
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(context)
|
||||||
|
.setTitle(context.getString(R.string.decrypt_files))
|
||||||
|
.setMessage(context.getString(R.string.decryption_disclaimer))
|
||||||
|
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
|
||||||
|
performDecryption(selectedFiles)
|
||||||
|
}
|
||||||
|
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performDecryption(selectedFiles: List<File>) {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
var successCount = 0
|
||||||
|
var failCount = 0
|
||||||
|
val updatedFiles = mutableListOf<File>()
|
||||||
|
|
||||||
|
for (file in selectedFiles) {
|
||||||
|
try {
|
||||||
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
|
if (hiddenFile?.isEncrypted != true) continue
|
||||||
|
val originalExtension = hiddenFile.originalExtension
|
||||||
|
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||||
|
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||||
|
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||||
|
hiddenFile.let {
|
||||||
|
hiddenFileRepository.updateEncryptionStatus(
|
||||||
|
filePath = file.absolutePath,
|
||||||
|
newFilePath = decryptedFile.absolutePath,
|
||||||
|
encryptedFileName = decryptedFile.name,
|
||||||
|
isEncrypted = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (file.delete()) {
|
||||||
|
updatedFiles.add(decryptedFile)
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decryptedFile.delete()
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (decryptedFile.exists()) {
|
||||||
|
decryptedFile.delete()
|
||||||
|
}
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
exitSelectionMode()
|
||||||
|
when {
|
||||||
|
successCount > 0 && failCount == 0 -> {
|
||||||
|
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
successCount > 0 && failCount > 0 -> {
|
||||||
|
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
failCount > 0 -> {
|
||||||
|
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||||
|
submitList(currentFiles)
|
||||||
|
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,10 @@ import java.io.File
|
|||||||
class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
||||||
// Compare by absolute path since File objects might be different instances
|
|
||||||
// but represent the same file
|
|
||||||
return oldItem.absolutePath == newItem.absolutePath
|
return oldItem.absolutePath == newItem.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
||||||
// Compare all relevant properties that might change and affect the UI
|
|
||||||
return oldItem.name == newItem.name &&
|
return oldItem.name == newItem.name &&
|
||||||
oldItem.length() == newItem.length() &&
|
oldItem.length() == newItem.length() &&
|
||||||
oldItem.lastModified() == newItem.lastModified() &&
|
oldItem.lastModified() == newItem.lastModified() &&
|
||||||
@@ -22,8 +19,6 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: File, newItem: File): Any? {
|
override fun getChangePayload(oldItem: File, newItem: File): Any? {
|
||||||
// Return a payload if only specific properties changed
|
|
||||||
// This allows for partial updates instead of full rebinding
|
|
||||||
val changes = mutableListOf<String>()
|
val changes = mutableListOf<String>()
|
||||||
|
|
||||||
if (oldItem.name != newItem.name) {
|
if (oldItem.name != newItem.name) {
|
||||||
@@ -42,6 +37,6 @@ class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
|||||||
changes.add("EXISTENCE_CHANGED")
|
changes.add("EXISTENCE_CHANGED")
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (changes.isNotEmpty()) changes else null
|
return changes.ifEmpty { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package devs.org.calculator.adapters
|
package devs.org.calculator.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -80,6 +81,7 @@ class FolderAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
val wasInSelectionMode = isSelectionMode
|
val wasInSelectionMode = isSelectionMode
|
||||||
selectedItems.clear()
|
selectedItems.clear()
|
||||||
|
|||||||
@@ -9,15 +9,20 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.MediaController
|
import android.widget.MediaController
|
||||||
import android.widget.SeekBar
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import devs.org.calculator.R
|
||||||
|
import devs.org.calculator.database.AppDatabase
|
||||||
|
import devs.org.calculator.database.HiddenFileRepository
|
||||||
import devs.org.calculator.databinding.ViewpagerItemsBinding
|
import devs.org.calculator.databinding.ViewpagerItemsBinding
|
||||||
import devs.org.calculator.utils.FileManager
|
import devs.org.calculator.utils.FileManager
|
||||||
|
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||||
|
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import devs.org.calculator.R
|
|
||||||
|
|
||||||
class ImagePreviewAdapter(
|
class ImagePreviewAdapter(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -25,11 +30,11 @@ class ImagePreviewAdapter(
|
|||||||
) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() {
|
) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() {
|
||||||
|
|
||||||
private val differ = AsyncListDiffer(this, FileDiffCallback())
|
private val differ = AsyncListDiffer(this, FileDiffCallback())
|
||||||
var currentMediaPlayer: MediaPlayer? = null
|
|
||||||
var isMediaPlayerPrepared = false
|
|
||||||
var currentViewHolder: ImageViewHolder? = null
|
|
||||||
private var currentPlayingPosition = -1
|
private var currentPlayingPosition = -1
|
||||||
private var isPlaying = false
|
private var currentViewHolder: ImageViewHolder? = null
|
||||||
|
private val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
|
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||||
|
}
|
||||||
|
|
||||||
var images: List<File>
|
var images: List<File>
|
||||||
get() = differ.currentList
|
get() = differ.currentList
|
||||||
@@ -42,40 +47,78 @@ class ImagePreviewAdapter(
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||||
val imageUrl = images[position]
|
val imageUrl = images[position]
|
||||||
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
|
val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
|
||||||
holder.bind(imageUrl,fileType)
|
stopAndResetCurrentAudio()
|
||||||
currentViewHolder = holder
|
|
||||||
|
|
||||||
currentMediaPlayer?.let {
|
holder.bind(imageUrl, position,fileType)
|
||||||
if (it.isPlaying) it.pause()
|
|
||||||
it.seekTo(0)
|
|
||||||
}
|
|
||||||
currentMediaPlayer = null
|
|
||||||
isMediaPlayerPrepared = false
|
|
||||||
|
|
||||||
if (currentMediaPlayer?.isPlaying == true) {
|
|
||||||
currentMediaPlayer?.stop()
|
|
||||||
currentMediaPlayer?.release()
|
|
||||||
}
|
|
||||||
currentMediaPlayer = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = images.size
|
override fun getItemCount(): Int = images.size
|
||||||
|
|
||||||
|
private fun stopAndResetCurrentAudio() {
|
||||||
|
currentViewHolder?.stopAndResetAudio()
|
||||||
|
currentPlayingPosition = -1
|
||||||
|
currentViewHolder = null
|
||||||
|
}
|
||||||
|
|
||||||
inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) {
|
inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
|
||||||
private var seekHandler = Handler(Looper.getMainLooper())
|
private var seekHandler = Handler(Looper.getMainLooper())
|
||||||
private var seekRunnable: Runnable? = null
|
private var seekRunnable: Runnable? = null
|
||||||
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
|
private var isMediaPlayerPrepared = false
|
||||||
|
private var isPlaying = false
|
||||||
|
private var currentPosition = 0
|
||||||
|
private var tempDecryptedFile: File? = null
|
||||||
|
|
||||||
fun bind(file: File, fileType: FileManager.FileType) {
|
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
|
||||||
|
currentPosition = position
|
||||||
|
|
||||||
|
releaseMediaPlayer()
|
||||||
|
resetAudioUI()
|
||||||
|
cleanupTempFile()
|
||||||
|
try {
|
||||||
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
|
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||||
|
if (hiddenFile != null){
|
||||||
|
val isEncrypted = hiddenFile.isEncrypted
|
||||||
|
val fileType = hiddenFile.fileType
|
||||||
|
if (isEncrypted) {
|
||||||
|
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
||||||
|
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
|
||||||
|
displayFile(tempDecryptedFile, fileType,true)
|
||||||
|
} else {
|
||||||
|
showEncryptedError()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayFile(file, decryptedFileType,false)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
displayFile(file, decryptedFileType,false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
displayFile(file, decryptedFileType,false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
|
||||||
|
val uri = getUriForPreviewFile(context, file)
|
||||||
when (fileType) {
|
when (fileType) {
|
||||||
FileManager.FileType.VIDEO -> {
|
FileManager.FileType.VIDEO -> {
|
||||||
binding.imageView.visibility = View.GONE
|
binding.imageView.visibility = View.GONE
|
||||||
binding.audioBg.visibility = View.GONE
|
binding.audioBg.visibility = View.GONE
|
||||||
binding.videoView.visibility = View.VISIBLE
|
binding.videoView.visibility = View.VISIBLE
|
||||||
|
|
||||||
val videoUri = Uri.fromFile(file)
|
val videoUri = if (isEncrypted){
|
||||||
|
uri
|
||||||
|
}else{
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
binding.videoView.setVideoURI(videoUri)
|
binding.videoView.setVideoURI(videoUri)
|
||||||
binding.videoView.start()
|
binding.videoView.start()
|
||||||
|
|
||||||
@@ -100,21 +143,30 @@ class ImagePreviewAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileManager.FileType.IMAGE -> {
|
FileManager.FileType.IMAGE -> {
|
||||||
|
val imageUri = if (isEncrypted){
|
||||||
|
uri
|
||||||
|
}else{
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
binding.imageView.visibility = View.VISIBLE
|
binding.imageView.visibility = View.VISIBLE
|
||||||
binding.videoView.visibility = View.GONE
|
binding.videoView.visibility = View.GONE
|
||||||
binding.audioBg.visibility = View.GONE
|
binding.audioBg.visibility = View.GONE
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.load(file)
|
.load(imageUri)
|
||||||
.into(binding.imageView)
|
.into(binding.imageView)
|
||||||
}
|
}
|
||||||
FileManager.FileType.AUDIO -> {
|
FileManager.FileType.AUDIO -> {
|
||||||
|
val audioFile: File? = if (isEncrypted) {
|
||||||
|
getFileFromUri(context, uri!!)
|
||||||
|
} else {
|
||||||
|
file
|
||||||
|
}
|
||||||
binding.imageView.visibility = View.GONE
|
binding.imageView.visibility = View.GONE
|
||||||
binding.audioBg.visibility = View.VISIBLE
|
binding.audioBg.visibility = View.VISIBLE
|
||||||
binding.videoView.visibility = View.GONE
|
binding.videoView.visibility = View.GONE
|
||||||
binding.audioTitle.text = file.name
|
binding.audioTitle.text = file.name
|
||||||
|
|
||||||
setupAudioPlayer(file)
|
setupAudioPlayer(audioFile!!)
|
||||||
setupSeekBar()
|
|
||||||
setupPlaybackControls()
|
setupPlaybackControls()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -125,27 +177,73 @@ class ImagePreviewAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFileFromUri(context: Context, uri: Uri): File? {
|
||||||
|
return try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
val tempFile = File.createTempFile("temp_audio", null, context.cacheDir)
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
inputStream?.copyTo(output)
|
||||||
|
}
|
||||||
|
tempFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEncryptedError() {
|
||||||
|
binding.imageView.visibility = View.VISIBLE
|
||||||
|
binding.videoView.visibility = View.GONE
|
||||||
|
binding.audioBg.visibility = View.GONE
|
||||||
|
binding.imageView.setImageResource(R.drawable.encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupTempFile() {
|
||||||
|
tempDecryptedFile?.let { file ->
|
||||||
|
if (file.exists()) {
|
||||||
|
try {
|
||||||
|
file.delete()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempDecryptedFile = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAudioUI() {
|
||||||
|
binding.playPause.setImageResource(R.drawable.play)
|
||||||
|
binding.audioSeekBar.value = 0f
|
||||||
|
binding.audioSeekBar.valueTo = 100f
|
||||||
|
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupAudioPlayer(file: File) {
|
private fun setupAudioPlayer(file: File) {
|
||||||
mediaPlayer = MediaPlayer().apply {
|
try {
|
||||||
setDataSource(file.absolutePath)
|
mediaPlayer = MediaPlayer().apply {
|
||||||
setOnPreparedListener { mp ->
|
setDataSource(file.absolutePath)
|
||||||
binding.audioSeekBar.valueTo = mp.duration.toFloat()
|
setOnPreparedListener { mp ->
|
||||||
|
binding.audioSeekBar.valueTo = mp.duration.toFloat()
|
||||||
isMediaPlayerPrepared = true
|
binding.audioSeekBar.value = 0f
|
||||||
|
setupSeekBar()
|
||||||
|
isMediaPlayerPrepared = true
|
||||||
|
}
|
||||||
|
setOnCompletionListener {
|
||||||
|
stopAndResetAudio()
|
||||||
|
}
|
||||||
|
setOnErrorListener { _, _, _ ->
|
||||||
|
releaseMediaPlayer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
prepareAsync()
|
||||||
}
|
}
|
||||||
setOnCompletionListener {
|
} catch (e: Exception) {
|
||||||
// isPlaying = false
|
e.printStackTrace()
|
||||||
binding.playPause.setImageResource(R.drawable.play)
|
releaseMediaPlayer()
|
||||||
binding.audioSeekBar.value = 0f
|
|
||||||
|
|
||||||
seekHandler.removeCallbacks(seekRunnable!!)
|
|
||||||
}
|
|
||||||
prepareAsync()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSeekBar() {
|
private fun setupSeekBar() {
|
||||||
binding.audioSeekBar.addOnChangeListener { slider, value, fromUser ->
|
binding.audioSeekBar.addOnChangeListener { _, value, fromUser ->
|
||||||
if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) {
|
if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) {
|
||||||
mediaPlayer?.seekTo(value.toInt())
|
mediaPlayer?.seekTo(value.toInt())
|
||||||
}
|
}
|
||||||
@@ -153,15 +251,18 @@ class ImagePreviewAdapter(
|
|||||||
|
|
||||||
seekRunnable = Runnable {
|
seekRunnable = Runnable {
|
||||||
mediaPlayer?.let { mp ->
|
mediaPlayer?.let { mp ->
|
||||||
if (mp.isPlaying) {
|
if (mp.isPlaying && isMediaPlayerPrepared) {
|
||||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
try {
|
||||||
seekHandler.postDelayed(seekRunnable!!, 100)
|
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||||
|
seekHandler.postDelayed(seekRunnable!!, 100)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun setupPlaybackControls() {
|
private fun setupPlaybackControls() {
|
||||||
binding.playPause.setOnClickListener {
|
binding.playPause.setOnClickListener {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@@ -173,65 +274,127 @@ class ImagePreviewAdapter(
|
|||||||
|
|
||||||
binding.preview.setOnClickListener {
|
binding.preview.setOnClickListener {
|
||||||
mediaPlayer?.let { mp ->
|
mediaPlayer?.let { mp ->
|
||||||
val newPosition = mp.currentPosition - 10000
|
if (isMediaPlayerPrepared) {
|
||||||
mp.seekTo(maxOf(0, newPosition))
|
try {
|
||||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
val newPosition = mp.currentPosition - 10000
|
||||||
|
mp.seekTo(maxOf(0, newPosition))
|
||||||
|
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.next.setOnClickListener {
|
binding.next.setOnClickListener {
|
||||||
mediaPlayer?.let { mp ->
|
mediaPlayer?.let { mp ->
|
||||||
val newPosition = mp.currentPosition + 10000
|
if (isMediaPlayerPrepared) {
|
||||||
mp.seekTo(minOf(mp.duration, newPosition))
|
try {
|
||||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
val newPosition = mp.currentPosition + 10000
|
||||||
|
mp.seekTo(minOf(mp.duration, newPosition))
|
||||||
|
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playAudio() {
|
private fun playAudio() {
|
||||||
mediaPlayer?.let { mp ->
|
mediaPlayer?.let { mp ->
|
||||||
if (currentPlayingPosition != -1 && currentPlayingPosition != adapterPosition) {
|
if (isMediaPlayerPrepared) {
|
||||||
currentViewHolder?.pauseAudio()
|
try {
|
||||||
|
if (currentPlayingPosition != currentPosition) {
|
||||||
|
stopAndResetCurrentAudio()
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.start()
|
||||||
|
isPlaying = true
|
||||||
|
binding.playPause.setImageResource(R.drawable.pause)
|
||||||
|
seekRunnable?.let { seekHandler.post(it) }
|
||||||
|
|
||||||
|
currentPlayingPosition = currentPosition
|
||||||
|
currentViewHolder = this@ImageViewHolder
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
releaseMediaPlayer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mp.start()
|
|
||||||
isPlaying = true
|
|
||||||
binding.playPause.setImageResource(R.drawable.pause)
|
|
||||||
seekHandler.post(seekRunnable!!)
|
|
||||||
currentPlayingPosition = adapterPosition
|
|
||||||
currentViewHolder = this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pauseAudio() {
|
private fun pauseAudio() {
|
||||||
mediaPlayer?.let { mp ->
|
mediaPlayer?.let { mp ->
|
||||||
if (mp.isPlaying) {
|
try {
|
||||||
mp.pause()
|
if (mp.isPlaying) {
|
||||||
isPlaying = false
|
mp.pause()
|
||||||
binding.playPause.setImageResource(R.drawable.play)
|
isPlaying = false
|
||||||
seekHandler.removeCallbacks(seekRunnable!!)
|
binding.playPause.setImageResource(R.drawable.play)
|
||||||
|
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
releaseMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun stopAndResetAudio() {
|
||||||
|
try {
|
||||||
|
mediaPlayer?.let { mp ->
|
||||||
|
if (mp.isPlaying) {
|
||||||
|
mp.stop()
|
||||||
|
mp.prepare()
|
||||||
|
} else if (isMediaPlayerPrepared) {
|
||||||
|
mp.seekTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isPlaying = false
|
||||||
|
resetAudioUI()
|
||||||
|
|
||||||
|
if (currentPlayingPosition == currentPosition) {
|
||||||
|
currentPlayingPosition = -1
|
||||||
|
currentViewHolder = null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
releaseMediaPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun releaseMediaPlayer() {
|
fun releaseMediaPlayer() {
|
||||||
mediaPlayer?.let { mp ->
|
try {
|
||||||
if (mp.isPlaying) {
|
mediaPlayer?.let { mp ->
|
||||||
mp.stop()
|
if (mp.isPlaying) {
|
||||||
|
mp.stop()
|
||||||
|
}
|
||||||
|
mp.release()
|
||||||
}
|
}
|
||||||
mp.release()
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
mediaPlayer = null
|
mediaPlayer = null
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
seekHandler.removeCallbacks(seekRunnable!!)
|
isMediaPlayerPrepared = false
|
||||||
|
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||||
|
|
||||||
|
if (currentPlayingPosition == currentPosition) {
|
||||||
|
currentPlayingPosition = -1
|
||||||
|
currentViewHolder = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playVideoAtPosition(position: Int) {
|
private fun playVideoAtPosition(position: Int) {
|
||||||
val nextFile = images[position]
|
if (position < images.size) {
|
||||||
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
|
val nextFile = images[position]
|
||||||
if (fileType == FileManager.FileType.VIDEO) {
|
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
|
||||||
val videoUri = Uri.fromFile(nextFile)
|
if (fileType == FileManager.FileType.VIDEO) {
|
||||||
binding.videoView.setVideoURI(videoUri)
|
val videoUri = Uri.fromFile(nextFile)
|
||||||
binding.videoView.start()
|
binding.videoView.setVideoURI(videoUri)
|
||||||
|
binding.videoView.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,5 +403,14 @@ class ImagePreviewAdapter(
|
|||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
holder.releaseMediaPlayer()
|
holder.releaseMediaPlayer()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
fun onItemScrolledAway(position: Int) {
|
||||||
|
if (currentPlayingPosition == position) {
|
||||||
|
stopAndResetCurrentAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun releaseAllResources() {
|
||||||
|
stopAndResetCurrentAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package devs.org.calculator.adapters
|
package devs.org.calculator.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
@@ -22,9 +22,9 @@ class ListFolderAdapter(
|
|||||||
private var isSelectionMode = false
|
private var isSelectionMode = false
|
||||||
|
|
||||||
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
|
private val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
|
||||||
|
|
||||||
val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
|
private val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
|
||||||
|
|
||||||
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
|
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
|
||||||
folderNameTextView.text = folder.name
|
folderNameTextView.text = folder.name
|
||||||
@@ -89,6 +89,7 @@ class ListFolderAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun clearSelection() {
|
fun clearSelection() {
|
||||||
val wasInSelectionMode = isSelectionMode
|
val wasInSelectionMode = isSelectionMode
|
||||||
selectedItems.clear()
|
selectedItems.clear()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,10 +27,20 @@ import java.io.File
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import devs.org.calculator.R
|
import devs.org.calculator.R
|
||||||
|
import devs.org.calculator.database.AppDatabase
|
||||||
|
import devs.org.calculator.database.HiddenFileRepository
|
||||||
|
import devs.org.calculator.utils.PrefsUtil
|
||||||
|
import android.util.Log
|
||||||
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
|
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
|
||||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
|
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
|
||||||
|
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||||
|
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HIDDEN_DIR = ".CalculatorHide"
|
const val HIDDEN_DIR = ".CalculatorHide"
|
||||||
@@ -38,8 +48,10 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
const val VIDEOS_DIR = "videos"
|
const val VIDEOS_DIR = "videos"
|
||||||
const val AUDIO_DIR = "audio"
|
const val AUDIO_DIR = "audio"
|
||||||
const val DOCS_DIR = "documents"
|
const val DOCS_DIR = "documents"
|
||||||
|
const val ENCRYPTED_EXTENSION = ".enc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getHiddenDirectory(): File {
|
fun getHiddenDirectory(): File {
|
||||||
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
@@ -47,7 +59,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
if (!created) {
|
if (!created) {
|
||||||
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
|
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
|
||||||
}
|
}
|
||||||
// Create .nomedia file to hide from media scanners
|
|
||||||
val nomediaFile = File(dir, ".nomedia")
|
val nomediaFile = File(dir, ".nomedia")
|
||||||
if (!nomediaFile.exists()) {
|
if (!nomediaFile.exists()) {
|
||||||
nomediaFile.createNewFile()
|
nomediaFile.createNewFile()
|
||||||
@@ -92,7 +103,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
val extension = MimeTypeMap.getSingleton()
|
val extension = MimeTypeMap.getSingleton()
|
||||||
.getExtensionFromMimeType(mimeType) ?: ""
|
.getExtensionFromMimeType(mimeType) ?: ""
|
||||||
val fileName = "${System.currentTimeMillis()}.${extension}"
|
val fileName = "${System.currentTimeMillis()}.${extension}"
|
||||||
val targetFile = File(targetDir, fileName)
|
var targetFile = File(targetDir, fileName)
|
||||||
|
|
||||||
// Copy file using DocumentFile
|
// Copy file using DocumentFile
|
||||||
contentResolver.openInputStream(uri)?.use { input ->
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
@@ -106,6 +117,8 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
throw Exception("File copy failed")
|
throw Exception("File copy failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt file if encryption is enabled
|
||||||
|
|
||||||
// Media scan the new file to hide it
|
// Media scan the new file to hide it
|
||||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||||
mediaScanIntent.data = Uri.fromFile(targetDir)
|
mediaScanIntent.data = Uri.fromFile(targetDir)
|
||||||
@@ -179,41 +192,39 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
targetFile
|
targetFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy file content
|
// Check if file is encrypted
|
||||||
file.copyTo(finalTargetFile, overwrite = false)
|
if (file.extension == ENCRYPTED_EXTENSION) {
|
||||||
|
// Decrypt file
|
||||||
// Verify copy success
|
val decryptedFile = SecurityUtils.changeFileExtension(file, SecurityUtils.getFileExtension(file))
|
||||||
if (finalTargetFile.exists() && finalTargetFile.length() > 0) {
|
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||||
// Delete original hidden file
|
decryptedFile.copyTo(finalTargetFile, overwrite = false)
|
||||||
if (file.delete()) {
|
decryptedFile.delete()
|
||||||
// Trigger media scan for the new file
|
|
||||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
|
||||||
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
|
|
||||||
context.sendBroadcast(mediaScanIntent)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Toast.makeText(context, context.getString(R.string.file_unhidden_successfully), Toast.LENGTH_SHORT).show()
|
|
||||||
onSuccess?.invoke() // Call success callback
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
throw Exception("Failed to decrypt file")
|
||||||
Toast.makeText(context, "File copied but failed to remove from hidden folder", Toast.LENGTH_SHORT).show()
|
|
||||||
onError?.invoke("Failed to remove from hidden folder")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
// Copy file content
|
||||||
Toast.makeText(context, "Failed to copy file", Toast.LENGTH_SHORT).show()
|
file.copyTo(finalTargetFile, overwrite = false)
|
||||||
onError?.invoke("Failed to copy file")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
// Verify copy success
|
||||||
withContext(Dispatchers.Main) {
|
if (!finalTargetFile.exists() || finalTargetFile.length() == 0L) {
|
||||||
Toast.makeText(context, "Error unhiding file: ${e.message}", Toast.LENGTH_LONG).show()
|
throw Exception("File copy failed")
|
||||||
onError?.invoke(e.message ?: "Unknown error")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Media scan the new file
|
||||||
|
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||||
|
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
|
||||||
|
context.sendBroadcast(mediaScanIntent)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onSuccess?.invoke()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError?.invoke(e.message ?: "Unknown error occurred")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,4 +399,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
|||||||
DOCUMENT(DOCS_DIR),
|
DOCUMENT(DOCS_DIR),
|
||||||
ALL("all")
|
ALL("all")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
package devs.org.calculator.utils
|
package devs.org.calculator.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Environment
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class FolderManager(private val context: Context) {
|
class FolderManager {
|
||||||
companion object {
|
|
||||||
const val HIDDEN_DIR = ".CalculatorHide"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createFolder(parentDir: File, folderName: String): Boolean {
|
fun createFolder(parentDir: File, folderName: String): Boolean {
|
||||||
val newFolder = File(parentDir, folderName)
|
val newFolder = File(parentDir, folderName)
|
||||||
return if (!newFolder.exists()) {
|
return if (!newFolder.exists()) {
|
||||||
newFolder.mkdirs()
|
newFolder.mkdirs()
|
||||||
// Create .nomedia file to hide from media scanners
|
|
||||||
File(newFolder, ".nomedia").createNewFile()
|
File(newFolder, ".nomedia").createNewFile()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -54,20 +49,4 @@ class FolderManager(private val context: Context) {
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveFileToFolder(file: File, targetFolder: File): Boolean {
|
|
||||||
return try {
|
|
||||||
if (!targetFolder.exists()) {
|
|
||||||
targetFolder.mkdirs()
|
|
||||||
File(targetFolder, ".nomedia").createNewFile()
|
|
||||||
}
|
|
||||||
val newFile = File(targetFolder, file.name)
|
|
||||||
file.copyTo(newFile, overwrite = true)
|
|
||||||
file.delete()
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package devs.org.calculator.utils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
class PrefsUtil(context: Context) {
|
class PrefsUtil(context: Context) {
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE)
|
private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE)
|
||||||
@@ -13,26 +14,33 @@ class PrefsUtil(context: Context) {
|
|||||||
|
|
||||||
fun savePassword(password: String) {
|
fun savePassword(password: String) {
|
||||||
val hashedPassword = hashPassword(password)
|
val hashedPassword = hashPassword(password)
|
||||||
prefs.edit()
|
prefs.edit {
|
||||||
.putString("password", hashedPassword)
|
putString("password", hashedPassword)
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBoolean(key:String, value: Boolean){
|
fun setBoolean(key:String, value: Boolean){
|
||||||
return prefs.edit().putBoolean(key,value).apply()
|
return prefs.edit { putBoolean(key, value) }
|
||||||
|
|
||||||
|
}
|
||||||
|
fun setInt(key:String, value: Int){
|
||||||
|
return prefs.edit { putInt(key, value) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBoolean(key: String, defValue: Boolean = false): Boolean{
|
fun getBoolean(key: String, defValue: Boolean = false): Boolean{
|
||||||
return prefs.getBoolean(key,defValue)
|
return prefs.getBoolean(key,defValue)
|
||||||
}
|
}
|
||||||
|
fun getInt(key: String, defValue: Int): Int{
|
||||||
|
return prefs.getInt(key,defValue)
|
||||||
|
}
|
||||||
|
|
||||||
fun resetPassword(){
|
fun resetPassword(){
|
||||||
prefs.edit()
|
prefs.edit {
|
||||||
.remove("password")
|
remove("password")
|
||||||
.remove("security_question")
|
.remove("security_question")
|
||||||
.remove("security_answer")
|
.remove("security_answer")
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validatePassword(input: String): Boolean {
|
fun validatePassword(input: String): Boolean {
|
||||||
@@ -40,11 +48,15 @@ class PrefsUtil(context: Context) {
|
|||||||
return stored == hashPassword(input)
|
return stored == hashPassword(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPassword(): String{
|
||||||
|
return prefs.getString("password", "") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
fun saveSecurityQA(question: String, answer: String) {
|
fun saveSecurityQA(question: String, answer: String) {
|
||||||
prefs.edit()
|
prefs.edit {
|
||||||
.putString("security_question", question)
|
putString("security_question", question)
|
||||||
.putString("security_answer", hashPassword(answer))
|
.putString("security_answer", hashPassword(answer))
|
||||||
.apply()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateSecurityAnswer(answer: String): Boolean {
|
fun validateSecurityAnswer(answer: String): Boolean {
|
||||||
|
|||||||
@@ -5,60 +5,237 @@ import android.net.Uri
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.security.SecureRandom
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.CipherOutputStream
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import devs.org.calculator.database.HiddenFileEntity
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
class SecurityUtils {
|
object SecurityUtils {
|
||||||
companion object {
|
private const val ALGORITHM = "AES"
|
||||||
private const val ALGORITHM = "AES"
|
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||||
private const val HIDDEN_FOLDER = "Calculator_Data"
|
private const val KEY_SIZE = 256
|
||||||
|
|
||||||
|
private fun getSecretKey(context: Context): SecretKey {
|
||||||
|
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||||
|
val useCustomKey = keyStore.getBoolean("use_custom_key", false)
|
||||||
|
|
||||||
fun validatePassword(input: String, storedHash: String): Boolean {
|
if (useCustomKey) {
|
||||||
return input.hashCode().toString() == storedHash
|
val customKey = keyStore.getString("custom_key", null)
|
||||||
}
|
if (customKey != null) {
|
||||||
|
try {
|
||||||
fun encryptFile(context: Context, sourceUri: Uri, password: String): File {
|
val messageDigest = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
val inputStream = context.contentResolver.openInputStream(sourceUri)
|
val keyBytes = messageDigest.digest(customKey.toByteArray())
|
||||||
val secretKey = generateKey(password)
|
return SecretKeySpec(keyBytes, ALGORITHM)
|
||||||
val cipher = Cipher.getInstance(ALGORITHM)
|
} catch (_: Exception) {
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
|
||||||
|
|
||||||
val hiddenDir = File(context.getExternalFilesDir(null), HIDDEN_FOLDER)
|
|
||||||
if (!hiddenDir.exists()) hiddenDir.mkdirs()
|
|
||||||
|
|
||||||
val encryptedFile = File(hiddenDir, "${System.currentTimeMillis()}_encrypted")
|
|
||||||
val outputStream = FileOutputStream(encryptedFile)
|
|
||||||
|
|
||||||
inputStream?.use { input ->
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var read: Int
|
|
||||||
while (input.read(buffer).also { read = it } != -1) {
|
|
||||||
val encrypted = cipher.update(buffer, 0, read)
|
|
||||||
outputStream.write(encrypted)
|
|
||||||
}
|
}
|
||||||
val finalBlock = cipher.doFinal()
|
|
||||||
outputStream.write(finalBlock)
|
|
||||||
}
|
}
|
||||||
outputStream.close()
|
|
||||||
return encryptedFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptFile(file: File, password: String): ByteArray {
|
val encodedKey = keyStore.getString("secret_key", null)
|
||||||
val secretKey = generateKey(password)
|
return if (encodedKey != null) {
|
||||||
val cipher = Cipher.getInstance(ALGORITHM)
|
try {
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey)
|
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
|
||||||
|
SecretKeySpec(decodedKey, ALGORITHM)
|
||||||
val inputStream = FileInputStream(file)
|
} catch (_: Exception) {
|
||||||
val bytes = inputStream.readBytes()
|
generateAndStoreNewKey(keyStore)
|
||||||
inputStream.close()
|
}
|
||||||
|
} else {
|
||||||
return cipher.doFinal(bytes)
|
generateAndStoreNewKey(keyStore)
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateKey(password: String): SecretKey {
|
|
||||||
val keyBytes = password.toByteArray().copyOf(16)
|
|
||||||
return SecretKeySpec(keyBytes, ALGORITHM)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateAndStoreNewKey(keyStore: SharedPreferences): SecretKey {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
|
||||||
|
keyGenerator.init(KEY_SIZE, SecureRandom())
|
||||||
|
val key = keyGenerator.generateKey()
|
||||||
|
val encodedKey = android.util.Base64.encodeToString(key.encoded, android.util.Base64.DEFAULT)
|
||||||
|
keyStore.edit { putString("secret_key", encodedKey) }
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||||
|
return try {
|
||||||
|
if (!inputFile.exists()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val secretKey = getSecretKey(context)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||||
|
|
||||||
|
FileInputStream(inputFile).use { input ->
|
||||||
|
FileOutputStream(outputFile).use { output ->
|
||||||
|
output.write(iv)
|
||||||
|
CipherOutputStream(output, cipher).use { cipherOutput ->
|
||||||
|
input.copyTo(cipherOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream(outputFile).use { input ->
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
val bytesRead = input.read(iv)
|
||||||
|
if (bytesRead != 16) {
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
if (outputFile.exists()) {
|
||||||
|
outputFile.delete()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDecryptedPreviewFile(context: Context, meta: HiddenFileEntity): File? {
|
||||||
|
try {
|
||||||
|
val encryptedFile = File(meta.filePath)
|
||||||
|
if (!encryptedFile.exists()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val tempDir = File(context.cacheDir, "preview_temp")
|
||||||
|
if (!tempDir.exists()) tempDir.mkdirs()
|
||||||
|
|
||||||
|
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
|
||||||
|
|
||||||
|
tempDir.listFiles()?.forEach { it.delete() }
|
||||||
|
|
||||||
|
val success = decryptFile(context, encryptedFile, tempFile)
|
||||||
|
|
||||||
|
if (success && tempFile.exists() && tempFile.length() > 0) {
|
||||||
|
return tempFile
|
||||||
|
} else {
|
||||||
|
if (tempFile.exists()) tempFile.delete()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getUriForPreviewFile(context: Context, file: File): Uri? {
|
||||||
|
return try {
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.provider", // Must match AndroidManifest
|
||||||
|
file
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||||
|
return try {
|
||||||
|
if (!inputFile.exists()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputFile.length() == 0L) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val secretKey = getSecretKey(context)
|
||||||
|
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||||
|
FileInputStream(inputFile).use { input ->
|
||||||
|
val iv = ByteArray(16)
|
||||||
|
val bytesRead = input.read(iv)
|
||||||
|
if (bytesRead != 16) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||||
|
|
||||||
|
FileInputStream(inputFile).use { decInput ->
|
||||||
|
decInput.skip(16)
|
||||||
|
|
||||||
|
FileOutputStream(outputFile).use { output ->
|
||||||
|
CipherInputStream(decInput, cipher).use { cipherInput ->
|
||||||
|
cipherInput.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
if (outputFile.exists()) {
|
||||||
|
outputFile.delete()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileExtension(file: File): String {
|
||||||
|
val name = file.name
|
||||||
|
val lastDotIndex = name.lastIndexOf('.')
|
||||||
|
return if (lastDotIndex > 0) {
|
||||||
|
name.substring(lastDotIndex)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeFileExtension(file: File, newExtension: String): File {
|
||||||
|
val name = file.name
|
||||||
|
val lastDotIndex = name.lastIndexOf('.')
|
||||||
|
val newName = if (lastDotIndex > 0) {
|
||||||
|
name.substring(0, lastDotIndex) + newExtension
|
||||||
|
} else {
|
||||||
|
name + newExtension
|
||||||
|
}
|
||||||
|
return File(file.parent, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCustomKey(context: Context, key: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||||
|
keyStore.edit {
|
||||||
|
putString("custom_key", key)
|
||||||
|
putBoolean("use_custom_key", true)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCustomKey(context: Context) {
|
||||||
|
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||||
|
keyStore.edit {
|
||||||
|
remove("custom_key")
|
||||||
|
putBoolean("use_custom_key", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isUsingCustomKey(context: Context): Boolean {
|
||||||
|
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||||
|
return keyStore.getBoolean("use_custom_key", false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
|||||||
onGranted()
|
onGranted()
|
||||||
} else {
|
} else {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
||||||
data = Uri.parse("package:${activity.packageName}")
|
data = "package:${activity.packageName}".toUri()
|
||||||
}
|
}
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="800dp"
|
android:width="50dp"
|
||||||
android:height="800dp"
|
android:height="50dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<corners android:bottomLeftRadius="15dp" android:bottomRightRadius="15dp"/>
|
<corners android:bottomLeftRadius="15dp" android:bottomRightRadius="15dp"/>
|
||||||
<solid android:color="?attr/cardForegroundColor"/>
|
<solid android:color="?attr/colorSecondaryContainer"/>
|
||||||
</shape>
|
</shape>
|
||||||
5
app/src/main/res/drawable/bottom_shade.xml
Normal 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>
|
||||||
17
app/src/main/res/drawable/encrypted.xml
Normal 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>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="50dp"
|
||||||
android:height="24dp"
|
android:height="50dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/textColor"
|
android:fillColor="#FF000000"
|
||||||
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6H6zM13,9V3.5L18.5,9H13z"/>
|
android:pathData="M13.172,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.828c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828C14.211,2.211 13.702,2 13.172,2zM15,18H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,17.552 15.552,18 15,18zM15,14H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,13.552 15.552,14 15,14zM13,9V3.5L18.5,9H13z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_file_no_item.xml
Normal 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>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnChangePassword"
|
android:id="@+id/btnChangePassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="50dp"
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="32dp"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:text="@string/change_password"
|
android:text="@string/change_password"
|
||||||
|
|||||||
@@ -39,57 +39,38 @@
|
|||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:id="@+id/folderName"/>
|
android:id="@+id/folderName"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_edit"
|
app:icon="@drawable/ic_edit"
|
||||||
app:tint="?attr/colorPrimary"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:background="#00000000"
|
|
||||||
android:id="@+id/edit"/>
|
android:id="@+id/edit"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/delete"
|
android:id="@+id/delete"
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="#00000000"
|
app:icon="@drawable/ic_delete"
|
||||||
android:padding="9dp"
|
style="@style/Widget.Material3.Button.IconButton" />
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_delete"
|
|
||||||
app:tint="?attr/colorPrimary"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/menuButton"
|
android:id="@+id/menuButton"
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_more"
|
app:icon="@drawable/ic_more"
|
||||||
app:tint="?attr/colorPrimary"
|
style="@style/Widget.Material3.Button.IconButton"/>
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:background="#00000000"/>
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_list"
|
app:icon="@drawable/ic_list"
|
||||||
android:scaleType="fitCenter"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
app:tint="?attr/colorPrimary"
|
|
||||||
android:background="#00000000"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:id="@+id/folderOrientation"/>
|
android:id="@+id/folderOrientation"/>
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_settings"
|
app:icon="@drawable/ic_settings"
|
||||||
android:scaleType="fitCenter"
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
app:tint="?attr/colorPrimary"
|
|
||||||
|
|
||||||
android:background="#00000000"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:id="@+id/settings"/>
|
android:id="@+id/settings"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -128,7 +109,7 @@
|
|||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:text="@string/no_items_available_add_one_by_clicking_on_the_plus_button"
|
android:text="@string/there_is_no_folders_available_create_one_by_clicking_on_the_add_folder_button_showing_in_the_bottom"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
android:layout_margin="0dp"
|
android:layout_margin="0dp"
|
||||||
android:background="@drawable/bottom_corner"
|
android:background="@drawable/bottom_corner"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHeight_percent="0.3"
|
app:layout_constraintHeight_percent="0.4"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
|
android:paddingTop="27dp"
|
||||||
android:gravity="end|bottom"
|
android:gravity="end|bottom"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/total"
|
app:layout_constraintBottom_toTopOf="@+id/total"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:autoSizeMaxTextSize="40sp"
|
android:autoSizeMaxTextSize="40sp"
|
||||||
android:autoSizeMinTextSize="24sp"
|
android:autoSizeMinTextSize="12sp"
|
||||||
android:autoSizeStepGranularity="2sp"
|
android:autoSizeStepGranularity="2sp"
|
||||||
android:autoSizeTextType="uniform"
|
android:autoSizeTextType="uniform"
|
||||||
android:gravity="end|bottom"
|
android:gravity="end|bottom"
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/displayContainer">
|
app:layout_constraintTop_toBottomOf="@id/displayContainer">
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -95,49 +96,68 @@
|
|||||||
android:id="@+id/btnClear"
|
android:id="@+id/btnClear"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_weight="1"
|
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||||
android:text="C"
|
android:text="C"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnPercent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnPercent"
|
android:id="@+id/btnPercent"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="%"
|
android:text="%"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
android:layout_marginHorizontal="3dp"
|
||||||
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnDivide"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btnClear"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnDivide"
|
android:id="@+id/btnDivide"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="÷"
|
android:text="÷"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
android:layout_marginHorizontal="3dp"
|
||||||
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/cut"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btnPercent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/cut"
|
android:id="@+id/cut"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
android:background="@drawable/gradient_bg"
|
android:background="@drawable/gradient_bg"
|
||||||
|
android:scaleType="centerInside"
|
||||||
android:src="@drawable/backspace"
|
android:src="@drawable/backspace"
|
||||||
android:scaleType="centerInside"/>
|
android:layout_marginHorizontal="3dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btnDivide"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -147,10 +167,16 @@
|
|||||||
android:id="@+id/btn7"
|
android:id="@+id/btn7"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="7"
|
android:text="7"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn8"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp" />
|
||||||
|
|
||||||
@@ -158,21 +184,32 @@
|
|||||||
android:id="@+id/btn8"
|
android:id="@+id/btn8"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="8"
|
android:text="8"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn9"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn7"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn9"
|
android:id="@+id/btn9"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="9"
|
android:text="9"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnMultiply"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn8"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp" />
|
||||||
|
|
||||||
@@ -180,18 +217,22 @@
|
|||||||
android:id="@+id/btnMultiply"
|
android:id="@+id/btnMultiply"
|
||||||
style="@style/CustomMaterialButton"
|
style="@style/CustomMaterialButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:textColor="@color/white"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="×"
|
android:text="×"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn9"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -201,51 +242,71 @@
|
|||||||
android:id="@+id/btn4"
|
android:id="@+id/btn4"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="4"
|
android:text="4"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn5"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn5"
|
android:id="@+id/btn5"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="5"
|
android:text="5"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn6"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn4"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn6"
|
android:id="@+id/btn6"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="6"
|
android:text="6"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnMinus"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn5"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnMinus"
|
android:id="@+id/btnMinus"
|
||||||
style="@style/CustomMaterialButton"
|
style="@style/CustomMaterialButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:text="-"
|
android:text="-"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn6"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -255,92 +316,144 @@
|
|||||||
android:id="@+id/btn1"
|
android:id="@+id/btn1"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="1"
|
android:text="1"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn2"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn2"
|
android:id="@+id/btn2"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="2"
|
android:text="2"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn3"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn1"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn3"
|
android:id="@+id/btn3"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="3"
|
android:text="3"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnPlus"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn2"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnPlus"
|
android:id="@+id/btnPlus"
|
||||||
style="@style/CustomMaterialButton"
|
style="@style/CustomMaterialButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="+"
|
android:text="+"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn3"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn00"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
|
android:text="00"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="30sp"
|
||||||
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn0"
|
android:id="@+id/btn0"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_weight="1"
|
||||||
android:layout_weight="2"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="0"
|
android:text="0"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnDot"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn00"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnDot"
|
android:id="@+id/btnDot"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="4dp"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="."
|
android:text="."
|
||||||
|
android:layout_weight="1"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1.15"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnEquals"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn0"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnEquals"
|
android:id="@+id/btnEquals"
|
||||||
style="@style/CustomMaterialButton"
|
style="@style/CustomMaterialButton"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_marginHorizontal="4dp"
|
|
||||||
android:layout_marginVertical="8dp"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:textColor="@color/white"
|
android:layout_marginHorizontal="3dp"
|
||||||
android:text="="
|
android:text="="
|
||||||
|
android:textColor="@color/white"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
app:cornerRadius="15dp" />
|
app:cornerRadius="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btnDot"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:cardCornerRadius="10dp"
|
app:cardCornerRadius="10dp"
|
||||||
app:cardElevation="5dp">
|
android:background="#00000000"
|
||||||
|
android:backgroundTint="#00000000"
|
||||||
|
app:cardElevation="0dp">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/appLogo"
|
android:id="@+id/appLogo"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
@@ -254,6 +256,23 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/restrict_screenshots_in_hidden_section"
|
android:text="@string/restrict_screenshots_in_hidden_section"
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/encryptionSwitch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/encrypt_file_when_hiding"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/customKeyStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/set_custom_encryption_key"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
|||||||
@@ -40,16 +40,12 @@
|
|||||||
android:id="@+id/folderName"/>
|
android:id="@+id/folderName"/>
|
||||||
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/menuButton"
|
android:id="@+id/menuButton"
|
||||||
android:layout_width="40dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="40dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_more"
|
app:icon="@drawable/ic_more"
|
||||||
app:tint="?attr/colorPrimary"
|
style="@style/Widget.Material3.Button.IconButton"/>
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:padding="7dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:background="#00000000"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -79,7 +75,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:src="@drawable/ic_no_items" />
|
android:src="@drawable/ic_file_no_item" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/noItemsTxt"
|
android:id="@+id/noItemsTxt"
|
||||||
|
|||||||
47
app/src/main/res/layout/dialog_custom_key.xml
Normal 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>
|
||||||
29
app/src/main/res/layout/dialog_file_type_selection.xml
Normal 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>
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="4dp"
|
android:layout_margin="4dp"
|
||||||
app:cardCornerRadius="8dp"
|
app:cardCornerRadius="8dp">
|
||||||
app:cardElevation="2dp">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="12dp"
|
android:layout_margin="3dp"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<ImageView
|
<LinearLayout
|
||||||
android:id="@+id/folderIcon"
|
|
||||||
android:layout_width="34dp"
|
|
||||||
android:layout_height="34dp"
|
|
||||||
android:src="@drawable/ic_folder"
|
|
||||||
app:tint="?attr/colorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/folderName"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:padding="12dp"
|
||||||
android:textSize="18sp"/>
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/folderIcon"
|
||||||
|
android:layout_width="34dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:src="@drawable/ic_folder"
|
||||||
|
app:tint="?attr/colorPrimary" />
|
||||||
|
|
||||||
</LinearLayout>
|
<TextView
|
||||||
|
android:id="@+id/folderName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:textSize="18sp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/cardView"
|
android:id="@+id/cardView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:cardCornerRadius="10dp"
|
app:cardCornerRadius="10dp"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
app:cardElevation="3dp"
|
android:backgroundTint="#404040"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintDimensionRatio="1:1"
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@@ -37,6 +37,11 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:src="@drawable/add_image" />
|
android:src="@drawable/add_image" />
|
||||||
|
<View
|
||||||
|
android:id="@+id/shade"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/bottom_shade"/>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/selectedLayer"
|
android:id="@+id/selectedLayer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -49,7 +54,21 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fileNameTextView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="13dp"
|
||||||
|
android:paddingVertical="6dp"
|
||||||
|
android:textColor="#fff"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/cardView"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -60,6 +79,13 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/ic_play_circle"
|
android:src="@drawable/ic_play_circle"
|
||||||
android:layout_gravity="center"/>
|
android:layout_gravity="center"/>
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:src="@drawable/encrypted"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:id="@+id/encrypted"/>
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/selected"
|
android:id="@+id/selected"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
@@ -70,16 +96,7 @@
|
|||||||
android:src="@drawable/selected"
|
android:src="@drawable/selected"
|
||||||
android:background="@drawable/gradient_bg"
|
android:background="@drawable/gradient_bg"
|
||||||
android:layout_gravity="end"/>
|
android:layout_gravity="end"/>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/fileNameTextView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
app:cardCornerRadius="15dp"
|
app:cardCornerRadius="15dp"
|
||||||
app:cardElevation="10dp"
|
|
||||||
android:layout_margin="20dp">
|
android:layout_margin="20dp">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
<item name="colorSecondaryContainer">#481A4324</item>
|
||||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
|
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<string name="preview_videos">Preview Videos</string>
|
<string name="preview_videos">Preview Videos</string>
|
||||||
<string name="preview_audios">Preview Audios</string>
|
<string name="preview_audios">Preview Audios</string>
|
||||||
<string name="preview_documents">Preview Documents</string>
|
<string name="preview_documents">Preview Documents</string>
|
||||||
<string name="delete_file">Delete File</string>
|
<string name="delete_file">Delete File!</string>
|
||||||
<string name="are_you_sure_to_delete_this_file_permanently">Are you sure to Delete this file permanently?</string>
|
<string name="are_you_sure_to_delete_this_file_permanently">Are you sure to Delete this file permanently?</string>
|
||||||
<string name="delete_permanently">Delete Permanently</string>
|
<string name="delete_permanently">Delete Permanently</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="unknown_file">Unknown File</string>
|
<string name="unknown_file">Unknown File</string>
|
||||||
<string name="details"> DETAILS</string>
|
<string name="details"> DETAILS</string>
|
||||||
<string name="audio_hidded_successfully">Audios hidden successfully</string>
|
<string name="audio_hidded_successfully">Audios hidden successfully</string>
|
||||||
<string name="no_items_available_add_one_by_clicking_on_the_plus_button">No Items Available, Add one by clicking on the</string>
|
<string name="no_items_available_add_one_by_clicking_on_the_plus_button">No Files Available, Add one by clicking on the \'+\' button</string>
|
||||||
<string name="now_enter_button">Now Enter \'=\' button</string>
|
<string name="now_enter_button">Now Enter \'=\' button</string>
|
||||||
<string name="enter_123456">Enter 123456</string>
|
<string name="enter_123456">Enter 123456</string>
|
||||||
<string name="create_folder">Create Folder</string>
|
<string name="create_folder">Create Folder</string>
|
||||||
@@ -79,6 +79,7 @@
|
|||||||
<string name="error_loading_files">Error loading files</string>
|
<string name="error_loading_files">Error loading files</string>
|
||||||
<string name="delete_items">Delete Folder</string>
|
<string name="delete_items">Delete Folder</string>
|
||||||
<string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected Folders?</string>
|
<string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected Folders?</string>
|
||||||
|
<string name="folder_will_be_deleted_permanently">Note: If you delete the folder(S) all the files available inside the folder will be deleted !</string>
|
||||||
<string name="items_deleted_successfully">Items deleted successfully</string>
|
<string name="items_deleted_successfully">Items deleted successfully</string>
|
||||||
<string name="folder_deleted_successfully">Folder deleted successfully</string>
|
<string name="folder_deleted_successfully">Folder deleted successfully</string>
|
||||||
<string name="some_items_could_not_be_deleted">Some items could not be deleted</string>
|
<string name="some_items_could_not_be_deleted">Some items could not be deleted</string>
|
||||||
@@ -107,8 +108,8 @@
|
|||||||
<string name="are_you_sure_you_want_to_un_hide_selected_files">Are you sure you want to unhide the selected files?</string>
|
<string name="are_you_sure_you_want_to_un_hide_selected_files">Are you sure you want to unhide the selected files?</string>
|
||||||
<string name="files_unhidden_successfully">Files unhidden successfully</string>
|
<string name="files_unhidden_successfully">Files unhidden successfully</string>
|
||||||
<string name="some_files_could_not_be_unhidden">Some files could not be unhidden</string>
|
<string name="some_files_could_not_be_unhidden">Some files could not be unhidden</string>
|
||||||
<string name="copy_to_another_folder">Copy to Another Folder</string>
|
<string name="copy_to_another_folder">Copy</string>
|
||||||
<string name="move_to_another_folder">Move to Another Folder</string>
|
<string name="move_to_another_folder">Move</string>
|
||||||
<string name="select_destination_folder">Select Destination Folder</string>
|
<string name="select_destination_folder">Select Destination Folder</string>
|
||||||
<string name="no_folders_available">No other folders available</string>
|
<string name="no_folders_available">No other folders available</string>
|
||||||
<string name="change_password">Change Password</string>
|
<string name="change_password">Change Password</string>
|
||||||
@@ -120,10 +121,59 @@
|
|||||||
<string name="app_settings">App Settings</string>
|
<string name="app_settings">App Settings</string>
|
||||||
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
|
<string name="restrict_screenshots_in_hidden_section">Restrict Screenshots in Hidden Section</string>
|
||||||
<string name="security_settings">Security Settings</string>
|
<string name="security_settings">Security Settings</string>
|
||||||
<string name="version">Version 1.3</string>
|
<string name="version">Version 1.4.0</string>
|
||||||
<string name="app_details">App Details</string>
|
<string name="app_details">App Details</string>
|
||||||
<string name="setup_password">Setup Password</string>
|
<string name="setup_password">Setup Password</string>
|
||||||
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
|
<string name="security_question_for_password_reset">Security Question (For Password Reset)</string>
|
||||||
<string name="security_answer">Security Answer</string>
|
<string name="security_answer">Security Answer</string>
|
||||||
<string name="save_password">Save Password</string>
|
<string name="save_password">Save Password</string>
|
||||||
|
<string name="there_is_no_folders_available_create_one_by_clicking_on_the_add_folder_button_showing_in_the_bottom">There is no Folders Available, create one by clicking on the \'Add Folder\' Button showing in the bottom.</string>
|
||||||
|
<string name="storage_permission">Storage Permission</string>
|
||||||
|
<string name="to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission">To ensure the app works properly and allows you to easily hide or un-hide your private files, please grant storage access permission.\n</string>
|
||||||
|
<string name="for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission">For devices running Android 11 or higher, you\'ll need to grant the \'All Files Access\' permission.</string>
|
||||||
|
<string name="grant_permission">Grant Permission</string>
|
||||||
|
<string name="later">Later</string>
|
||||||
|
<string name="storage_permission_is_required_for_the_app_to_function_properly">Storage permission is required for the app to function properly</string>
|
||||||
|
<string name="you_can_grant_permission_later_from_settings">You can grant permission later from Settings</string>
|
||||||
|
<string name="permission_granted">Permission granted</string>
|
||||||
|
<string name="permission_denied">Permission denied</string>
|
||||||
|
<string name="github_profile">https://github.com/binondi</string>
|
||||||
|
<string name="calculator_hide_files">%1$s/calculator-hide-files</string>
|
||||||
|
<string name="would_you_like_to_set_a_specific_theme_mode">Would you like to set a specific theme mode?</string>
|
||||||
|
<string name="no">No</string>
|
||||||
|
<string name="could_not_open_url">Could not open URL</string>
|
||||||
|
<string name="files_copied_successfully">Files copied successfully</string>
|
||||||
|
<string name="some_files_could_not_be_copied">Some files could not be copied</string>
|
||||||
|
<string name="files_moved_successfully">Files moved successfully</string>
|
||||||
|
<string name="some_files_could_not_be_moved">Some files could not be moved</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app">If you turn on/off this option, dynamic theme changes will be visible after you reopen the app.</string>
|
||||||
|
<string name="attention">Attention!</string>
|
||||||
|
<string name="are_you_sure_to_delete_selected_files_permanently">Are you sure to delete selected files permanently ?</string>
|
||||||
|
<string name="encrypt_file_when_hiding">Encrypt File When Hiding</string>
|
||||||
|
<string name="encrypt_file">Encrypt File</string>
|
||||||
|
<string name="decrypt_file">Decrypt File</string>
|
||||||
|
<string name="encrypt_files">Encrypt Files</string>
|
||||||
|
<string name="decrypt_files">Decrypt Files</string>
|
||||||
|
<string name="encrypt">Encrypt</string>
|
||||||
|
<string name="decrypt">Decrypt</string>
|
||||||
|
<string name="encryption_disclaimer">Warning: This will encrypt the selected files. Please use a custom encryption key from Settings, you can only decrypt the encrypted files with the same key so if you use default key and uninstall the app without decrypting the file you cannot decrypt the file later!. Do you want to continue?</string>
|
||||||
|
<string name="decryption_disclaimer">Warning: This will decrypt the selected files. The encrypted files will be deleted after successful decryption. Make sure to do this. Do you want to continue?</string>
|
||||||
|
<string name="custom_encryption_key_cleared">Custom encryption key cleared</string>
|
||||||
|
<string name="failed_to_set_custom_key">Failed to set custom key</string>
|
||||||
|
<string name="custom_key_set_successfully">Custom key set successfully</string>
|
||||||
|
<string name="keys_do_not_match">Keys do not match</string>
|
||||||
|
<string name="key_cannot_be_empty">Key cannot be empty</string>
|
||||||
|
<string name="set_custom_encryption_key">Set Custom Encryption Key</string>
|
||||||
|
<string name="set">Set</string>
|
||||||
|
<string name="delete_key">Delete Key</string>
|
||||||
|
<string name="enter_folder_name_to_create">Enter Folder Name To Create</string>
|
||||||
|
<string name="please_select_exactly_one_folder_to_edit">Please select exactly one folder to edit</string>
|
||||||
|
<string name="invalid_folder_name">Invalid folder name</string>
|
||||||
|
<string name="folder_with_this_name_already_exists">Folder with this name already exists</string>
|
||||||
|
<string name="failed_to_rename_folder">Failed to rename folder</string>
|
||||||
|
<string name="select_file_type">Select File Type!</string>
|
||||||
|
<string name="please_select_the_type_of_file_to_decrypt">Please select the type of file to decrypt</string>
|
||||||
|
<string name="file_no_longer_exists">File no longer exists</string>
|
||||||
|
<string name="something_went_wrong">Something went wrong !</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="colorControlNormal">#483CFF61</item>
|
<item name="colorControlNormal">#483CFF61</item>
|
||||||
|
<item name="colorSecondaryContainer">#48BDFFCB</item>
|
||||||
|
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
|
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- Allow access to external files -->
|
|
||||||
<external-path name="external_files" path="." />
|
<external-path name="external_files" path="." />
|
||||||
<!-- Allow access to hidden directories -->
|
|
||||||
<external-files-path name="hidden_files" path=".CalculatorHide/" />
|
<external-files-path name="hidden_files" path=".CalculatorHide/" />
|
||||||
|
<cache-path name="cache_files" path="." />
|
||||||
</paths>
|
</paths>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.9.3"
|
||||||
documentfile = "1.0.1"
|
documentfile = "1.1.0"
|
||||||
exp4j = "0.4.8"
|
exp4j = "0.4.8"
|
||||||
glide = "4.16.0"
|
glide = "4.16.0"
|
||||||
kotlin = "1.9.24"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.15.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
lottie = "6.2.0"
|
lottie = "6.2.0"
|
||||||
material = "1.12.0"
|
material = "1.12.0"
|
||||||
activity = "1.9.3"
|
activity = "1.10.1"
|
||||||
constraintlayout = "2.2.0"
|
constraintlayout = "2.2.1"
|
||||||
materialColorUtilities = "1.3.0"
|
materialColorUtilities = "1.3.0"
|
||||||
gridlayout = "1.0.0"
|
gridlayout = "1.1.0"
|
||||||
photoview = "2.3.0"
|
photoview = "2.3.0"
|
||||||
|
roomRuntime = "2.7.1"
|
||||||
viewpager = "1.1.0"
|
viewpager = "1.1.0"
|
||||||
zoomage = "1.3.1"
|
zoomage = "1.3.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
|
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
|
||||||
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
||||||
|
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
|
||||||
|
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
||||||
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
|
androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" }
|
||||||
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
|
exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" }
|
||||||
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||||
|
|||||||