diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3e65b6e..b6cd3d8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,6 +32,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.Calculator"
tools:targetApi="31">
+
@@ -50,9 +53,6 @@
-
diff --git a/app/src/main/java/devs/org/calculator/CalculatorApp.kt b/app/src/main/java/devs/org/calculator/CalculatorApp.kt
index 6d5a0a3..55b74ab 100644
--- a/app/src/main/java/devs/org/calculator/CalculatorApp.kt
+++ b/app/src/main/java/devs/org/calculator/CalculatorApp.kt
@@ -1,12 +1,23 @@
package devs.org.calculator
import android.app.Application
+import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.DynamicColors
class CalculatorApp : Application() {
override fun onCreate() {
super.onCreate()
- // Apply dynamic colors to enable Material You theming
- DynamicColors.applyToActivitiesIfAvailable(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)
+ AppCompatDelegate.setDefaultNightMode(themeMode)
+
+ // Apply dynamic colors only if dynamic theme is enabled
+ if (prefs.getBoolean("dynamic_theme", true)) {
+ DynamicColors.applyToActivitiesIfAvailable(this)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt b/app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt
index 3ed441a..4c789f6 100644
--- a/app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt
@@ -1,484 +1,530 @@
package devs.org.calculator.activities
-import android.Manifest
import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
-import android.provider.Settings
import android.util.Log
import android.view.View
-import android.view.animation.Animation
-import android.view.animation.AnimationUtils
+import android.view.WindowManager
+import android.widget.EditText
import android.widget.Toast
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
-import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderAdapter
-import devs.org.calculator.callbacks.FileProcessCallback
+import devs.org.calculator.adapters.ListFolderAdapter
import devs.org.calculator.databinding.ActivityHiddenBinding
-import devs.org.calculator.databinding.ProccessingDialogBinding
import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
import devs.org.calculator.utils.FolderManager
-import kotlinx.coroutines.launch
+import devs.org.calculator.utils.PrefsUtil
import java.io.File
class HiddenActivity : AppCompatActivity() {
- private var isFabOpen = false
- private lateinit var fabOpen: Animation
- private lateinit var fabClose: Animation
- private lateinit var rotateOpen: Animation
- private lateinit var rotateClose: Animation
-
private lateinit var binding: ActivityHiddenBinding
- private val fileManager = FileManager(this, this)
- private val folderManager = FolderManager(this)
- private val dialogUtil = DialogUtil(this)
- private var customDialog: androidx.appcompat.app.AlertDialog? = null
- private val STORAGE_PERMISSION_CODE = 101
+ private lateinit var fileManager: FileManager
+ private lateinit var folderManager: FolderManager
+ private lateinit var dialogUtil: DialogUtil
private var currentFolder: File? = null
private var folderAdapter: FolderAdapter? = null
- val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
+ private var listFolderAdapter: ListFolderAdapter? = null
+ private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
- private lateinit var pickImageLauncher: ActivityResultLauncher
- private var dialogShowTime: Long = 0
- private val MINIMUM_DIALOG_DURATION = 1700L
+ private val mainHandler = Handler(Looper.getMainLooper())
- private fun showCustomDialog(i: Int) {
- val dialogView = ProccessingDialogBinding.inflate(layoutInflater)
- customDialog = MaterialAlertDialogBuilder(this)
- .setView(dialogView.root)
- .setCancelable(false)
- .create()
- dialogView.title.text = "Hiding $i files"
- customDialog?.show()
- dialogShowTime = System.currentTimeMillis()
-
- }
-
- private fun dismissCustomDialog() {
- val currentTime = System.currentTimeMillis()
- val elapsedTime = currentTime - dialogShowTime
-
- if (elapsedTime < MINIMUM_DIALOG_DURATION) {
- val remainingTime = MINIMUM_DIALOG_DURATION - elapsedTime
- Handler(Looper.getMainLooper()).postDelayed({
- customDialog?.dismiss()
- customDialog = null
- }, remainingTime)
- } else {
- customDialog?.dismiss()
- customDialog = null
- }
+ companion object {
+ private const val TAG = "HiddenActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHiddenBinding.inflate(layoutInflater)
setContentView(binding.root)
- //initialized animations for fabs
- fabOpen = AnimationUtils.loadAnimation(this, R.anim.fab_open)
- fabClose = AnimationUtils.loadAnimation(this, R.anim.fab_close)
- rotateOpen = AnimationUtils.loadAnimation(this, R.anim.rotate_open)
- rotateClose = AnimationUtils.loadAnimation(this, R.anim.rotate_close)
- binding.fabExpend.visibility = View.GONE
- binding.addImage.visibility = View.GONE
- binding.addVideo.visibility = View.GONE
- binding.addAudio.visibility = View.GONE
- binding.addDocument.visibility = View.GONE
+ // Initialize managers
+ fileManager = FileManager(this, this)
+ folderManager = FolderManager(this)
+ dialogUtil = DialogUtil(this)
+
+ setupInitialUIState()
+ setupClickListeners()
+ setupBackPressedHandler()
+
+ // Initialize permissions and load data
+ fileManager.askPermission(this)
+
+ // Set initial orientation icon based on saved preference
+ refreshCurrentView()
+ }
+
+ private fun setupInitialUIState() {
+
binding.addFolder.visibility = View.VISIBLE
binding.deleteSelected.visibility = View.GONE
+ binding.delete.visibility = View.GONE
+ binding.menuButton.visibility = View.GONE
+ }
- binding.fabExpend.setOnClickListener {
- if (isFabOpen) {
- closeFabs()
+ private fun setupClickListeners() {
- } else {
- openFabs()
-
- }
- }
binding.settings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
- binding.addImage.setOnClickListener { openFilePicker("image/*") }
- binding.addVideo.setOnClickListener { openFilePicker("video/*") }
- binding.addAudio.setOnClickListener { openFilePicker("audio/*") }
+
binding.back.setOnClickListener {
- if (currentFolder != null) {
- pressBack()
+ handleBackPress()
+ }
+
+ binding.addFolder.setOnClickListener {
+ createNewFolder()
+ }
+
+ binding.deleteSelected.setOnClickListener {
+ deleteSelectedItems()
+ }
+
+ binding.delete.setOnClickListener {
+ deleteSelectedItems()
+ }
+
+ binding.edit.setOnClickListener {
+ editSelectedFolder()
+ }
+
+ binding.folderOrientation.setOnClickListener {
+ // Switch between grid mode and list mode
+ val currentIsList = PrefsUtil(this).getBoolean("isList", false)
+ val newIsList = !currentIsList
+
+ if (newIsList) {
+ // Switch to list view
+ showListUI()
+ PrefsUtil(this).setBoolean("isList", true)
+ binding.folderOrientation.setImageResource(R.drawable.ic_grid)
} else {
- super.onBackPressed()
+ // Switch to grid view
+ showGridUI()
+ PrefsUtil(this).setBoolean("isList", false)
+ binding.folderOrientation.setImageResource(R.drawable.ic_list)
}
}
- binding.addDocument.setOnClickListener { openFilePicker("*/*") }
- binding.addFolder.setOnClickListener {
- dialogUtil.createInputDialog(
- title = "Enter Folder Name To Create",
- hint = "",
- callback = object : DialogUtil.InputDialogCallback {
- override fun onPositiveButtonClicked(input: String) {
- fileManager.askPermission(this@HiddenActivity)
- folderManager.createFolder( hiddenDir,input )
- listFoldersInHiddenDirectory()
- }
- }
- )
- }
+ }
- fileManager.askPermission(this)
+ private fun showGridUI() {
listFoldersInHiddenDirectory()
+ }
- setupDeleteButton()
+ private fun showListUI() {
+ listFoldersInHiddenDirectoryListStyle()
+ }
- pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val clipData = result.data?.clipData
- val uriList = mutableListOf()
+ private fun listFoldersInHiddenDirectoryListStyle() {
+ try {
+ if (!hiddenDir.exists()) {
+ fileManager.getHiddenDirectory()
+ }
- if (clipData != null) {
- for (i in 0 until clipData.itemCount) {
- val uri = clipData.getItemAt(i).uri
- uriList.add(uri)
- }
+ if (hiddenDir.exists() && hiddenDir.isDirectory) {
+ val folders = folderManager.getFoldersInDirectory(hiddenDir)
+
+ if (folders.isNotEmpty()) {
+ showFolderListStyle(folders)
} else {
- result.data?.data?.let { uriList.add(it) }
+ showEmptyState()
}
+ } else {
+ Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
+ showEmptyState()
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error listing folders: ${e.message}")
+ showEmptyState()
+ }
+ }
- if (uriList.isNotEmpty()) {
- showCustomDialog(uriList.size)
- lifecycleScope.launch {
- if (currentFolder != null){
- FileManager(this@HiddenActivity, this@HiddenActivity)
- .processMultipleFiles(uriList, currentFolder!!,
- object : FileProcessCallback {
- override fun onFilesProcessedSuccessfully(copiedFiles: List) {
- Toast.makeText(this@HiddenActivity, "${copiedFiles.size} ${getString(R.string.documents_hidden_successfully)}", Toast.LENGTH_SHORT).show()
- openFolder(currentFolder!!)
- dismissCustomDialog()
- }
+ private fun setupBackPressedHandler() {
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ handleBackPress()
+ }
+ })
+ }
- override fun onFileProcessFailed() {
- Toast.makeText(this@HiddenActivity,
- getString(R.string.failed_to_hide_files), Toast.LENGTH_SHORT).show()
- dismissCustomDialog()
- }
- })
- }else{
+ private fun createNewFolder() {
+ dialogUtil.createInputDialog(
+ title = "Enter Folder Name To Create",
+ hint = "",
+ callback = object : DialogUtil.InputDialogCallback {
+ override fun onPositiveButtonClicked(input: String) {
+ if (input.trim().isNotEmpty()) {
+ try {
+ folderManager.createFolder(hiddenDir, input.trim())
+ refreshCurrentView()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error creating folder: ${e.message}")
Toast.makeText(
this@HiddenActivity,
- getString(R.string.there_was_a_problem_in_the_folder),
+ "Failed to create folder",
Toast.LENGTH_SHORT
).show()
- dismissCustomDialog()
}
-
}
- } else {
- Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
}
}
- }
- askPermissiom()
+ )
}
- private fun askPermissiom() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
- if (!Environment.isExternalStorageManager()){
- val intent = Intent().setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- startActivity(intent)
- }
- }
- else {
- checkAndRequestStoragePermission()
- }
+ override fun onResume() {
+ super.onResume()
+ setupFlagSecure()
}
- private fun checkAndRequestStoragePermission() {
- if (ContextCompat.checkSelfPermission(
- this,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED ||
- ContextCompat.checkSelfPermission(
- this,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(
- this,
- arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ),
- STORAGE_PERMISSION_CODE
+ private fun setupFlagSecure() {
+ val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
+ if (prefs.getBoolean("screenshot_restriction", true)) {
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
)
}
}
- private fun openFilePicker(mimeType: String) {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- type = mimeType
- addCategory(Intent.CATEGORY_OPENABLE)
- putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
- addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
- }
- pickImageLauncher.launch(intent)
- }
private fun listFoldersInHiddenDirectory() {
- if (hiddenDir.exists() && hiddenDir.isDirectory) {
- val folders = folderManager.getFoldersInDirectory(hiddenDir)
-
- if (folders.isNotEmpty()) {
- binding.noItems.visibility = View.GONE
- binding.recyclerView.visibility = View.VISIBLE
-
- // Initialize adapter only once
- if (folderAdapter == null) {
- binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
- folderAdapter = FolderAdapter(
- onFolderClick = { clickedFolder ->
- openFolder(clickedFolder)
- },
- onFolderLongClick = { folder ->
- // Enter selection mode
- binding.fabExpend.visibility = View.GONE
- binding.addFolder.visibility = View.GONE
- binding.deleteSelected.visibility = View.VISIBLE
- },
- onSelectionModeChanged = { isSelectionMode ->
- if (!isSelectionMode) {
- binding.deleteSelected.visibility = View.GONE
- binding.addFolder.visibility = View.VISIBLE
- }
- }
- )
- binding.recyclerView.adapter = folderAdapter
- }
-
- // Submit new list to adapter - DiffUtil will handle the comparison
- folderAdapter?.submitList(folders)
- } else {
- binding.noItems.visibility = View.VISIBLE
- binding.recyclerView.visibility = View.GONE
+ try {
+ if (!hiddenDir.exists()) {
+ fileManager.getHiddenDirectory()
}
- } else if (!hiddenDir.exists()) {
- fileManager.getHiddenDirectory()
- } else {
- Log.e("HiddenActivity", "Hidden directory is not a directory: ${hiddenDir.absolutePath}")
+
+ if (hiddenDir.exists() && hiddenDir.isDirectory) {
+ val folders = folderManager.getFoldersInDirectory(hiddenDir)
+
+ if (folders.isNotEmpty()) {
+ showFolderList(folders)
+ } else {
+ showEmptyState()
+ }
+ } else {
+ Log.e(TAG, "Hidden directory is not accessible: ${hiddenDir.absolutePath}")
+ showEmptyState()
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error listing folders: ${e.message}")
+ showEmptyState()
}
}
- private fun openFolder(folder: File) {
- Log.d("HiddenActivity", "Opening folder: ${folder.name}")
- currentFolder = folder
- binding.addFolder.visibility = View.GONE
- binding.fabExpend.visibility = View.VISIBLE
+ private fun showFolderList(folders: List) {
+ binding.noItems.visibility = View.GONE
+ binding.recyclerView.visibility = View.VISIBLE
- // Read files in the clicked folder and update RecyclerView
- val files = folderManager.getFilesInFolder(folder)
- Log.d("HiddenActivity", "Found ${files.size} files in ${folder.name}")
- binding.folderName.text = folder.name
+ // Clear the existing adapter to avoid conflicts
+ listFolderAdapter = null
- if (files.isNotEmpty()) {
- binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
+ binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
+ folderAdapter = FolderAdapter(
+ onFolderClick = { clickedFolder ->
+ startActivity(Intent(this,ViewFolderActivity::class.java).putExtra("folder",clickedFolder.toString()))
+ },
+ onFolderLongClick = {
+ enterFolderSelectionMode()
+ },
+ onSelectionModeChanged = { isSelectionMode ->
+ handleFolderSelectionModeChange(isSelectionMode)
+ },
+ onSelectionCountChanged = { selectedCount ->
+ updateEditButtonVisibility()
+ }
+ )
+ binding.recyclerView.adapter = folderAdapter
+ folderAdapter?.submitList(folders)
- val fileAdapter = FileAdapter(this, this, folder).apply {
- fileOperationCallback = object : FileAdapter.FileOperationCallback {
- override fun onFileDeleted(file: File) {
- // Refresh the file list
- refreshCurrentFolder()
+ // Ensure proper icon state for folder view
+ if (folderAdapter?.isInSelectionMode() != true) {
+ showFolderViewIcons()
+ }
+ }
+ private fun showFolderListStyle(folders: List) {
+ binding.noItems.visibility = View.GONE
+ binding.recyclerView.visibility = View.VISIBLE
+
+ // Clear the existing adapter to avoid conflicts
+ folderAdapter = null
+
+ binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
+ listFolderAdapter = ListFolderAdapter(
+ onFolderClick = { clickedFolder ->
+ startActivity(Intent(this,ViewFolderActivity::class.java).putExtra("folder",clickedFolder.toString()))
+ },
+ onFolderLongClick = {
+ enterFolderSelectionMode()
+ },
+ onSelectionModeChanged = { isSelectionMode ->
+ handleFolderSelectionModeChange(isSelectionMode)
+ },
+ onSelectionCountChanged = { selectedCount ->
+ updateEditButtonVisibility()
+ }
+ )
+ binding.recyclerView.adapter = listFolderAdapter
+ listFolderAdapter?.submitList(folders)
+
+ // Ensure proper icon state for folder view
+ if (listFolderAdapter?.isInSelectionMode() != true) {
+ showFolderViewIcons()
+ }
+ }
+
+ private fun updateEditButtonVisibility() {
+ val selectedCount = when {
+ folderAdapter != null -> folderAdapter?.getSelectedItems()?.size ?: 0
+ listFolderAdapter != null -> listFolderAdapter?.getSelectedItems()?.size ?: 0
+ else -> 0
+ }
+ binding.edit.visibility = if (selectedCount == 1) View.VISIBLE else View.GONE
+ }
+
+ private fun showEmptyState() {
+ binding.noItems.visibility = View.VISIBLE
+ binding.recyclerView.visibility = View.GONE
+ }
+
+ private fun enterFolderSelectionMode() {
+ showFolderSelectionIcons()
+ }
+
+
+ private fun refreshCurrentView() {
+ val isList = PrefsUtil(this).getBoolean("isList", false)
+ if (isList) {
+ binding.folderOrientation.setImageResource(R.drawable.ic_grid)
+ listFoldersInHiddenDirectoryListStyle()
+ } else {
+ binding.folderOrientation.setImageResource(R.drawable.ic_list)
+ listFoldersInHiddenDirectory()
+ }
+ }
+
+
+ private fun deleteSelectedItems() {
+ deleteSelectedFolders()
+ }
+
+ private fun deleteSelectedFolders() {
+ val selectedFolders = when {
+ folderAdapter != null -> folderAdapter?.getSelectedItems() ?: emptyList()
+ listFolderAdapter != null -> listFolderAdapter?.getSelectedItems() ?: emptyList()
+ else -> emptyList()
+ }
+
+ if (selectedFolders.isNotEmpty()) {
+ dialogUtil.showMaterialDialog(
+ getString(R.string.delete_items),
+ getString(R.string.are_you_sure_you_want_to_delete_selected_items),
+ getString(R.string.delete),
+ getString(R.string.cancel),
+ object : DialogUtil.DialogCallback {
+ override fun onPositiveButtonClicked() {
+ performFolderDeletion(selectedFolders)
}
- override fun onFileRenamed(oldFile: File, newFile: File) {
- // Refresh the file list
- refreshCurrentFolder()
+ override fun onNegativeButtonClicked() {
+ // Do nothing
}
- override fun onRefreshNeeded() {
- // Refresh the file list
- refreshCurrentFolder()
- }
-
- override fun onSelectionModeChanged(
- isSelectionMode: Boolean,
- selectedCount: Int
- ) {
-
- }
-
- override fun onSelectionCountChanged(selectedCount: Int) {
-
-
+ override fun onNaturalButtonClicked() {
+ // Do nothing
}
}
+ )
+ }
+ }
- submitList(files)
+ private fun performFolderDeletion(selectedFolders: List) {
+ var allDeleted = true
+ selectedFolders.forEach { folder ->
+ if (!folderManager.deleteFolder(folder)) {
+ allDeleted = false
+ Log.e(TAG, "Failed to delete folder: ${folder.name}")
}
+ }
- binding.recyclerView.adapter = fileAdapter
- binding.recyclerView.visibility = View.VISIBLE
- binding.noItems.visibility = View.GONE
+ val message = if (allDeleted) {
+ getString(R.string.folder_deleted_successfully)
} else {
- binding.recyclerView.visibility = View.GONE
- binding.noItems.visibility = View.VISIBLE
+ getString(R.string.some_items_could_not_be_deleted)
}
+
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+
+ // Clear selection from both adapters
+ folderAdapter?.clearSelection()
+ listFolderAdapter?.clearSelection()
+
+ // This will trigger the selection mode change callback and show proper icons
+ exitFolderSelectionMode()
+
+ // Refresh the current view based on orientation
+ refreshCurrentView()
}
- private fun refreshCurrentFolder() {
- currentFolder?.let { folder ->
- val files = folderManager.getFilesInFolder(folder)
- (binding.recyclerView.adapter as? FileAdapter)?.submitList(files)
+ private fun handleBackPress() {
- if (files.isEmpty()) {
- binding.recyclerView.visibility = View.GONE
- binding.noItems.visibility = View.VISIBLE
- } else {
- binding.recyclerView.visibility = View.VISIBLE
- binding.noItems.visibility = View.GONE
- }
+
+ // Check if folder adapters are in selection mode
+ if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
+ return
}
- }
- private fun openFabs() {
- binding.addImage.startAnimation(fabOpen)
- binding.addVideo.startAnimation(fabOpen)
- binding.addAudio.startAnimation(fabOpen)
- binding.addDocument.startAnimation(fabOpen)
- binding.addFolder.startAnimation(fabOpen)
- binding.fabExpend.startAnimation(rotateOpen)
-
- binding.addImage.visibility = View.VISIBLE
- binding.addVideo.visibility = View.VISIBLE
- binding.addAudio.visibility = View.VISIBLE
- binding.addDocument.visibility = View.VISIBLE
- binding.addFolder.visibility = View.VISIBLE
-
- isFabOpen = true
- Handler(Looper.getMainLooper()).postDelayed({
- binding.fabExpend.setImageResource(R.drawable.wrong)
- },200)
- }
-
- private fun closeFabs() {
- binding.addImage.startAnimation(fabClose)
- binding.addVideo.startAnimation(fabClose)
- binding.addAudio.startAnimation(fabClose)
- binding.addDocument.startAnimation(fabClose)
- binding.addFolder.startAnimation(fabClose)
- binding.fabExpend.startAnimation(rotateClose)
-
- binding.addImage.visibility = View.INVISIBLE
- binding.addVideo.visibility = View.INVISIBLE
- binding.addAudio.visibility = View.INVISIBLE
- binding.addDocument.visibility = View.INVISIBLE
- binding.addFolder.visibility = View.INVISIBLE
-
- isFabOpen = false
- binding.fabExpend.setImageResource(R.drawable.ic_add)
- }
-
-
- private fun getFileNameFromUri(uri: Uri): String? {
- var name: String? = null
- val cursor = contentResolver.query(uri, null, null, null, null)
- cursor?.use { it ->
- val nameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
- if (nameIndex != -1) {
- it.moveToFirst()
- name = it.getString(nameIndex)
- }
- }
- return name
- }
-
-
- private fun setupDeleteButton() {
- binding.deleteSelected.setOnClickListener {
- val selectedFolders = folderAdapter?.getSelectedItems() ?: emptyList()
- if (selectedFolders.isNotEmpty()) {
- dialogUtil.showMaterialDialog(
- getString(R.string.delete_items),
- getString(R.string.are_you_sure_you_want_to_delete_selected_items),
- getString(R.string.delete),
- getString(R.string.cancel),
- object : DialogUtil.DialogCallback {
- override fun onPositiveButtonClicked() {
- var allDeleted = true
- selectedFolders.forEach { folder ->
- if (!folderManager.deleteFolder(folder)) {
- allDeleted = false
- }
- }
- if (allDeleted) {
- Toast.makeText(this@HiddenActivity, getString(R.string.folder_deleted_successfully), Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(this@HiddenActivity, getString(R.string.some_items_could_not_be_deleted), Toast.LENGTH_SHORT).show()
- }
- folderAdapter?.clearSelection()
- binding.deleteSelected.visibility = View.GONE
- binding.addFolder.visibility = View.VISIBLE
- listFoldersInHiddenDirectory()
- }
-
- override fun onNegativeButtonClicked() {
- // Do nothing
- }
-
- override fun onNaturalButtonClicked() {
- // Do nothing
- }
- }
- )
- }
- }
- }
-
- private fun pressBack(){
- currentFolder = null
- if (isFabOpen) {
- closeFabs()
- }
- if (folderAdapter != null) {
- binding.recyclerView.adapter = folderAdapter
- }
- binding.folderName.text = getString(R.string.hidden_space)
- listFoldersInHiddenDirectory()
- binding.fabExpend.visibility = View.GONE
- binding.addImage.visibility = View.GONE
- binding.addVideo.visibility = View.GONE
- binding.addAudio.visibility = View.GONE
- binding.addDocument.visibility = View.GONE
- binding.addFolder.visibility = View.VISIBLE
- }
-
- @Deprecated("This method has been deprecated in favor of using the\n {@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}.\n The OnBackPressedDispatcher controls how back button events are dispatched\n to one or more {@link OnBackPressedCallback} objects.")
- override fun onBackPressed() {
+ // Handle navigation back
if (currentFolder != null) {
- pressBack()
+ navigateBackToFolders()
} else {
- super.onBackPressed()
+ finish()
+ }
+ }
+
+ private fun navigateBackToFolders() {
+ currentFolder = null
+
+ // Clean up file adapter
+
+ refreshCurrentView()
+
+ binding.folderName.text = getString(R.string.hidden_space)
+
+ // Set proper icons for folder view
+ showFolderViewIcons()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+
+ // Remove any pending callbacks
+ mainHandler.removeCallbacksAndMessages(null)
+ }
+
+ //visibility related code
+ private fun showFolderViewIcons() {
+ binding.folderOrientation.visibility = View.VISIBLE
+ binding.settings.visibility = View.VISIBLE
+ binding.delete.visibility = View.GONE
+ binding.deleteSelected.visibility = View.GONE
+ binding.menuButton.visibility = View.GONE
+ binding.addFolder.visibility = View.VISIBLE
+ binding.edit.visibility = View.GONE
+ // Ensure FABs are properly managed
+ if (currentFolder == null) {
+
+ binding.addFolder.visibility = View.VISIBLE
+ }
+ }
+ private fun showFolderSelectionIcons() {
+ binding.folderOrientation.visibility = View.GONE
+ binding.settings.visibility = View.GONE
+ binding.delete.visibility = View.VISIBLE
+ binding.deleteSelected.visibility = View.VISIBLE
+ binding.menuButton.visibility = View.GONE
+ binding.addFolder.visibility = View.GONE
+
+ // Update edit button visibility based on current selection count
+ updateEditButtonVisibility()
+ }
+ private fun exitFolderSelectionMode() {
+ showFolderViewIcons()
+ }
+
+ private fun handleFolderSelectionModeChange(isSelectionMode: Boolean) {
+ if (!isSelectionMode) {
+ exitFolderSelectionMode()
+ } else {
+ enterFolderSelectionMode()
+ }
+ // Always update edit button visibility when selection mode changes
+ updateEditButtonVisibility()
+ }
+
+ private fun editSelectedFolder() {
+ val selectedFolders = when {
+ folderAdapter != null -> folderAdapter?.getSelectedItems() ?: emptyList()
+ listFolderAdapter != null -> listFolderAdapter?.getSelectedItems() ?: emptyList()
+ else -> emptyList()
+ }
+
+ if (selectedFolders.size != 1) {
+ Toast.makeText(this, "Please select exactly one folder to edit", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val folder = selectedFolders[0]
+ showEditFolderDialog(folder)
+ }
+
+ private fun showEditFolderDialog(folder: File) {
+ val inputEditText = EditText(this).apply {
+ setText(folder.name)
+ selectAll()
+ }
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle("Rename Folder")
+ .setView(inputEditText)
+ .setPositiveButton("Rename") { dialog, _ ->
+ val newName = inputEditText.text.toString().trim()
+ if (newName.isNotEmpty() && newName != folder.name) {
+ if (isValidFolderName(newName)) {
+ renameFolder(folder, newName)
+ } else {
+ Toast.makeText(this, "Invalid folder name", Toast.LENGTH_SHORT).show()
+ }
+ }
+ dialog.dismiss()
+ }
+ .setNegativeButton("Cancel") { dialog, _ ->
+ dialog.cancel()
+ }
+ .show()
+ }
+
+ private fun isValidFolderName(folderName: String): Boolean {
+ val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
+ return folderName.isNotBlank() &&
+ folderName.none { it in forbiddenChars } &&
+ !folderName.startsWith(".") &&
+ folderName.length <= 255
+ }
+
+ private fun renameFolder(oldFolder: File, newName: String) {
+ val parentDir = oldFolder.parentFile
+ if (parentDir != null) {
+ val newFolder = File(parentDir, newName)
+ if (newFolder.exists()) {
+ Toast.makeText(this, "Folder with this name already exists", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ if (oldFolder.renameTo(newFolder)) {
+ // Clear selection from both adapters
+ folderAdapter?.clearSelection()
+ listFolderAdapter?.clearSelection()
+
+ // Exit selection mode
+ exitFolderSelectionMode()
+
+ refreshCurrentView()
+ } else {
+ Toast.makeText(this, "Failed to rename folder", Toast.LENGTH_SHORT).show()
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt b/app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt
deleted file mode 100644
index 96a2702..0000000
--- a/app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package devs.org.calculator.activities
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import devs.org.calculator.databinding.ActivityHiddenVaultBinding
-import devs.org.calculator.utils.FileManager
-
-class HiddenVaultActivity : AppCompatActivity() {
- private lateinit var binding: ActivityHiddenVaultBinding
- private lateinit var fileManager: FileManager
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityHiddenVaultBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- fileManager = FileManager(this, this)
- setupNavigation()
- }
-
- private fun setupNavigation() {
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt
index 7dd8ca8..8eb7809 100644
--- a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt
@@ -2,6 +2,7 @@ package devs.org.calculator.activities
import android.net.Uri
import android.os.Bundle
+import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
@@ -30,6 +31,7 @@ class PreviewActivity : AppCompatActivity() {
binding = ActivityPreviewBinding.inflate(layoutInflater)
setContentView(binding.root)
+
fileManager = FileManager(this, this)
currentPosition = intent.getIntExtra("position", 0)
@@ -55,6 +57,21 @@ class PreviewActivity : AppCompatActivity() {
}
+ override fun onResume() {
+ super.onResume()
+ setupFlagSecure()
+ }
+
+ private fun setupFlagSecure() {
+ val prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
+ if (prefs.getBoolean("screenshot_restriction", true)) {
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ }
+ }
+
private fun setupFileType() {
when (type) {
"IMAGE" -> {
diff --git a/app/src/main/java/devs/org/calculator/activities/SettingsActivity.kt b/app/src/main/java/devs/org/calculator/activities/SettingsActivity.kt
index c36a4a3..0dfb400 100644
--- a/app/src/main/java/devs/org/calculator/activities/SettingsActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/SettingsActivity.kt
@@ -1,20 +1,154 @@
package devs.org.calculator.activities
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
import devs.org.calculator.R
import devs.org.calculator.databinding.ActivitySettingsBinding
class SettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
+ private lateinit var prefs: SharedPreferences
+ private val DEV_GITHUB_URL = "https://github.com/binondi"
+ private val GITHUB_URL = "$DEV_GITHUB_URL/calculator-hide-files"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
+ prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
+ setupUI()
+ loadSettings()
+ setupListeners()
+ }
+
+ private fun setupUI() {
+ binding.back.setOnClickListener {
+ onBackPressed()
+ }
+ }
+
+ private fun loadSettings() {
+
+ binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
+
+ val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ binding.themeModeSwitch.isChecked = themeMode != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+
+ when (themeMode) {
+ AppCompatDelegate.MODE_NIGHT_YES -> binding.darkThemeRadio.isChecked = true
+ AppCompatDelegate.MODE_NIGHT_NO -> binding.lightThemeRadio.isChecked = true
+ else -> binding.systemThemeRadio.isChecked = true
+ }
+
+ binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
+ binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
+
+ updateThemeModeVisibility()
+ }
+
+ private fun setupListeners() {
+
+ binding.githubButton.setOnClickListener {
+ openUrl(GITHUB_URL)
+ }
+
+ binding.devGithubButton.setOnClickListener {
+ openUrl(DEV_GITHUB_URL)
+ }
+
+ binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
+ prefs.edit().putBoolean("dynamic_theme", isChecked).apply()
+ if (!isChecked) {
+ showThemeModeDialog()
+ }
+ }
+
+
+ binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
+ binding.themeRadioGroup.visibility = if (isChecked) View.VISIBLE else View.GONE
+ if (!isChecked) {
+ binding.systemThemeRadio.isChecked = true
+ applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ }
+ }
+
+ binding.themeRadioGroup.setOnCheckedChangeListener { _, checkedId ->
+ val themeMode = when (checkedId) {
+ R.id.lightThemeRadio -> AppCompatDelegate.MODE_NIGHT_NO
+ R.id.darkThemeRadio -> AppCompatDelegate.MODE_NIGHT_YES
+ else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ }
+ applyThemeMode(themeMode)
+ }
+
+ binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
+ prefs.edit().putBoolean("screenshot_restriction", isChecked).apply()
+ if (isChecked) {
+ enableScreenshotRestriction()
+ } else {
+ disableScreenshotRestriction()
+ }
+ }
+ binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
+ prefs.edit().putBoolean("showFileName", isChecked).apply()
+ }
+ }
+
+ private fun updateThemeModeVisibility() {
+ binding.themeRadioGroup.visibility = if (binding.themeModeSwitch.isChecked) View.VISIBLE else View.GONE
+ }
+
+ private fun showThemeModeDialog() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle("Theme Mode")
+ .setMessage("Would you like to set a specific theme mode?")
+ .setPositiveButton("Yes") { _, _ ->
+ binding.themeModeSwitch.isChecked = true
+ }
+ .setNegativeButton("No") { _, _ ->
+ binding.systemThemeRadio.isChecked = true
+ applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ }
+ .show()
+ }
+
+ private fun applyThemeMode(themeMode: Int) {
+ prefs.edit().putInt("theme_mode", themeMode).apply()
+ AppCompatDelegate.setDefaultNightMode(themeMode)
+ }
+
+ private fun enableScreenshotRestriction() {
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ }
+
+ private fun disableScreenshotRestriction() {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+
+ private fun openUrl(url: String) {
+ try {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(intent)
+ } catch (e: Exception) {
+ Snackbar.make(binding.root, "Could not open URL", Snackbar.LENGTH_SHORT).show()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/ViewFolderActivity.kt b/app/src/main/java/devs/org/calculator/activities/ViewFolderActivity.kt
new file mode 100644
index 0000000..03b6590
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/activities/ViewFolderActivity.kt
@@ -0,0 +1,596 @@
+package devs.org.calculator.activities
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Bundle
+import android.os.Environment
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import devs.org.calculator.R
+import devs.org.calculator.adapters.FileAdapter
+import devs.org.calculator.callbacks.FileProcessCallback
+import devs.org.calculator.databinding.ActivityViewFolderBinding
+import devs.org.calculator.databinding.ProccessingDialogBinding
+import devs.org.calculator.utils.DialogUtil
+import devs.org.calculator.utils.FileManager
+import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
+import devs.org.calculator.utils.FolderManager
+import devs.org.calculator.utils.PrefsUtil
+import kotlinx.coroutines.launch
+import java.io.File
+
+class ViewFolderActivity : AppCompatActivity() {
+
+ private var isFabOpen = false
+ private lateinit var fabOpen: Animation
+ private lateinit var fabClose: Animation
+ private lateinit var rotateOpen: Animation
+ private lateinit var rotateClose: Animation
+ private lateinit var binding: ActivityViewFolderBinding
+ private val mainHandler = Handler(Looper.getMainLooper())
+ private lateinit var fileManager: FileManager
+ private lateinit var folderManager: FolderManager
+ private lateinit var dialogUtil: DialogUtil
+ private var fileAdapter: FileAdapter? = null
+ private var currentFolder: File? = null
+ private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
+ private lateinit var pickImageLauncher: ActivityResultLauncher
+ private lateinit var prefs: SharedPreferences
+
+ private var customDialog: androidx.appcompat.app.AlertDialog? = null
+
+ private var dialogShowTime: Long = 0
+ private val MINIMUM_DIALOG_DURATION = 1700L
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityViewFolderBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setupAnimations()
+ initialize()
+ setupClickListeners()
+ closeFabs()
+ val folder = intent.getStringExtra("folder").toString()
+ currentFolder = File(folder)
+ if (currentFolder != null){
+ openFolder(currentFolder!!)
+ }else {
+ showEmptyState()
+ }
+
+ setupActivityResultLaunchers()
+ }
+
+ private fun initialize() {
+ fileManager = FileManager(this, this)
+ folderManager = FolderManager(this)
+ dialogUtil = DialogUtil(this)
+ prefs = getSharedPreferences("app_settings", MODE_PRIVATE)
+ }
+
+ private fun setupActivityResultLaunchers() {
+ pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ handleFilePickerResult(result.data)
+ }
+ }
+ }
+
+ private fun handleFilePickerResult(data: Intent?) {
+ val clipData = data?.clipData
+ val uriList = mutableListOf()
+
+ if (clipData != null) {
+ for (i in 0 until clipData.itemCount) {
+ val uri = clipData.getItemAt(i).uri
+ uriList.add(uri)
+ }
+ } else {
+ data?.data?.let { uriList.add(it) }
+ }
+
+ if (uriList.isNotEmpty()) {
+ processSelectedFiles(uriList)
+ } else {
+ Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun showCustomDialog(count: Int) {
+ val dialogView = ProccessingDialogBinding.inflate(layoutInflater)
+ customDialog = MaterialAlertDialogBuilder(this)
+ .setView(dialogView.root)
+ .setCancelable(false)
+ .create()
+ dialogView.title.text = "Hiding $count files"
+ customDialog?.show()
+ dialogShowTime = System.currentTimeMillis()
+ }
+ private fun dismissCustomDialog() {
+ val currentTime = System.currentTimeMillis()
+ val elapsedTime = currentTime - dialogShowTime
+
+ if (elapsedTime < MINIMUM_DIALOG_DURATION) {
+ val remainingTime = MINIMUM_DIALOG_DURATION - elapsedTime
+ mainHandler.postDelayed({
+ customDialog?.dismiss()
+ customDialog = null
+ }, remainingTime)
+ } else {
+ customDialog?.dismiss()
+ customDialog = null
+ }
+ }
+
+
+ private fun processSelectedFiles(uriList: List) {
+ val targetFolder = currentFolder ?: hiddenDir
+
+ showCustomDialog(uriList.size)
+ lifecycleScope.launch {
+ try {
+ fileManager.processMultipleFiles(uriList, targetFolder,
+ object : FileProcessCallback {
+ override fun onFilesProcessedSuccessfully(copiedFiles: List) {
+ mainHandler.post {
+ refreshCurrentView()
+ dismissCustomDialog()
+ }
+ }
+
+ override fun onFileProcessFailed() {
+ mainHandler.post {
+ Toast.makeText(
+ this@ViewFolderActivity,
+ getString(R.string.failed_to_hide_files),
+ Toast.LENGTH_SHORT
+ ).show()
+ dismissCustomDialog()
+ }
+ }
+ })
+ } catch (e: Exception) {
+ mainHandler.post {
+ Toast.makeText(
+ this@ViewFolderActivity,
+ getString(R.string.there_was_a_problem_in_the_folder),
+ Toast.LENGTH_SHORT
+ ).show()
+ dismissCustomDialog()
+ }
+ }
+ }
+ }
+
+ private fun refreshCurrentView() {
+ if (currentFolder != null) {
+ refreshCurrentFolder()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refreshCurrentFolder()
+ }
+
+ private fun openFolder(folder: File) {
+
+ val files = folderManager.getFilesInFolder(folder)
+ binding.folderName.text = folder.name
+
+ if (files.isNotEmpty()) {
+ showFileList(files, folder)
+ } else {
+ showEmptyState()
+ }
+ }
+
+ private fun showEmptyState() {
+ binding.noItems.visibility = View.VISIBLE
+ binding.recyclerView.visibility = View.GONE
+ }
+
+ private fun showFileList(files: List, folder: File) {
+ binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
+
+ // Clean up previous adapter
+ fileAdapter?.cleanup()
+
+ fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
+ onFolderLongClick = { isSelected ->
+ handleFileSelectionModeChange(isSelected, 0)
+ }).apply {
+ setFileOperationCallback(object : FileAdapter.FileOperationCallback {
+ override fun onFileDeleted(file: File) {
+ refreshCurrentFolder()
+ }
+
+ override fun onFileRenamed(oldFile: File, newFile: File) {
+ refreshCurrentFolder()
+ }
+
+ override fun onRefreshNeeded() {
+ refreshCurrentFolder()
+ }
+
+ override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
+ handleFileSelectionModeChange(isSelectionMode, selectedCount)
+ }
+
+ override fun onSelectionCountChanged(selectedCount: Int) {
+ updateSelectionCountDisplay(selectedCount)
+ }
+ })
+
+ submitList(files)
+ }
+
+ binding.recyclerView.adapter = fileAdapter
+ binding.recyclerView.visibility = View.VISIBLE
+ binding.noItems.visibility = View.GONE
+
+ // Setup menu button click listener
+ binding.menuButton.setOnClickListener {
+ fileAdapter?.let { adapter ->
+ showFileOptionsMenu(adapter.getSelectedItems())
+ }
+ }
+
+ // Set initial UI state
+ showFileViewIcons()
+ }
+
+
+ @SuppressLint("MissingSuperCall")
+ override fun onBackPressed() {
+ handleBackPress()
+ }
+
+ private fun handleBackPress() {
+ if (fileAdapter?.onBackPressed() == true) {
+ return
+ }
+
+ super.onBackPressed()
+ }
+
+ private fun handleFileSelectionModeChange(isSelectionMode: Boolean, selectedCount: Int) {
+ if (isSelectionMode) {
+ showFileSelectionIcons()
+ } else {
+ showFileViewIcons()
+ }
+ }
+
+ private fun updateSelectionCountDisplay(selectedCount: Int) {
+ // Update any UI elements that show selection count
+ if (selectedCount > 0) {
+ showFileSelectionIcons()
+ } else {
+ showFileViewIcons()
+ }
+ }
+
+ private fun showFileOptionsMenu(selectedFiles: List) {
+ if (selectedFiles.isEmpty()) return
+
+ val options = arrayOf(
+ getString(R.string.un_hide),
+ getString(R.string.delete),
+ getString(R.string.copy_to_another_folder),
+ getString(R.string.move_to_another_folder)
+ )
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.file_options))
+ .setItems(options) { _, which ->
+ when (which) {
+ 0 -> unhideSelectedFiles(selectedFiles)
+ 1 -> deleteSelectedFiles(selectedFiles)
+ 2 -> copyToAnotherFolder(selectedFiles)
+ 3 -> moveToAnotherFolder(selectedFiles)
+ }
+ }
+ .show()
+ }
+
+ private fun moveToAnotherFolder(selectedFiles: List) {
+ showFolderSelectionDialog { destinationFolder ->
+ moveFilesToFolder(selectedFiles, destinationFolder)
+ }
+ }
+
+
+ private fun unhideSelectedFiles(selectedFiles: List) {
+ dialogUtil.showMaterialDialog(
+ getString(R.string.un_hide_files),
+ getString(R.string.are_you_sure_you_want_to_un_hide_selected_files),
+ getString(R.string.un_hide),
+ getString(R.string.cancel),
+ object : DialogUtil.DialogCallback {
+ override fun onPositiveButtonClicked() {
+ performFileUnhiding(selectedFiles)
+ }
+
+ override fun onNegativeButtonClicked() {
+ // Do nothing
+ }
+
+ override fun onNaturalButtonClicked() {
+ // Do nothing
+ }
+ }
+ )
+ }
+
+
+
+ private fun deleteSelectedFiles(selectedFiles: List) {
+ dialogUtil.showMaterialDialog(
+ getString(R.string.delete_items),
+ getString(R.string.are_you_sure_you_want_to_delete_selected_items),
+ getString(R.string.delete),
+ getString(R.string.cancel),
+ object : DialogUtil.DialogCallback {
+ override fun onPositiveButtonClicked() {
+ performFileDeletion(selectedFiles)
+ }
+
+ override fun onNegativeButtonClicked() {
+ // Do nothing
+ }
+
+ override fun onNaturalButtonClicked() {
+ // Do nothing
+ }
+ }
+ )
+ }
+
+
+
+ private fun refreshCurrentFolder() {
+ currentFolder?.let { folder ->
+ val files = folderManager.getFilesInFolder(folder)
+ fileAdapter?.submitList(files)
+
+ if (files.isEmpty()) {
+ showEmptyState()
+ } else {
+ binding.recyclerView.visibility = View.VISIBLE
+ binding.noItems.visibility = View.GONE
+ fileAdapter?.let { adapter ->
+ if (adapter.isInSelectionMode()) {
+ showFileSelectionIcons()
+ } else {
+ showFileViewIcons()
+ }
+ }
+ }
+ }
+ }
+ private fun setupClickListeners() {
+ binding.fabExpend.setOnClickListener {
+ if (isFabOpen) closeFabs()
+ else openFabs()
+ }
+ binding.back.setOnClickListener {
+ finish()
+ }
+
+ binding.addImage.setOnClickListener { openFilePicker("image/*") }
+ binding.addVideo.setOnClickListener { openFilePicker("video/*") }
+ binding.addAudio.setOnClickListener { openFilePicker("audio/*") }
+ binding.addDocument.setOnClickListener { openFilePicker("*/*") }
+ }
+
+ private fun setupAnimations() {
+ fabOpen = AnimationUtils.loadAnimation(this, R.anim.fab_open)
+ fabClose = AnimationUtils.loadAnimation(this, R.anim.fab_close)
+ rotateOpen = AnimationUtils.loadAnimation(this, R.anim.rotate_open)
+ rotateClose = AnimationUtils.loadAnimation(this, R.anim.rotate_close)
+ }
+
+ private fun openFilePicker(mimeType: String) {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ type = mimeType
+ addCategory(Intent.CATEGORY_OPENABLE)
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ }
+ pickImageLauncher.launch(intent)
+ }
+
+ private fun openFabs() {
+ if (!isFabOpen) {
+ binding.addImage.startAnimation(fabOpen)
+ binding.addVideo.startAnimation(fabOpen)
+ binding.addAudio.startAnimation(fabOpen)
+ binding.addDocument.startAnimation(fabOpen)
+ binding.fabExpend.startAnimation(rotateOpen)
+
+ binding.addImage.visibility = View.VISIBLE
+ binding.addVideo.visibility = View.VISIBLE
+ binding.addAudio.visibility = View.VISIBLE
+ binding.addDocument.visibility = View.VISIBLE
+
+ isFabOpen = true
+ mainHandler.postDelayed({
+ binding.fabExpend.setImageResource(R.drawable.wrong)
+ }, 200)
+ }
+ }
+
+ private fun closeFabs() {
+ if (isFabOpen) {
+ binding.addImage.startAnimation(fabClose)
+ binding.addVideo.startAnimation(fabClose)
+ binding.addAudio.startAnimation(fabClose)
+ binding.addDocument.startAnimation(fabClose)
+ binding.fabExpend.startAnimation(rotateClose)
+
+ binding.addImage.visibility = View.INVISIBLE
+ binding.addVideo.visibility = View.INVISIBLE
+ binding.addAudio.visibility = View.INVISIBLE
+ binding.addDocument.visibility = View.INVISIBLE
+
+ isFabOpen = false
+ binding.fabExpend.setImageResource(R.drawable.ic_add)
+ }
+ }
+
+ private fun showFileViewIcons() {
+ binding.menuButton.visibility = View.GONE
+ binding.fabExpend.visibility = View.VISIBLE
+ binding.addImage.visibility = View.INVISIBLE
+ binding.addVideo.visibility = View.INVISIBLE
+ binding.addAudio.visibility = View.INVISIBLE
+ binding.addDocument.visibility = View.INVISIBLE
+ isFabOpen = false
+ binding.fabExpend.setImageResource(R.drawable.ic_add)
+ }
+
+ private fun showFileSelectionIcons() {
+ binding.menuButton.visibility = View.VISIBLE
+ binding.fabExpend.visibility = View.GONE
+ binding.addImage.visibility = View.INVISIBLE
+ binding.addVideo.visibility = View.INVISIBLE
+ binding.addAudio.visibility = View.INVISIBLE
+ binding.addDocument.visibility = View.INVISIBLE
+ isFabOpen = false
+ }
+
+ private fun performFileUnhiding(selectedFiles: List) {
+ lifecycleScope.launch {
+ var allUnhidden = true
+ selectedFiles.forEach { file ->
+ try {
+ val fileUri = Uri.fromFile(file)
+ val result = fileManager.copyFileToNormalDir(fileUri)
+ if (result == null) {
+ allUnhidden = false
+ } else {
+ // Delete the hidden file after successful copy
+ file.delete()
+ }
+ } catch (e: Exception) {
+ allUnhidden = false
+ }
+ }
+
+ mainHandler.post {
+ val message = if (allUnhidden) {
+ getString(R.string.files_unhidden_successfully)
+ } else {
+ getString(R.string.some_files_could_not_be_unhidden)
+ }
+
+ Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
+
+ // Fixed: Ensure proper order of operations
+ fileAdapter?.exitSelectionMode()
+ refreshCurrentFolder()
+ }
+ }
+ }
+
+ private fun performFileDeletion(selectedFiles: List) {
+ var allDeleted = true
+ selectedFiles.forEach { file ->
+ if (!file.delete()) {
+ allDeleted = false
+ }
+ }
+
+ 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) {
+ showFolderSelectionDialog { destinationFolder ->
+ copyFilesToFolder(selectedFiles, destinationFolder)
+ }
+ }
+
+ private fun copyFilesToFolder(selectedFiles: List, destinationFolder: File) {
+ var allCopied = true
+ selectedFiles.forEach { file ->
+ try {
+ val newFile = File(destinationFolder, file.name)
+ file.copyTo(newFile, overwrite = true)
+ } catch (e: Exception) {
+ allCopied = false
+ }
+ }
+
+ 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, destinationFolder: File) {
+ var allMoved = true
+ selectedFiles.forEach { file ->
+ try {
+ val newFile = File(destinationFolder, file.name)
+ file.copyTo(newFile, overwrite = true)
+ file.delete()
+ } catch (e: Exception) {
+ allMoved = false
+ }
+ }
+
+ 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) {
+ val folders = folderManager.getFoldersInDirectory(hiddenDir)
+ .filter { it != currentFolder } // Exclude current folder
+
+ if (folders.isEmpty()) {
+ Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val folderNames = folders.map { it.name }.toTypedArray()
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.select_destination_folder))
+ .setItems(folderNames) { _, which ->
+ onFolderSelected(folders[which])
+ }
+ .show()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt
index 7743f28..0d3c2a5 100644
--- a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt
+++ b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt
@@ -1,9 +1,10 @@
package devs.org.calculator.adapters
-import android.app.AlertDialog
import android.content.Context
import android.content.Intent
-import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -16,21 +17,37 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
import devs.org.calculator.activities.PreviewActivity
import devs.org.calculator.utils.FileManager
import java.io.File
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executors
class FileAdapter(
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
- private val currentFolder: File
+ private val currentFolder: File,
+ private val showFileName: Boolean,
+ private val onFolderLongClick: (Boolean) -> Unit
) : ListAdapter(FileDiffCallback()) {
private val selectedItems = mutableSetOf()
private var isSelectionMode = false
+ // Use WeakReference to prevent memory leaks
+ private var fileOperationCallback: WeakReference? = null
+
+ // Background executor for file operations
+ private val fileExecutor = Executors.newSingleThreadExecutor()
+ private val mainHandler = Handler(Looper.getMainLooper())
+
+ companion object {
+ private const val TAG = "FileAdapter"
+ }
+
// Callback interface for handling file operations and selection changes
interface FileOperationCallback {
fun onFileDeleted(file: File)
@@ -40,23 +57,29 @@ class FileAdapter(
fun onSelectionCountChanged(selectedCount: Int)
}
- var fileOperationCallback: FileOperationCallback? = null
+ fun setFileOperationCallback(callback: FileOperationCallback?) {
+ fileOperationCallback = callback?.let { WeakReference(it) }
+ }
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
- val selectionOverlay: View? = view.findViewById(R.id.selectedLayer) // Optional overlay for selection
- val checkIcon: ImageView? = view.findViewById(R.id.selected) // Optional check icon
+ val selectedLayer: View = view.findViewById(R.id.selectedLayer)
+ val selected: ImageView = view.findViewById(R.id.selected)
fun bind(file: File) {
val fileType = FileManager(context, lifecycleOwner).getFileType(file)
setupFileDisplay(file, fileType)
setupClickListeners(file, fileType)
+ fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
- // Handle selection state
- val isSelected = selectedItems.contains(adapterPosition)
- updateSelectionUI(isSelected)
+ // Update selection state
+ val position = adapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ val isSelected = selectedItems.contains(position)
+ updateSelectionUI(isSelected)
+ }
}
fun bind(file: File, payloads: List) {
@@ -76,101 +99,88 @@ class FileAdapter(
// Could update file info if displayed
}
"SELECTION_CHANGED" -> {
- val isSelected = selectedItems.contains(adapterPosition)
- updateSelectionUI(isSelected)
+ val position = adapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ val isSelected = selectedItems.contains(position)
+ updateSelectionUI(isSelected)
+ // Notify activity about selection change
+ notifySelectionModeChange()
+ }
}
}
}
}
private fun updateSelectionUI(isSelected: Boolean) {
- // Update visual selection state
- itemView.isSelected = isSelected
-
- // If you have a selection overlay, show/hide it
- selectionOverlay?.visibility = if (isSelected) View.VISIBLE else View.GONE
-
- // If you have a check icon, show/hide it
- checkIcon?.visibility = if (isSelected) View.VISIBLE else View.GONE
-
- // You can also change the background or add other visual indicators
- itemView.alpha = if (isSelectionMode && !isSelected) 0.7f else 1.0f
+ selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
+ selected.visibility = if (isSelected) View.VISIBLE else View.GONE
}
private fun setupFileDisplay(file: File, fileType: FileManager.FileType) {
+ fileNameTextView.text = file.name
+
when (fileType) {
FileManager.FileType.IMAGE -> {
- loadImageThumbnail(file)
- fileNameTextView.visibility = View.GONE
playIcon.visibility = View.GONE
+ Glide.with(context)
+ .load(file)
+ .centerCrop()
+ .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
+ .error(R.drawable.ic_document)
+ .placeholder(R.drawable.ic_document)
+ .into(imageView)
}
FileManager.FileType.VIDEO -> {
- loadVideoThumbnail(file)
- fileNameTextView.visibility = View.GONE
playIcon.visibility = View.VISIBLE
+ Glide.with(context)
+ .load(file)
+ .centerCrop()
+ .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
+ .error(R.drawable.ic_document)
+ .placeholder(R.drawable.ic_document)
+ .into(imageView)
+ }
+ FileManager.FileType.AUDIO -> {
+ playIcon.visibility = View.GONE
+ imageView.setImageResource(R.drawable.ic_audio)
}
else -> {
- loadFileIcon(fileType)
- fileNameTextView.visibility = View.VISIBLE
playIcon.visibility = View.GONE
+ imageView.setImageResource(R.drawable.ic_document)
}
}
- fileNameTextView.text = file.name
- }
-
- private fun loadImageThumbnail(file: File) {
- Glide.with(imageView)
- .load(file)
- .thumbnail(0.1f)
- .centerCrop()
- .override(300, 300)
- .placeholder(R.drawable.ic_file)
- .error(R.drawable.ic_file)
- .into(imageView)
- }
-
- private fun loadVideoThumbnail(file: File) {
- Glide.with(imageView)
- .asBitmap()
- .load(file)
- .thumbnail(0.1f)
- .centerCrop()
- .override(300, 300)
- .placeholder(R.drawable.ic_file)
- .error(R.drawable.ic_file)
- .into(imageView)
- }
-
- private fun loadFileIcon(fileType: FileManager.FileType) {
- val resourceId = when (fileType) {
- FileManager.FileType.AUDIO -> R.drawable.ic_audio
- FileManager.FileType.DOCUMENT -> R.drawable.ic_document
- else -> R.drawable.ic_file
- }
- imageView.setImageResource(resourceId)
}
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
itemView.setOnClickListener {
+ val position = adapterPosition
+ if (position == RecyclerView.NO_POSITION) return@setOnClickListener
+
if (isSelectionMode) {
- toggleSelection(adapterPosition)
- return@setOnClickListener
+ toggleSelection(position)
+ } else {
+ openFile(file, fileType)
}
- openFile(file, fileType)
}
itemView.setOnLongClickListener {
+ val position = adapterPosition
+ if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
+
if (!isSelectionMode) {
- showFileOptionsDialog(file)
- true
- } else {
- toggleSelection(adapterPosition)
- true
+ enterSelectionMode()
+ toggleSelection(position)
}
+ true
}
}
private fun openFile(file: File, fileType: FileManager.FileType) {
+ if (!file.exists()) {
+ Toast.makeText(context, "File no longer exists", Toast.LENGTH_SHORT).show()
+ return
+ }
+
when (fileType) {
FileManager.FileType.AUDIO -> openAudioFile(file)
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> openInPreview(fileType)
@@ -180,19 +190,20 @@ class FileAdapter(
}
private fun openAudioFile(file: File) {
- val uri = FileProvider.getUriForFile(
- context,
- "${context.packageName}.fileprovider",
- file
- )
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(uri, "audio/*")
- putExtra("folder", currentFolder.toString())
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
try {
+ val uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileprovider",
+ file
+ )
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "audio/*")
+ putExtra("folder", currentFolder.toString())
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
context.startActivity(intent)
} catch (e: Exception) {
+ Log.e(TAG, "Failed to open audio file: ${e.message}")
Toast.makeText(
context,
context.getString(R.string.no_audio_player_found),
@@ -202,19 +213,20 @@ class FileAdapter(
}
private fun openDocumentFile(file: File) {
- val uri = FileProvider.getUriForFile(
- context,
- "${context.packageName}.fileprovider",
- file
- )
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(uri, "*/*")
- putExtra("folder", currentFolder.toString())
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
try {
+ val uri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.fileprovider",
+ file
+ )
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "*/*")
+ putExtra("folder", currentFolder.toString())
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
context.startActivity(intent)
} catch (e: Exception) {
+ Log.e(TAG, "Failed to open document file: ${e.message}")
Toast.makeText(
context,
context.getString(R.string.no_suitable_app_found_to_open_this_document),
@@ -239,23 +251,41 @@ class FileAdapter(
}
private fun showFileOptionsDialog(file: File) {
- val options = 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)
- )
+ 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 ->
- when (which) {
- 0 -> unHideFile(file)
- 1 -> enableSelectMultipleFiles()
- 2 -> renameFile(file)
- 3 -> deleteFile(file)
- 4 -> shareFile(file)
+ 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()
}
@@ -264,18 +294,19 @@ class FileAdapter(
}
private fun enableSelectMultipleFiles() {
- // Enable multiple selection mode and select current item
+ val position = adapterPosition
+ if (position == RecyclerView.NO_POSITION) return
+
enterSelectionMode()
- selectedItems.add(adapterPosition)
- notifyItemChanged(adapterPosition, listOf("SELECTION_CHANGED"))
- fileOperationCallback?.onSelectionCountChanged(selectedItems.size)
+ selectedItems.add(position)
+ notifyItemChanged(position, listOf("SELECTION_CHANGED"))
}
private fun unHideFile(file: File) {
FileManager(context, lifecycleOwner).unHideFile(
file = file,
onSuccess = {
- fileOperationCallback?.onFileDeleted(file)
+ fileOperationCallback?.get()?.onFileDeleted(file)
},
onError = { errorMessage ->
Toast.makeText(context, "Failed to unhide: $errorMessage", Toast.LENGTH_SHORT).show()
@@ -284,11 +315,34 @@ class FileAdapter(
}
private fun deleteFile(file: File) {
- if (file.delete()) {
- fileOperationCallback?.onFileDeleted(file)
- Toast.makeText(context, "File deleted", Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show()
+ // 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()
+ }
+ }
}
}
@@ -304,15 +358,10 @@ class FileAdapter(
.setPositiveButton(context.getString(R.string.rename)) { dialog, _ ->
val newName = inputEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != file.name) {
- val parentDir = file.parentFile
- if (parentDir != null) {
- val newFile = File(parentDir, newName)
- if (file.renameTo(newFile)) {
- fileOperationCallback?.onFileRenamed(file, newFile)
- Toast.makeText(context, "File renamed", Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, "Failed to rename file", Toast.LENGTH_SHORT).show()
- }
+ if (isValidFileName(newName)) {
+ renameFileAsync(file, newName)
+ } else {
+ Toast.makeText(context, "Invalid file name", Toast.LENGTH_SHORT).show()
}
}
dialog.dismiss()
@@ -324,37 +373,87 @@ class FileAdapter(
.show()
}
- private fun shareFile(file: File) {
- 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)
+ private fun isValidFileName(fileName: String): Boolean {
+ val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
+ return fileName.isNotBlank() &&
+ fileName.none { it in forbiddenChars } &&
+ !fileName.startsWith(".") &&
+ fileName.length <= 255
+ }
+
+ private fun renameFileAsync(file: File, newName: String) {
+ fileExecutor.execute {
+ val parentDir = file.parentFile
+ if (parentDir != null) {
+ val newFile = File(parentDir, newName)
+ if (newFile.exists()) {
+ mainHandler.post {
+ Toast.makeText(context, "File with this name already exists", Toast.LENGTH_SHORT).show()
+ }
+ return@execute
+ }
+
+ val success = try {
+ file.renameTo(newFile)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to rename file: ${e.message}")
+ false
+ }
+
+ mainHandler.post {
+ if (success) {
+ fileOperationCallback?.get()?.onFileRenamed(file, newFile)
+ Toast.makeText(context, "File renamed", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(context, "Failed to rename file", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
}
- context.startActivity(
- Intent.createChooser(shareIntent, context.getString(R.string.share_file))
- )
+ }
+
+ 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) {
if (selectedItems.contains(position)) {
selectedItems.remove(position)
+ if (selectedItems.isEmpty()) {
+ exitSelectionMode()
+ }
} else {
selectedItems.add(position)
}
-
- // Exit selection mode if no items are selected
- if (selectedItems.isEmpty()) {
- exitSelectionMode()
- } else {
- fileOperationCallback?.onSelectionCountChanged(selectedItems.size)
- }
-
- notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ onSelectionCountChanged(selectedItems.size)
+ notifyItemChanged(position)
}
}
@@ -365,29 +464,30 @@ class FileAdapter(
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
- val file = getItem(position)
- holder.bind(file)
+ if (position < itemCount) {
+ val file = getItem(position)
+ holder.bind(file)
+ }
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int, payloads: MutableList) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
} else {
- val file = getItem(position)
- holder.bind(file, payloads)
+ if (position < itemCount) {
+ val file = getItem(position)
+ holder.bind(file, payloads)
+ }
}
}
- // Public methods for external control
-
/**
- * Enter selection mode
+ * Enter selection mode and notify callback immediately
*/
fun enterSelectionMode() {
if (!isSelectionMode) {
isSelectionMode = true
- fileOperationCallback?.onSelectionModeChanged(true, selectedItems.size)
- notifyDataSetChanged() // Refresh all items to show selection UI
+ notifySelectionModeChange()
}
}
@@ -398,8 +498,8 @@ class FileAdapter(
if (isSelectionMode) {
isSelectionMode = false
selectedItems.clear()
- fileOperationCallback?.onSelectionModeChanged(false, 0)
- notifyDataSetChanged() // Refresh all items to hide selection UI
+ notifySelectionModeChange()
+ notifyDataSetChanged()
}
}
@@ -410,11 +510,11 @@ class FileAdapter(
if (selectedItems.isNotEmpty()) {
val previouslySelected = selectedItems.toSet()
selectedItems.clear()
- fileOperationCallback?.onSelectionCountChanged(0)
-
- // Only update previously selected items
+ fileOperationCallback?.get()?.onSelectionCountChanged(0)
previouslySelected.forEach { position ->
- notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ if (position < itemCount) {
+ notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ }
}
}
}
@@ -435,16 +535,33 @@ class FileAdapter(
selectedItems.add(i)
}
- fileOperationCallback?.onSelectionCountChanged(selectedItems.size)
+ // Notify callback about selection change
+ fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
- // Update UI for changed items
- val allPositions = (0 until itemCount).toSet()
- val changedPositions = allPositions - previouslySelected + previouslySelected - allPositions
- changedPositions.forEach { position ->
- notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ // Update UI for changed items efficiently
+ updateSelectionItems(selectedItems.toSet(), previouslySelected)
+ }
+
+ /**
+ * Efficiently update selection UI for changed items only
+ */
+ private fun updateSelectionItems(newSelections: Set, oldSelections: Set) {
+ val changedItems = (oldSelections - newSelections) + (newSelections - oldSelections)
+ changedItems.forEach { position ->
+ if (position < itemCount) {
+ notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ }
}
}
+ /**
+ * Centralized method to notify selection mode changes
+ */
+ private fun notifySelectionModeChange() {
+ fileOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
+ onFolderLongClick(isSelectionMode)
+ }
+
/**
* Get selected files
*/
@@ -465,34 +582,67 @@ class FileAdapter(
fun isInSelectionMode(): Boolean = isSelectionMode
/**
- * Delete selected files
+ * Delete selected files with proper error handling and background processing
*/
fun deleteSelectedFiles() {
val selectedFiles = getSelectedItems()
- var deletedCount = 0
- var failedCount = 0
+ if (selectedFiles.isEmpty()) return
- selectedFiles.forEach { file ->
- if (file.delete()) {
- deletedCount++
- fileOperationCallback?.onFileDeleted(file)
- } else {
- failedCount++
+ // 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()
+ }
- exitSelectionMode()
+ private fun deleteFilesAsync(selectedFiles: List) {
+ fileExecutor.execute {
+ var deletedCount = 0
+ var failedCount = 0
+ val failedFiles = mutableListOf()
- // Show result message
- when {
- deletedCount > 0 && failedCount == 0 -> {
- Toast.makeText(context, "Deleted $deletedCount file(s)", Toast.LENGTH_SHORT).show()
+ 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}")
+ }
}
- 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()
+
+ 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()
+ }
+ }
}
}
}
@@ -504,39 +654,54 @@ class FileAdapter(
val selectedFiles = getSelectedItems()
if (selectedFiles.isEmpty()) return
- 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.map { file ->
- FileProvider.getUriForFile(
+ 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()
+ }
}
- 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")
- )
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to share files: ${e.message}")
+ Toast.makeText(context, "Failed to share files", Toast.LENGTH_SHORT).show()
}
exitSelectionMode()
@@ -554,4 +719,48 @@ class FileAdapter(
false
}
}
+
+ /**
+ * Force refresh of all selection states
+ * Call this if you notice selection UI issues
+ */
+ fun refreshSelectionStates() {
+ if (isSelectionMode) {
+ selectedItems.forEach { position ->
+ if (position < itemCount) {
+ notifyItemChanged(position, listOf("SELECTION_CHANGED"))
+ }
+ }
+ // Ensure callback is notified
+ notifySelectionModeChange()
+ }
+ }
+
+ /**
+ * Clean up resources to prevent memory leaks
+ */
+ fun cleanup() {
+ try {
+ if (!fileExecutor.isShutdown) {
+ fileExecutor.shutdown()
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error shutting down executor: ${e.message}")
+ }
+
+ fileOperationCallback?.clear()
+ fileOperationCallback = null
+ }
+
+ /**
+ * Call this from the activity's onDestroy()
+ */
+ override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView)
+ cleanup()
+ }
+
+ private fun onSelectionCountChanged(count: Int) {
+ fileOperationCallback?.get()?.onSelectionCountChanged(count)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt b/app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt
index da5c9db..424a8c9 100644
--- a/app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt
+++ b/app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt
@@ -14,7 +14,8 @@ import java.io.File
class FolderAdapter(
private val onFolderClick: (File) -> Unit,
private val onFolderLongClick: (File) -> Unit,
- private val onSelectionModeChanged: (Boolean) -> Unit
+ private val onSelectionModeChanged: (Boolean) -> Unit,
+ private val onSelectionCountChanged: (Int) -> Unit
) : ListAdapter(FolderDiffCallback()) {
private val selectedItems = mutableSetOf()
@@ -41,8 +42,7 @@ class FolderAdapter(
itemView.setOnLongClickListener {
if (!isSelectionMode) {
- isSelectionMode = true
- onSelectionModeChanged(true)
+ enterSelectionMode()
onFolderLongClick(folder)
toggleSelection(adapterPosition)
}
@@ -54,12 +54,12 @@ class FolderAdapter(
if (selectedItems.contains(position)) {
selectedItems.remove(position)
if (selectedItems.isEmpty()) {
- isSelectionMode = false
- onSelectionModeChanged(false)
+ exitSelectionMode()
}
} else {
selectedItems.add(position)
}
+ onSelectionCountChanged(selectedItems.size)
notifyItemChanged(position)
}
}
@@ -81,10 +81,36 @@ class FolderAdapter(
}
fun clearSelection() {
+ val wasInSelectionMode = isSelectionMode
selectedItems.clear()
+ if (wasInSelectionMode) {
+ exitSelectionMode()
+ }
+ onSelectionCountChanged(0)
+ notifyDataSetChanged()
+ }
+
+ fun onBackPressed(): Boolean {
+ return if (isInSelectionMode()) {
+ clearSelection()
+ true
+ } else {
+ false
+ }
+ }
+
+ fun isInSelectionMode(): Boolean {
+ return isSelectionMode
+ }
+
+ private fun enterSelectionMode() {
+ isSelectionMode = true
+ onSelectionModeChanged(true)
+ }
+
+ private fun exitSelectionMode() {
isSelectionMode = false
onSelectionModeChanged(false)
- notifyDataSetChanged()
}
private class FolderDiffCallback : DiffUtil.ItemCallback() {
diff --git a/app/src/main/java/devs/org/calculator/adapters/ListFolderAdapter.kt b/app/src/main/java/devs/org/calculator/adapters/ListFolderAdapter.kt
new file mode 100644
index 0000000..c670ebd
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/adapters/ListFolderAdapter.kt
@@ -0,0 +1,124 @@
+package devs.org.calculator.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import devs.org.calculator.R
+import java.io.File
+
+class ListFolderAdapter(
+ private val onFolderClick: (File) -> Unit,
+ private val onFolderLongClick: (File) -> Unit,
+ private val onSelectionModeChanged: (Boolean) -> Unit,
+ private val onSelectionCountChanged: (Int) -> Unit
+) : ListAdapter(FolderDiffCallback()) {
+
+ private val selectedItems = mutableSetOf()
+ private var isSelectionMode = false
+
+ inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
+
+ val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
+
+ fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
+ folderNameTextView.text = folder.name
+
+ selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
+
+ itemView.setOnClickListener {
+ if (isSelectionMode) {
+ toggleSelection(adapterPosition)
+ } else {
+ onFolderClick(folder)
+ }
+ }
+
+ itemView.setOnLongClickListener {
+ if (!isSelectionMode) {
+ enterSelectionMode()
+ onFolderLongClick(folder)
+ toggleSelection(adapterPosition)
+ }
+ true
+ }
+ }
+
+ private fun toggleSelection(position: Int) {
+ if (selectedItems.contains(position)) {
+ selectedItems.remove(position)
+ if (selectedItems.isEmpty()) {
+ exitSelectionMode()
+ }
+ } else {
+ selectedItems.add(position)
+ }
+ onSelectionCountChanged(selectedItems.size)
+ notifyItemChanged(position)
+ }
+ }
+
+ private fun enterSelectionMode() {
+ isSelectionMode = true
+ onSelectionModeChanged(true)
+ }
+
+ private fun exitSelectionMode() {
+ isSelectionMode = false
+ onSelectionModeChanged(false)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.item_folder_list_style, parent, false)
+ return FolderViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
+ val folder = getItem(position)
+ holder.bind(folder, onFolderClick, onFolderLongClick, selectedItems.contains(position))
+ }
+
+ fun getSelectedItems(): List {
+ return selectedItems.mapNotNull { position ->
+ if (position < itemCount) getItem(position) else null
+ }
+ }
+
+ fun clearSelection() {
+ val wasInSelectionMode = isSelectionMode
+ selectedItems.clear()
+ if (wasInSelectionMode) {
+ exitSelectionMode()
+ }
+ onSelectionCountChanged(0)
+ notifyDataSetChanged()
+ }
+
+ fun onBackPressed(): Boolean {
+ return if (isInSelectionMode()) {
+ clearSelection()
+ true
+ } else {
+ false
+ }
+ }
+
+ fun isInSelectionMode(): Boolean {
+ return isSelectionMode
+ }
+
+ private class FolderDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
+ return oldItem.absolutePath == newItem.absolutePath
+ }
+
+ override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
+ return oldItem.name == newItem.name
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/utils/PrefsUtil.kt b/app/src/main/java/devs/org/calculator/utils/PrefsUtil.kt
index b2dfc0b..6c52a81 100644
--- a/app/src/main/java/devs/org/calculator/utils/PrefsUtil.kt
+++ b/app/src/main/java/devs/org/calculator/utils/PrefsUtil.kt
@@ -18,6 +18,15 @@ class PrefsUtil(context: Context) {
.apply()
}
+ fun setBoolean(key:String, value: Boolean){
+ return prefs.edit().putBoolean(key,value).apply()
+
+ }
+
+ fun getBoolean(key: String, defValue: Boolean = false): Boolean{
+ return prefs.getBoolean(key,defValue)
+ }
+
fun resetPassword(){
prefs.edit()
.remove("password")
diff --git a/app/src/main/res/drawable/bottom_corner.xml b/app/src/main/res/drawable/bottom_corner.xml
new file mode 100644
index 0000000..7133542
--- /dev/null
+++ b/app/src/main/res/drawable/bottom_corner.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
index 86e0ece..411311a 100644
--- a/app/src/main/res/drawable/ic_back.xml
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -5,8 +5,8 @@
android:viewportHeight="1024">
+ android:fillColor="@color/svgTintColor"/>
+ android:fillColor="@color/svgTintColor"/>
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
index f25a97f..5ebddca 100644
--- a/app/src/main/res/drawable/ic_delete.xml
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -5,6 +5,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 0000000..ef58b5f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_folder_yellow.xml b/app/src/main/res/drawable/ic_folder_yellow.xml
new file mode 100644
index 0000000..ee9af53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder_yellow.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml
new file mode 100644
index 0000000..75ceb33
--- /dev/null
+++ b/app/src/main/res/drawable/ic_github.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_grid.xml b/app/src/main/res/drawable/ic_grid.xml
new file mode 100644
index 0000000..b838800
--- /dev/null
+++ b/app/src/main/res/drawable/ic_grid.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml
new file mode 100644
index 0000000..6a72bb2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_list.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml
new file mode 100644
index 0000000..86f2ecd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_play_circle.xml b/app/src/main/res/drawable/ic_play_circle.xml
new file mode 100644
index 0000000..1eb3f09
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_setting.xml b/app/src/main/res/drawable/ic_setting.xml
index 512fdde..a6fd9de 100644
--- a/app/src/main/res/drawable/ic_setting.xml
+++ b/app/src/main/res/drawable/ic_setting.xml
@@ -1,11 +1,11 @@
-
+
-
+
-
+
-
+
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..8939319
--- /dev/null
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml
index 92d7c2f..f2e69b1 100644
--- a/app/src/main/res/drawable/play.xml
+++ b/app/src/main/res/drawable/play.xml
@@ -1,8 +1,5 @@
-
+
-
+
diff --git a/app/src/main/res/drawable/selected.xml b/app/src/main/res/drawable/selected.xml
index 35e8a5a..40db326 100644
--- a/app/src/main/res/drawable/selected.xml
+++ b/app/src/main/res/drawable/selected.xml
@@ -8,5 +8,5 @@
android:fillColor="#00ffffff"/>
+ android:fillColor="@color/black"/>
diff --git a/app/src/main/res/layout/activity_change_password.xml b/app/src/main/res/layout/activity_change_password.xml
index 52acdf5..acb8af0 100644
--- a/app/src/main/res/layout/activity_change_password.xml
+++ b/app/src/main/res/layout/activity_change_password.xml
@@ -12,7 +12,7 @@
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Change Password"
+ android:text="@string/change_password"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
@@ -24,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
- android:hint="Enter Old Password"
+ android:hint="@string/enter_old_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
@@ -42,7 +42,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
- android:hint="Enter New Password"
+ android:hint="@string/enter_new_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
@@ -61,7 +61,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:padding="12dp"
- android:text="Change Password"
+ android:text="@string/change_password"
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
diff --git a/app/src/main/res/layout/activity_folders.xml b/app/src/main/res/layout/activity_folders.xml
deleted file mode 100644
index 77d9ef6..0000000
--- a/app/src/main/res/layout/activity_folders.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml
deleted file mode 100644
index c9ccbe7..0000000
--- a/app/src/main/res/layout/activity_gallery.xml
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_gallery_base.xml b/app/src/main/res/layout/activity_gallery_base.xml
deleted file mode 100644
index cc3f21b..0000000
--- a/app/src/main/res/layout/activity_gallery_base.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_hidden.xml b/app/src/main/res/layout/activity_hidden.xml
index 1937a21..a3ab21e 100644
--- a/app/src/main/res/layout/activity_hidden.xml
+++ b/app/src/main/res/layout/activity_hidden.xml
@@ -41,10 +41,48 @@
+
+
+
+
+
+
+
@@ -88,90 +126,28 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 5c3d1a2..d5c72ac 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -11,7 +11,8 @@
android:id="@+id/displayContainer"
android:layout_width="0dp"
android:layout_height="0dp"
- android:layout_margin="16dp"
+ android:layout_margin="0dp"
+ android:background="@drawable/bottom_corner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintStart_toStartOf="parent"
@@ -40,14 +41,14 @@
android:id="@+id/display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:autoSizeMaxTextSize="48sp"
+ android:autoSizeMaxTextSize="70sp"
android:autoSizeMinTextSize="16sp"
android:autoSizeStepGranularity="2sp"
android:gravity="end|bottom"
android:autoSizeTextType="uniform"
android:padding="10dp"
- android:text="0"
- android:textSize="48sp"
+ android:text=""
+ android:textSize="70sp"
tools:ignore="Suspicious0dp" />
@@ -57,7 +58,7 @@
android:id="@+id/total"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:autoSizeMaxTextSize="26sp"
+ android:autoSizeMaxTextSize="40sp"
android:autoSizeMinTextSize="24sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
@@ -65,7 +66,7 @@
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:text=""
- android:textSize="26sp"
+ android:textSize="40sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -183,6 +184,7 @@
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1"
+ android:textColor="@color/white"
android:text="×"
android:textSize="30sp"
app:cornerRadius="15dp" />
@@ -236,6 +238,7 @@
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1"
+ android:textColor="@color/white"
android:text="-"
android:textSize="30sp"
app:cornerRadius="15dp" />
@@ -289,6 +292,7 @@
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1"
+ android:textColor="@color/white"
android:text="+"
android:textSize="30sp"
app:cornerRadius="15dp" />
@@ -331,6 +335,7 @@
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="8dp"
android:layout_weight="1"
+ android:textColor="@color/white"
android:text="="
android:textSize="30sp"
app:cornerRadius="15dp" />
diff --git a/app/src/main/res/layout/activity_preview.xml b/app/src/main/res/layout/activity_preview.xml
index 424fc03..f5abfcd 100644
--- a/app/src/main/res/layout/activity_preview.xml
+++ b/app/src/main/res/layout/activity_preview.xml
@@ -29,7 +29,7 @@
android:textSize="22sp"
android:layout_height="wrap_content"
android:textStyle="bold"
- android:text="Preview File"/>
+ android:text="@string/preview_file"/>
+ android:text="@string/un_hide" />
+ android:text="@string/delete" />
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 87d76b3..b0b26e6 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -1,10 +1,291 @@
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_setup_password.xml b/app/src/main/res/layout/activity_setup_password.xml
index b70ae1e..ebc5c41 100644
--- a/app/src/main/res/layout/activity_setup_password.xml
+++ b/app/src/main/res/layout/activity_setup_password.xml
@@ -12,7 +12,7 @@
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Setup Password"
+ android:text="@string/setup_password"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
@@ -24,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
- android:hint="Enter Password"
+ android:hint="@string/enter_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
@@ -42,7 +42,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
- android:hint="Confirm Password"
+ android:hint="@string/confirm_password"
app:endIconMode="password_toggle"
app:layout_constraintTop_toBottomOf="@id/tilPassword">
@@ -60,7 +60,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
- android:hint="Security Question (For Password Reset)"
+ android:hint="@string/security_question_for_password_reset"
app:layout_constraintTop_toBottomOf="@id/tilConfirmPassword">
diff --git a/app/src/main/res/layout/activity_view_folder.xml b/app/src/main/res/layout/activity_view_folder.xml
new file mode 100644
index 0000000..a1f208d
--- /dev/null
+++ b/app/src/main/res/layout/activity_view_folder.xml
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_folder.xml b/app/src/main/res/layout/item_folder.xml
index b298013..083fdd8 100644
--- a/app/src/main/res/layout/item_folder.xml
+++ b/app/src/main/res/layout/item_folder.xml
@@ -5,12 +5,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
+ app:cardCornerRadius="8dp">
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_folder_list_style.xml b/app/src/main/res/layout/item_folder_list_style.xml
new file mode 100644
index 0000000..6aa39d6
--- /dev/null
+++ b/app/src/main/res/layout/item_folder_list_style.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_file.xml b/app/src/main/res/layout/list_item_file.xml
index bba5461..99c4a6c 100644
--- a/app/src/main/res/layout/list_item_file.xml
+++ b/app/src/main/res/layout/list_item_file.xml
@@ -13,18 +13,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
+
-
-
+ android:layout_height="match_parent">
+
+
+
+
+
-
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index fe93e10..f2fe878 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -1,5 +1,5 @@
-
+
#FF000000
#FFFFFFFF
@@ -8,4 +8,5 @@
#00C15C
#39FF97
#ffffff
+ #ffffff
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 219b5c5..430ce43 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,20 +1,15 @@
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 8a8302c..1a7ef7c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,5 +1,5 @@
-
+
#FF000000
#FFFFFFFF
@@ -8,4 +8,5 @@
#00C15C
#39FF97
#000000
+ #000000
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 54a708a..ec28001 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -91,4 +91,39 @@
Failed to create URI for file
Error unhiding file: %1$s
Select Multiple
+ Settings
+ Binondi Borthakur
+ Dynamic Theme
+ Theme Mode
+ Light
+ Dark
+ System Default
+ Calculator Hide Files
+ Developer Details
+ Theme Settings
+ Files deleted successfully
+ Some files could not be deleted
+ Unhide Files
+ Are you sure you want to unhide the selected files?
+ Files unhidden successfully
+ Some files could not be unhidden
+ Copy to Another Folder
+ Move to Another Folder
+ Select Destination Folder
+ No other folders available
+ Change Password
+ Enter Old Password
+ Enter New Password
+ Forgot Password?
+ Preview File
+ Show File Names
+ App Settings
+ Restrict Screenshots in Hidden Section
+ Security Settings
+ Version 1.3
+ App Details
+ Setup Password
+ Security Question (For Password Reset)
+ Security Answer
+ Save Password
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 6a72d8f..341c1c6 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,20 +1,18 @@
-
+