From 4069ddc20020458e97c95cde50e941c0bf676cf4 Mon Sep 17 00:00:00 2001 From: Binondi Date: Tue, 3 Jun 2025 00:52:41 +0530 Subject: [PATCH] Changes For Folder Feature --- app/src/main/AndroidManifest.xml | 6 +- .../java/devs/org/calculator/CalculatorApp.kt | 15 +- .../calculator/activities/HiddenActivity.kt | 804 +++++++++--------- .../activities/HiddenVaultActivity.kt | 24 - .../calculator/activities/PreviewActivity.kt | 17 + .../calculator/activities/SettingsActivity.kt | 136 ++- .../activities/ViewFolderActivity.kt | 596 +++++++++++++ .../org/calculator/adapters/FileAdapter.kt | 633 +++++++++----- .../org/calculator/adapters/FolderAdapter.kt | 38 +- .../calculator/adapters/ListFolderAdapter.kt | 124 +++ .../devs/org/calculator/utils/PrefsUtil.kt | 9 + app/src/main/res/drawable/bottom_corner.xml | 6 + app/src/main/res/drawable/ic_back.xml | 4 +- app/src/main/res/drawable/ic_delete.xml | 2 +- app/src/main/res/drawable/ic_edit.xml | 9 + .../main/res/drawable/ic_folder_yellow.xml | 37 + app/src/main/res/drawable/ic_github.xml | 10 + app/src/main/res/drawable/ic_grid.xml | 12 + app/src/main/res/drawable/ic_list.xml | 9 + app/src/main/res/drawable/ic_more.xml | 8 + app/src/main/res/drawable/ic_play_circle.xml | 12 + app/src/main/res/drawable/ic_setting.xml | 8 +- app/src/main/res/drawable/ic_settings.xml | 9 + app/src/main/res/drawable/play.xml | 7 +- app/src/main/res/drawable/selected.xml | 2 +- .../res/layout/activity_change_password.xml | 10 +- app/src/main/res/layout/activity_folders.xml | 6 - app/src/main/res/layout/activity_gallery.xml | 145 ---- .../main/res/layout/activity_gallery_base.xml | 25 - app/src/main/res/layout/activity_hidden.xml | 110 +-- .../main/res/layout/activity_hidden_vault.xml | 121 --- app/src/main/res/layout/activity_main.xml | 17 +- app/src/main/res/layout/activity_preview.xml | 6 +- app/src/main/res/layout/activity_settings.xml | 287 ++++++- .../res/layout/activity_setup_password.xml | 14 +- .../main/res/layout/activity_view_folder.xml | 169 ++++ app/src/main/res/layout/item_folder.xml | 28 +- .../res/layout/item_folder_list_style.xml | 97 +++ app/src/main/res/layout/list_item_file.xml | 66 +- app/src/main/res/values-night/colors.xml | 3 +- app/src/main/res/values-night/themes.xml | 7 +- app/src/main/res/values/colors.xml | 3 +- app/src/main/res/values/strings.xml | 35 + app/src/main/res/values/themes.xml | 8 +- 44 files changed, 2608 insertions(+), 1086 deletions(-) delete mode 100644 app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt create mode 100644 app/src/main/java/devs/org/calculator/activities/ViewFolderActivity.kt create mode 100644 app/src/main/java/devs/org/calculator/adapters/ListFolderAdapter.kt create mode 100644 app/src/main/res/drawable/bottom_corner.xml create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_folder_yellow.xml create mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_grid.xml create mode 100644 app/src/main/res/drawable/ic_list.xml create mode 100644 app/src/main/res/drawable/ic_more.xml create mode 100644 app/src/main/res/drawable/ic_play_circle.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml delete mode 100644 app/src/main/res/layout/activity_folders.xml delete mode 100644 app/src/main/res/layout/activity_gallery.xml delete mode 100644 app/src/main/res/layout/activity_gallery_base.xml delete mode 100644 app/src/main/res/layout/activity_hidden_vault.xml create mode 100644 app/src/main/res/layout/activity_view_folder.xml create mode 100644 app/src/main/res/layout/item_folder_list_style.xml 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 @@ - +