diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 32d1582..60d62f1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,52 +1,46 @@
+ package="devs.org.calculator">
-
-
-
-
-
-
-
-
-
+ android:exported="true">
@@ -58,7 +52,7 @@
android:exported="true" />
+ android:configChanges="orientation|screenSize" />
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt
deleted file mode 100644
index e4053e0..0000000
--- a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package devs.org.calculator.activities
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import devs.org.calculator.R
-import devs.org.calculator.callbacks.FileProcessCallback
-import devs.org.calculator.utils.FileManager
-import kotlinx.coroutines.launch
-import java.io.File
-
-class AudioGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
- override val fileType = FileManager.FileType.AUDIO
- private lateinit var pickAudioLauncher: ActivityResultLauncher
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setupFabButton()
-
- pickAudioLauncher =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val clipData = result.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 {
- result.data?.data?.let { uriList.add(it) }
- }
-
- if (uriList.isNotEmpty()) {
- lifecycleScope.launch {
- FileManager(
- this@AudioGalleryActivity,
- this@AudioGalleryActivity
- ).processMultipleFiles(uriList, fileType, this@AudioGalleryActivity)
- }
- } else {
- Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
-
- override fun onFilesProcessedSuccessfully(copiedFiles: List) {
- Toast.makeText(
- this@AudioGalleryActivity,
- "${copiedFiles.size} ${getString(R.string.audio_hidded_successfully)} ",
- Toast.LENGTH_SHORT
- ).show()
- loadFiles()
- }
-
- override fun onFileProcessFailed() {
- Toast.makeText(this@AudioGalleryActivity, "Failed to hide Audios", Toast.LENGTH_SHORT)
- .show()
- }
-
- private fun setupFabButton() {
- binding.fabAdd.setOnClickListener {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- type = "audio/*"
- 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)
- }
- pickAudioLauncher.launch(intent)
- }
- }
-
- override fun openPreview() {
- // Not implemented audio preview
- }
-
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt
deleted file mode 100644
index 4977a9b..0000000
--- a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package devs.org.calculator.activities
-
-import android.Manifest
-import android.annotation.SuppressLint
-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.provider.Settings
-import android.view.View
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.IntentSenderRequest
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.GridLayoutManager
-import devs.org.calculator.R
-import devs.org.calculator.adapters.FileAdapter
-import devs.org.calculator.databinding.ActivityGalleryBinding
-import devs.org.calculator.utils.FileManager
-import java.io.File
-
-abstract class BaseGalleryActivity : AppCompatActivity() {
- protected lateinit var binding: ActivityGalleryBinding
- private lateinit var fileManager: FileManager
- private lateinit var adapter: FileAdapter
- private lateinit var files: List
-
- private lateinit var intentSenderLauncher: ActivityResultLauncher
- private val storagePermissionLauncher = registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- val granted = permissions.values.all { it }
- if (granted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
- loadFiles()
- } else {
- showPermissionDeniedDialog()
- }
- }
-
- abstract val fileType: FileManager.FileType
-
- @SuppressLint("SetTextI18n")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setupIntentSenderLauncher()
- binding = ActivityGalleryBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- fileManager = FileManager(this, this)
-
- when(fileType){
- FileManager.FileType.IMAGE -> {
- val image = getString(R.string.add_image)
- binding.fabAdd.text = image
- binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$image' button"
- }
- FileManager.FileType.AUDIO -> {
- val text = getString(R.string.add_audio)
- binding.fabAdd.text = text
- binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
-
- }
- FileManager.FileType.VIDEO -> {
- val text = getString(R.string.add_video)
- binding.fabAdd.text = text
- binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
- }
- FileManager.FileType.DOCUMENT -> {
- val text = getString(R.string.add_files)
- binding.fabAdd.text = text
- binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
- }
- }
- binding.recyclerView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
- if (scrollY > oldScrollY && binding.fabAdd.isExtended) {
-
- binding.fabAdd.shrink()
- } else if (scrollY < oldScrollY && !binding.fabAdd.isExtended) {
-
- binding.fabAdd.extend()
- }
- }
- setupRecyclerView()
- checkPermissionsAndLoadFiles()
- }
-
- private fun setupIntentSenderLauncher() {
- intentSenderLauncher = registerForActivityResult(
- ActivityResultContracts.StartIntentSenderForResult()
- ) { result ->
- if (result.resultCode == RESULT_OK) {
- loadFiles()
- }
- }
- }
-
- private fun setupRecyclerView() {
- binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
- adapter = FileAdapter(fileType, this, this)
- binding.recyclerView.adapter = adapter
- }
-
- private fun checkPermissionsAndLoadFiles() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- if (!Environment.isExternalStorageManager()) {
- val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
- .addCategory("android.intent.category.DEFAULT")
- .setData(Uri.parse("package:${applicationContext.packageName}"))
- startActivityForResult(intent, 2296)
- } else {
- loadFiles()
- }
- } else {
- val permissions = arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- )
- if (permissions.any { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }) {
- storagePermissionLauncher.launch(permissions)
- } else {
- loadFiles()
- }
- }
- }
-
- protected open fun loadFiles() {
- files = fileManager.getFilesInHiddenDir(fileType)
- adapter.submitList(files)
- if (files.isEmpty()){
- binding.recyclerView.visibility = View.GONE
- binding.loading.visibility = View.GONE
- binding.noItems.visibility = View.VISIBLE
- }else{
- binding.recyclerView.visibility = View.VISIBLE
- binding.loading.visibility = View.GONE
- binding.noItems.visibility = View.GONE
- }
- }
-
- override fun onResume() {
- super.onResume()
- loadFiles()
- }
-
- abstract fun openPreview()
-
- private fun showPermissionDeniedDialog() {
- // permission denied
- }
-
- @Deprecated("This method has been deprecated in favor of using the Activity Result API\n which brings increased type safety via an {@link ActivityResultContract} and the prebuilt\n contracts for common intents available in\n {@link androidx.activity.result.contract.ActivityResultContracts}, provides hooks for\n testing, and allow receiving results in separate, testable classes independent from your\n activity. Use\n {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}\n with the appropriate {@link ActivityResultContract} and handling the result in the\n {@link ActivityResultCallback#onActivityResult(Object) callback}.")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == 2296 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- if (Environment.isExternalStorageManager()) {
- loadFiles()
- }
- }
- }
-}
diff --git a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt b/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt
deleted file mode 100644
index d620736..0000000
--- a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package devs.org.calculator.activities
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import devs.org.calculator.R
-import devs.org.calculator.utils.FileManager
-import devs.org.calculator.callbacks.FileProcessCallback
-import kotlinx.coroutines.launch
-import java.io.File
-
-class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback {
- override val fileType = FileManager.FileType.DOCUMENT
- private lateinit var pickLauncher: ActivityResultLauncher
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setupFabButton()
-
- pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val clipData = result.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 {
- result.data?.data?.let { uriList.add(it) } // Single file selected
- }
-
- if (uriList.isNotEmpty()) {
- lifecycleScope.launch {
- FileManager(this@DocumentsActivity, this@DocumentsActivity).processMultipleFiles(uriList, fileType,this@DocumentsActivity )
- }
- } else {
- Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
-
- override fun onFilesProcessedSuccessfully(copiedFiles: List) {
- Toast.makeText(this@DocumentsActivity,"${copiedFiles.size} ${getString(R.string.documents_hidden_successfully )}"
- , Toast.LENGTH_SHORT).show()
- loadFiles()
- }
-
- override fun onFileProcessFailed() {
- Toast.makeText(this@DocumentsActivity,
- getString(R.string.failed_to_hide_documents), Toast.LENGTH_SHORT).show()
- }
-
- private fun setupFabButton() {
- binding.fabAdd.setOnClickListener {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- type = "*/*"
- 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)
- }
- pickLauncher.launch(intent)
- }
- }
-
- override fun openPreview() {
- //Not implemented document preview
- }
-}
\ 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
new file mode 100644
index 0000000..6b07e0e
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt
@@ -0,0 +1,421 @@
+package devs.org.calculator.activities
+
+import android.app.AlertDialog
+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.util.Log
+import android.view.View
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.widget.EditText
+import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import devs.org.calculator.R
+import devs.org.calculator.adapters.FileAdapter
+import devs.org.calculator.adapters.FolderAdapter
+import devs.org.calculator.callbacks.DialogActionsCallback
+import devs.org.calculator.databinding.ActivityHiddenBinding
+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 java.io.File
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+
+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 val STORAGE_PERMISSION_CODE = 101
+ private val PICK_FILE_REQUEST_CODE = 102
+ private var currentFolder: File? = null
+ private var folderAdapter: FolderAdapter? = null
+ val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ 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
+ binding.addFolder.visibility = View.VISIBLE
+
+ binding.fabExpend.setOnClickListener {
+ if (isFabOpen) {
+ closeFabs()
+
+ } else {
+ openFabs()
+
+ }
+ }
+
+ binding.addImage.setOnClickListener { openFilePicker("image/*") }
+ binding.addVideo.setOnClickListener { openFilePicker("video/*") }
+ binding.addAudio.setOnClickListener { openFilePicker("audio/*") }
+ 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)
+ listFoldersInHiddenDirectory()
+ }
+
+ private fun openFilePicker(mimeType: String) {
+ val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
+ type = mimeType
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ startActivityForResult(intent, PICK_FILE_REQUEST_CODE)
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == STORAGE_PERMISSION_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.d("HiddenActivity", "READ/WRITE_EXTERNAL_STORAGE permission granted via onRequestPermissionsResult")
+ listFoldersInHiddenDirectory()
+ } else {
+ Log.d("HiddenActivity", "READ/WRITE_EXTERNAL_STORAGE permission denied via onRequestPermissionsResult")
+ // Handle denied case, maybe show a message or disable functionality
+ }
+ }
+ }
+
+ @Deprecated("This method has been deprecated in favor of using the Activity Result API\n which brings increased type safety via an {@link ActivityResultContract} and the prebuilt\n contracts for common intents available in\n {@link androidx.activity.result.contract.ActivityResultContracts}, provides hooks for\n testing, and allow receiving results in separate, testable classes independent from your\n activity. Use\n {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}\n with the appropriate {@link ActivityResultContract} and handling the result in the\n {@link ActivityResultCallback#onActivityResult(Object) callback}.")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == STORAGE_PERMISSION_CODE) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (Environment.isExternalStorageManager()) {
+ listFoldersInHiddenDirectory()
+ } else {
+ // Handle denied case
+ }
+ }
+ } else if (requestCode == PICK_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
+ data?.data?.let { uri ->
+ Log.d("HiddenActivity", "Selected file URI: $uri")
+ copyFileToHiddenDirectory(uri)
+ }
+ }
+ }
+
+ 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 ->
+ // go to selection mode
+ }
+ )
+ 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
+ }
+ } else if (!hiddenDir.exists()) {
+ fileManager.getHiddenDirectory()
+ } else {
+ Log.e("HiddenActivity", "Hidden directory is not a directory: ${hiddenDir.absolutePath}")
+ }
+ }
+
+ private fun openFolder(folder: File) {
+ Log.d("HiddenActivity", "Opening folder: ${folder.name}")
+ currentFolder = folder
+ binding.addFolder.visibility = View.GONE
+ binding.fabExpend.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}")
+
+ if (files.isNotEmpty()) {
+ binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
+
+ val fileAdapter = FileAdapter(this, this, folder).apply {
+ // Set up the callback for file operations
+ fileOperationCallback = object : FileAdapter.FileOperationCallback {
+ override fun onFileDeleted(file: File) {
+ // Refresh the file list
+ refreshCurrentFolder()
+ }
+
+ override fun onFileRenamed(oldFile: File, newFile: File) {
+ // Refresh the file list
+ refreshCurrentFolder()
+ }
+
+ override fun onRefreshNeeded() {
+ // Refresh the file list
+ refreshCurrentFolder()
+ }
+ }
+
+ submitList(files)
+ }
+
+ binding.recyclerView.adapter = fileAdapter
+ binding.recyclerView.visibility = View.VISIBLE
+ binding.noItems.visibility = View.GONE
+ } else {
+ binding.recyclerView.visibility = View.GONE
+ binding.noItems.visibility = View.VISIBLE
+ }
+ }
+
+ private fun refreshCurrentFolder() {
+ currentFolder?.let { folder ->
+ val files = folderManager.getFilesInFolder(folder)
+ (binding.recyclerView.adapter as? FileAdapter)?.submitList(files)
+
+ if (files.isEmpty()) {
+ binding.recyclerView.visibility = View.GONE
+ binding.noItems.visibility = View.VISIBLE
+ } else {
+ binding.recyclerView.visibility = View.VISIBLE
+ binding.noItems.visibility = View.GONE
+ }
+ }
+ }
+
+ 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 // Keep this visible if in folder list, but should be GONE when showing files
+
+ 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 copyFileToHiddenDirectory(uri: Uri) {
+ currentFolder?.let { destinationFolder ->
+ try {
+ val inputStream: InputStream? = contentResolver.openInputStream(uri)
+ val fileName = getFileNameFromUri(uri) ?: "unknown_file"
+ val destinationFile = File(destinationFolder, fileName)
+
+ inputStream?.use { input ->
+ val outputStream: OutputStream = FileOutputStream(destinationFile)
+ outputStream.use { output ->
+ input.copyTo(output)
+ }
+ }
+ Log.d("HiddenActivity", "File copied to: ${destinationFile.absolutePath}")
+ // Refresh the file list in the RecyclerView
+ currentFolder?.let { openFolder(it) }
+ } catch (e: Exception) {
+ Log.e("HiddenActivity", "Error copying file", e)
+ // TODO: Show error message to user
+ }
+ } ?: run {
+ Log.e("HiddenActivity", "Current folder is null, cannot copy file")
+ // TODO: Show error message to user
+ }
+ }
+
+ 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 showFileOptionsDialog(file: File) {
+ val options = arrayOf("Delete", "Rename", "Share")
+ AlertDialog.Builder(this)
+ .setTitle("Choose an action for ${file.name}")
+ .setItems(options) { dialog, which ->
+ when (which) {
+ 0 -> deleteFile(file)
+ 1 -> renameFile(file)
+ 2 -> shareFile(file)
+ }
+ }
+ .create()
+ .show()
+ }
+
+ private fun deleteFile(file: File) {
+ Log.d("HiddenActivity", "Deleting file: ${file.name}")
+ if (file.exists()) {
+ if (file.delete()) {
+ Log.d("HiddenActivity", "File deleted successfully")
+ // Refresh the file list in the RecyclerView
+ currentFolder?.let { openFolder(it) }
+ } else {
+ Log.e("HiddenActivity", "Failed to delete file: ${file.absolutePath}")
+ // TODO: Show error message to user
+ }
+ } else {
+ Log.e("HiddenActivity", "File not found for deletion: ${file.absolutePath}")
+ // TODO: Show error message to user
+ }
+ }
+
+ private fun renameFile(file: File) {
+ Log.d("HiddenActivity", "Renaming file: ${file.name}")
+ val inputEditText = EditText(this)
+ AlertDialog.Builder(this)
+ .setTitle("Rename ${file.name}")
+ .setView(inputEditText)
+ .setPositiveButton("Rename") { dialog, _ ->
+ val newName = inputEditText.text.toString().trim()
+ if (newName.isNotEmpty()) {
+ val parentDir = file.parentFile
+ if (parentDir != null) {
+ val newFile = File(parentDir, newName)
+ if (file.renameTo(newFile)) {
+ Log.d("HiddenActivity", "File renamed to: ${newFile.name}")
+ currentFolder?.let { openFolder(it) }
+ } else {
+ Log.e("HiddenActivity", "Failed to rename file: ${file.absolutePath} to ${newFile.absolutePath}")
+ }
+ } else {
+ Log.e("HiddenActivity", "Parent directory is null for renaming: ${file.absolutePath}")
+ }
+ } else {
+ Log.d("HiddenActivity", "New file name is empty")
+ }
+ dialog.dismiss()
+ }
+ .setNegativeButton("Cancel") { dialog, _ ->
+ dialog.cancel()
+ }
+ .create()
+ .show()
+ }
+
+ private fun shareFile(file: File) {
+ val uri: Uri? = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
+ uri?.let { fileUri ->
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = contentResolver.getType(fileUri) ?: "*/*"
+ putExtra(Intent.EXTRA_STREAM, fileUri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(shareIntent, "Share ${file.name}"))
+ } ?: run {
+ Log.e("HiddenActivity", "Could not get URI for sharing file: ${file.absolutePath}")
+ //Show error message to user
+ }
+ }
+
+ override fun onBackPressed() {
+ if (currentFolder != null) {
+ currentFolder = null
+ if (isFabOpen) {
+ closeFabs()
+ }
+ if (folderAdapter != null) {
+ binding.recyclerView.adapter = folderAdapter
+ }
+ 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
+ } else {
+ super.onBackPressed()
+ }
+ }
+}
\ 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
index e5f0fef..96a2702 100644
--- a/app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/HiddenVaultActivity.kt
@@ -20,17 +20,5 @@ class HiddenVaultActivity : AppCompatActivity() {
}
private fun setupNavigation() {
- binding.btnImages.setOnClickListener {
- startActivity(Intent(this, ImageGalleryActivity::class.java))
- }
- binding.btnVideos.setOnClickListener {
- startActivity(Intent(this, VideoGalleryActivity::class.java))
- }
- binding.btnAudio.setOnClickListener {
- startActivity(Intent(this, AudioGalleryActivity::class.java))
- }
- binding.btnDocs.setOnClickListener {
- startActivity(Intent(this, DocumentsActivity::class.java))
- }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt
deleted file mode 100644
index 723d823..0000000
--- a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-package devs.org.calculator.activities
-
-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.provider.Settings
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.IntentSenderRequest
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.lifecycleScope
-import devs.org.calculator.utils.FileManager
-import kotlinx.coroutines.launch
-import java.io.File
-import android.Manifest
-import devs.org.calculator.R
-import devs.org.calculator.callbacks.FileProcessCallback
-
-class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
- override val fileType = FileManager.FileType.IMAGE
- private val STORAGE_PERMISSION_CODE = 100
-
- private lateinit var intentSenderLauncher: ActivityResultLauncher
- private lateinit var pickImageLauncher: ActivityResultLauncher
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setupIntentSenderLauncher()
- setupFabButton()
-
- intentSenderLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){
- if (it.resultCode != RESULT_OK) Toast.makeText(this,
- getString(R.string.failed_to_hide_unhide_photo), Toast.LENGTH_SHORT).show()
- }
-
- pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val clipData = result.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 {
- result.data?.data?.let { uriList.add(it) }
- }
-
- if (uriList.isNotEmpty()) {
- lifecycleScope.launch {
- FileManager(this@ImageGalleryActivity, this@ImageGalleryActivity)
- .processMultipleFiles(uriList, fileType,this@ImageGalleryActivity )
- }
- } else {
- Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
- }
- }
- }
- askPermissiom()
- }
-
- override fun onFilesProcessedSuccessfully(copiedFiles: List) {
- Toast.makeText(this@ImageGalleryActivity, "${copiedFiles.size} ${getString(R.string.images_hidden_successfully)}", Toast.LENGTH_SHORT).show()
- loadFiles()
- }
-
- override fun onFileProcessFailed() {
- Toast.makeText(this@ImageGalleryActivity,
- getString(R.string.failed_to_hide_images), Toast.LENGTH_SHORT).show()
- }
-
- private fun setupIntentSenderLauncher() {
- intentSenderLauncher = registerForActivityResult(
- ActivityResultContracts.StartIntentSenderForResult()
- ) { result ->
- if (result.resultCode == RESULT_OK) {
- loadFiles()
- }
- }
- }
-
- 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()
- }
- }
-
- 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
- )
- } else {
- //storage permission granted
- }
- }
-
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (requestCode == STORAGE_PERMISSION_CODE) {
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Toast.makeText(this,
- getString(R.string.storage_permissions_granted), Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(this,
- getString(R.string.storage_permissions_denied), Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- private fun setupFabButton() {
- binding.fabAdd.setOnClickListener {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- type = "image/*"
- 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)
- }
- }
-
- override fun openPreview() {
- val intent = Intent(this, PreviewActivity::class.java).apply {
- putExtra("type", fileType)
- }
- startActivity(intent)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/activities/MainActivity.kt b/app/src/main/java/devs/org/calculator/activities/MainActivity.kt
index 6687d3d..001a02b 100644
--- a/app/src/main/java/devs/org/calculator/activities/MainActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/MainActivity.kt
@@ -23,7 +23,7 @@ import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder
import java.util.regex.Pattern
-class MainActivity : AppCompatActivity(), DialogActionsCallback {
+class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
private lateinit var binding: ActivityMainBinding
private var currentExpression = "0"
private var lastWasOperator = false
@@ -57,8 +57,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
"\n" +
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.",
"Grant",
- "Cancel",
- this
+ "Later",
+ object : DialogUtil.DialogCallback {
+ override fun onPositiveButtonClicked() {
+ fileManager.askPermission(this@MainActivity)
+ }
+
+ override fun onNegativeButtonClicked() {
+ Toast.makeText(this@MainActivity,
+ "Storage permission is required for the app to function properly",
+ Toast.LENGTH_LONG).show()
+ }
+
+ override fun onNaturalButtonClicked() {
+ Toast.makeText(this@MainActivity,
+ "You can grant permission later from Settings",
+ Toast.LENGTH_LONG).show()
+ }
+ }
)
}
setupNumberButton(binding.btn0, "0")
@@ -298,7 +314,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
}
if (PrefsUtil(this).validatePassword(currentExpression)) {
- val intent = Intent(this, HiddenVaultActivity::class.java)
+ val intent = Intent(this, HiddenActivity::class.java)
intent.putExtra("password", currentExpression)
startActivity(intent)
clearDisplay()
@@ -395,15 +411,18 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
}
override fun onPositiveButtonClicked() {
+ // Handle positive button click for both DialogUtil and DialogActionsCallback
fileManager.askPermission(this)
}
override fun onNegativeButtonClicked() {
-
+ // Handle negative button click
+ Toast.makeText(this, "Storage permission is required for the app to function properly", Toast.LENGTH_LONG).show()
}
override fun onNaturalButtonClicked() {
-
+ // Handle neutral button click
+ Toast.makeText(this, "You can grant permission later from Settings", Toast.LENGTH_LONG).show()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
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 61879d6..3c01b85 100644
--- a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt
+++ b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt
@@ -7,7 +7,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import devs.org.calculator.R
import devs.org.calculator.adapters.ImagePreviewAdapter
-import devs.org.calculator.callbacks.DialogActionsCallback
import devs.org.calculator.databinding.ActivityPreviewBinding
import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
@@ -20,6 +19,7 @@ class PreviewActivity : AppCompatActivity() {
private var currentPosition: Int = 0
private lateinit var files: List
private lateinit var type: String
+ private lateinit var folder: String
private lateinit var filetype: FileManager.FileType
private lateinit var adapter: ImagePreviewAdapter
private lateinit var fileManager: FileManager
@@ -34,9 +34,10 @@ class PreviewActivity : AppCompatActivity() {
currentPosition = intent.getIntExtra("position", 0)
type = intent.getStringExtra("type").toString()
+ folder = intent.getStringExtra("folder").toString()
setupFileType()
- files = fileManager.getFilesInHiddenDir(filetype)
+ files = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
setupImagePreview()
clickListeners()
@@ -72,7 +73,7 @@ class PreviewActivity : AppCompatActivity() {
}
private fun setupImagePreview() {
- adapter = ImagePreviewAdapter(this, filetype)
+ adapter = ImagePreviewAdapter(this, this)
adapter.images = files
binding.viewPager.adapter = adapter
@@ -110,7 +111,7 @@ class PreviewActivity : AppCompatActivity() {
getString(R.string.are_you_sure_to_delete_this_file_permanently),
getString(R.string.delete_permanently),
getString(R.string.cancel),
- object : DialogActionsCallback{
+ object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
lifecycleScope.launch {
FileManager(this@PreviewActivity, this@PreviewActivity).deletePhotoFromExternalStorage(fileUri)
@@ -119,13 +120,12 @@ class PreviewActivity : AppCompatActivity() {
}
override fun onNegativeButtonClicked() {
-
+ // Handle negative button click
}
override fun onNaturalButtonClicked() {
-
+ // Handle neutral button click
}
-
}
)
}
@@ -139,7 +139,7 @@ class PreviewActivity : AppCompatActivity() {
getString(R.string.are_you_sure_you_want_to_un_hide_this_file),
getString(R.string.un_hide),
getString(R.string.cancel),
- object : DialogActionsCallback{
+ object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
lifecycleScope.launch {
FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri)
@@ -148,13 +148,12 @@ class PreviewActivity : AppCompatActivity() {
}
override fun onNegativeButtonClicked() {
-
+ // Handle negative button click
}
override fun onNaturalButtonClicked() {
-
+ // Handle neutral button click
}
-
}
)
}
diff --git a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt
deleted file mode 100644
index ff726cc..0000000
--- a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package devs.org.calculator.activities
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import devs.org.calculator.R
-import devs.org.calculator.utils.FileManager
-import devs.org.calculator.callbacks.FileProcessCallback
-import kotlinx.coroutines.launch
-import java.io.File
-
-class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
- override val fileType = FileManager.FileType.VIDEO
- private lateinit var pickLauncher: ActivityResultLauncher
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setupFabButton()
-
- pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == RESULT_OK) {
- val clipData = result.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 {
- result.data?.data?.let { uriList.add(it) } // Single file selected
- }
-
- if (uriList.isNotEmpty()) {
- lifecycleScope.launch {
- FileManager(this@VideoGalleryActivity, this@VideoGalleryActivity)
- .processMultipleFiles(uriList, fileType,this@VideoGalleryActivity )
- }
- } else {
- Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
-
- override fun onFilesProcessedSuccessfully(copiedFiles: List) {
- Toast.makeText(this@VideoGalleryActivity, "${copiedFiles.size} ${getString(R.string.videos_hidden_successfully)}"
- , Toast.LENGTH_SHORT).show()
- loadFiles()
- }
-
- override fun onFileProcessFailed() {
- Toast.makeText(this@VideoGalleryActivity,
- getString(R.string.failed_to_hide_videos), Toast.LENGTH_SHORT).show()
- }
-
- private fun setupFabButton() {
- binding.fabAdd.setOnClickListener {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- type = "video/*"
- 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)
- }
- pickLauncher.launch(intent)
- }
- }
-
- override fun openPreview() {
- val intent = Intent(this, PreviewActivity::class.java).apply {
- putExtra("type", fileType)
- }
- startActivity(intent)
- }
-}
\ 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 6b1fc0e..d483806 100644
--- a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt
+++ b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt
@@ -1,197 +1,341 @@
package devs.org.calculator.adapters
+import android.app.AlertDialog
import android.content.Context
import android.content.Intent
+import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.EditText
import android.widget.ImageView
+import android.widget.TextView
import android.widget.Toast
+import androidx.core.content.FileProvider
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import devs.org.calculator.R
import devs.org.calculator.activities.PreviewActivity
-import devs.org.calculator.callbacks.DialogActionsCallback
-import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
-import kotlinx.coroutines.launch
import java.io.File
class FileAdapter(
- private val fileType: FileManager.FileType,
- var context: Context,
- private var lifecycleOwner: LifecycleOwner
-) :
- ListAdapter(FileDiffCallback()) {
+ private val context: Context,
+ private val lifecycleOwner: LifecycleOwner,
+ private val currentFolder: File
+) : ListAdapter(FileDiffCallback()) {
private val selectedItems = mutableSetOf()
private var isSelectionMode = false
- private var fileName = "Unknown File"
- private var fileTypes = when (fileType) {
-
- FileManager.FileType.IMAGE -> {
- context.getString(R.string.image)
- }
-
- FileManager.FileType.VIDEO -> {
- context.getString(R.string.video)
- }
-
- FileManager.FileType.AUDIO -> {
- context.getString(R.string.audio)
- }
-
- else -> context.getString(R.string.document)
+ // Callback interface for handling file operations
+ interface FileOperationCallback {
+ fun onFileDeleted(file: File)
+ fun onFileRenamed(oldFile: File, newFile: File)
+ fun onRefreshNeeded()
}
+ var fileOperationCallback: FileOperationCallback? = null
+
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
- private val imageView: ImageView = view.findViewById(R.id.imageView)
+ val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
+ val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
+ val playIcon: ImageView = view.findViewById(R.id.videoPlay)
fun bind(file: File) {
+ val fileType = FileManager(context, lifecycleOwner).getFileType(file)
+ setupFileDisplay(file, fileType)
+ setupClickListeners(file, fileType)
+ // Handle selection state
+ itemView.isSelected = selectedItems.contains(adapterPosition)
+ }
+
+ fun bind(file: File, payloads: List) {
+ if (payloads.isEmpty()) {
+ bind(file)
+ return
+ }
+
+ // Handle partial updates based on payload
+ val changes = payloads.firstOrNull() as? List
+ changes?.forEach { change ->
+ when (change) {
+ "NAME_CHANGED" -> {
+ fileNameTextView.text = file.name
+ }
+ "SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
+ // Could update file info if displayed
+ }
+ }
+ }
+ }
+
+ private fun setupFileDisplay(file: File, fileType: FileManager.FileType) {
when (fileType) {
FileManager.FileType.IMAGE -> {
- Glide.with(imageView)
- .load(file)
- .centerCrop()
- .into(imageView)
+ loadImageThumbnail(file)
+ fileNameTextView.visibility = View.GONE
+ playIcon.visibility = View.GONE
}
-
FileManager.FileType.VIDEO -> {
- Glide.with(imageView)
- .asBitmap()
- .load(file)
- .centerCrop()
- .into(imageView)
+ loadVideoThumbnail(file)
+ fileNameTextView.visibility = View.GONE
+ playIcon.visibility = View.VISIBLE
}
-
else -> {
- 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)
+ loadFileIcon(fileType)
+ fileNameTextView.visibility = View.VISIBLE
+ playIcon.visibility = View.GONE
}
}
+ 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 {
-
-
- when(fileType){
- FileManager.FileType.AUDIO -> {
- // Create an intent to play audio using available audio players
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(FileManager.FileManager().getContentUriImage(context, file, fileType), "audio/*")
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- try {
- context.startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(context,
- context.getString(R.string.no_audio_player_found), Toast.LENGTH_SHORT).show()
- }
- }
-
- FileManager.FileType.DOCUMENT -> {
- // Create an intent to open the document using available viewers or file managers
- val intent = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(FileManager.FileManager().getContentUriImage(context, file, fileType), "*/*")
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- try {
- context.startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(context,
- context.getString(R.string.no_suitable_app_found_to_open_this_document), Toast.LENGTH_SHORT).show()
- }
- }
- else -> {
- val intent = Intent(context, PreviewActivity::class.java).apply {
- putExtra("type", fileTypes)
- putExtra("position", position)
- }
- context.startActivity(intent)
- }
+ if (isSelectionMode) {
+ toggleSelection(adapterPosition)
+ return@setOnClickListener
}
-
-
+ openFile(file, fileType)
}
+
itemView.setOnLongClickListener {
-
- val fileUri = FileManager.FileManager().getContentUriImage(context, file, fileType)
- if (fileUri == null) {
- Toast.makeText(context, "Unable to access file: $file", Toast.LENGTH_SHORT)
- .show()
-
- return@setOnLongClickListener true
-
+ if (!isSelectionMode) {
+ showFileOptionsDialog(file)
+ true
+ } else {
+ false
}
- fileName = FileManager.FileName(context).getFileNameFromUri(fileUri)?.toString()
- ?: context.getString(R.string.unknown_file)
+ }
+ }
- DialogUtil(context).showMaterialDialogWithNaturalButton(
- context.getString(R.string.details, fileTypes),
- "File Name: $fileName\n\nFile Path: $file\n\nYou can permanently delete or un-hide this file.",
- context.getString(R.string.delete_permanently),
- context.getString(R.string.un_hide),
- context.getString(R.string.cancel),
- object : DialogActionsCallback {
- override fun onPositiveButtonClicked() {
- lifecycleOwner.lifecycleScope.launch {
- FileManager(context, lifecycleOwner).deletePhotoFromExternalStorage(
- fileUri
- )
- }
- val currentList = currentList.toMutableList()
- currentList.remove(file)
- submitList(currentList)
- }
+ private fun openFile(file: File, fileType: FileManager.FileType) {
+ when (fileType) {
+ FileManager.FileType.AUDIO -> openAudioFile(file)
+ FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> openInPreview(fileType)
+ FileManager.FileType.DOCUMENT -> openDocumentFile(file)
+ else -> openDocumentFile(file)
+ }
+ }
- override fun onNegativeButtonClicked() {
- FileManager(context, lifecycleOwner).copyFileToNormalDir(fileUri)
- val currentList = currentList.toMutableList()
- currentList.remove(file)
- submitList(currentList)
- }
+ 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 {
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.no_audio_player_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
- override fun onNaturalButtonClicked() {
+ 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 {
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.no_suitable_app_found_to_open_this_document),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
- }
- }
- )
-
- return@setOnLongClickListener true
+ private fun openInPreview(fileType: FileManager.FileType) {
+ val fileTypeString = when (fileType) {
+ FileManager.FileType.IMAGE -> context.getString(R.string.image)
+ FileManager.FileType.VIDEO -> context.getString(R.string.video)
+ else -> "unknown"
}
+ val intent = Intent(context, PreviewActivity::class.java).apply {
+ putExtra("type", fileTypeString)
+ putExtra("folder", currentFolder.toString())
+ putExtra("position", adapterPosition)
+ }
+ context.startActivity(intent)
+ }
+ private fun showFileOptionsDialog(file: File) {
+ val options = arrayOf(
+ context.getString(R.string.delete),
+ context.getString(R.string.rename),
+ context.getString(R.string.share)
+ )
+
+ AlertDialog.Builder(context)
+ .setTitle(context.getString(R.string.file_options))
+ .setItems(options) { dialog, which ->
+ when (which) {
+ 0 -> deleteFile(file)
+ 1 -> renameFile(file)
+ 2 -> shareFile(file)
+ }
+ dialog.dismiss()
+ }
+ .create()
+ .show()
+ }
+
+ 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()
+ }
+ }
+
+ private fun renameFile(file: File) {
+ val inputEditText = EditText(context).apply {
+ setText(file.name)
+ selectAll()
+ }
+
+ AlertDialog.Builder(context)
+ .setTitle(context.getString(R.string.rename_file))
+ .setView(inputEditText)
+ .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()
+ }
+ }
+ }
+ dialog.dismiss()
+ }
+ .setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
+ dialog.cancel()
+ }
+ .create()
+ .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)
+ }
+ context.startActivity(
+ Intent.createChooser(shareIntent, context.getString(R.string.share_file))
+ )
+ }
+
+ private fun toggleSelection(position: Int) {
+ if (selectedItems.contains(position)) {
+ selectedItems.remove(position)
+ } else {
+ selectedItems.add(position)
+ }
+
+ if (selectedItems.isEmpty()) {
+ isSelectionMode = false
+ }
+
+ notifyItemChanged(position)
}
}
-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.item_file, parent, false)
+ .inflate(R.layout.list_item_file, parent, false)
return FileViewHolder(view)
}
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
- holder.bind(getItem(position))
+ val file = getItem(position)
+ holder.bind(file)
}
- class FileDiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
- return oldItem.path == newItem.path
- }
-
- override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
- return oldItem == newItem
+ 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)
}
}
+ // Public methods for external control
+ fun clearSelection() {
+ selectedItems.clear()
+ isSelectionMode = false
+ notifyDataSetChanged()
+ }
-}
+ fun getSelectedItems(): List {
+ return selectedItems.mapNotNull { position ->
+ if (position < itemCount) getItem(position) else null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/adapters/FileDiffCallback.kt b/app/src/main/java/devs/org/calculator/adapters/FileDiffCallback.kt
new file mode 100644
index 0000000..1058113
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/adapters/FileDiffCallback.kt
@@ -0,0 +1,42 @@
+package devs.org.calculator.adapters
+
+import androidx.recyclerview.widget.DiffUtil
+import java.io.File
+
+class FileDiffCallback : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
+ // Compare by absolute path since File objects might be different instances
+ // but represent the same file
+ return oldItem.absolutePath == newItem.absolutePath
+ }
+
+ override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
+ // Compare all relevant properties that might change and affect the UI
+ return oldItem.name == newItem.name &&
+ oldItem.length() == newItem.length() &&
+ oldItem.lastModified() == newItem.lastModified() &&
+ oldItem.canRead() == newItem.canRead() &&
+ oldItem.canWrite() == newItem.canWrite()
+ }
+
+ override fun getChangePayload(oldItem: File, newItem: File): Any? {
+ // Return a payload if only specific properties changed
+ // This allows for partial updates instead of full rebinding
+ val changes = mutableListOf()
+
+ if (oldItem.name != newItem.name) {
+ changes.add("NAME_CHANGED")
+ }
+
+ if (oldItem.length() != newItem.length()) {
+ changes.add("SIZE_CHANGED")
+ }
+
+ if (oldItem.lastModified() != newItem.lastModified()) {
+ changes.add("MODIFIED_DATE_CHANGED")
+ }
+
+ return if (changes.isNotEmpty()) changes else null
+ }
+}
\ 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
new file mode 100644
index 0000000..75dae84
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt
@@ -0,0 +1,53 @@
+package devs.org.calculator.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+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 FolderAdapter(
+ private val onFolderClick: (File) -> Unit,
+ private val onFolderLongClick: (File) -> Unit
+) : ListAdapter(FolderDiffCallback()) {
+
+ class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
+
+ fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit) {
+ folderNameTextView.text = folder.name
+
+ itemView.setOnClickListener { onFolderClick(folder) }
+ itemView.setOnLongClickListener {
+ onFolderLongClick(folder)
+ true
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.item_folder, parent, false)
+ return FolderViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
+ val folder = getItem(position)
+ holder.bind(folder, onFolderClick, onFolderLongClick)
+ }
+
+ 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 &&
+ oldItem.lastModified() == newItem.lastModified() &&
+ oldItem.length() == newItem.length()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt b/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt
index 9280294..35312e5 100644
--- a/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt
+++ b/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt
@@ -10,10 +10,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import android.widget.SeekBar
+import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
-import devs.org.calculator.adapters.FileAdapter.FileDiffCallback
import devs.org.calculator.databinding.ViewpagerItemsBinding
import devs.org.calculator.utils.FileManager
import java.io.File
@@ -21,7 +21,7 @@ import devs.org.calculator.R
class ImagePreviewAdapter(
private val context: Context,
- private var fileType: FileManager.FileType
+ private var lifecycleOwner: LifecycleOwner
) : RecyclerView.Adapter() {
private val differ = AsyncListDiffer(this, FileDiffCallback())
@@ -42,7 +42,8 @@ class ImagePreviewAdapter(
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = images[position]
- holder.bind(imageUrl)
+ val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
+ holder.bind(imageUrl,fileType)
currentViewHolder = holder
currentMediaPlayer?.let {
@@ -67,7 +68,7 @@ class ImagePreviewAdapter(
private var seekHandler = Handler(Looper.getMainLooper())
private var seekRunnable: Runnable? = null
- fun bind(file: File) {
+ fun bind(file: File, fileType: FileManager.FileType) {
when (fileType) {
FileManager.FileType.VIDEO -> {
binding.imageView.visibility = View.GONE
@@ -228,6 +229,7 @@ class ImagePreviewAdapter(
private fun playVideoAtPosition(position: Int) {
val nextFile = images[position]
+ val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
if (fileType == FileManager.FileType.VIDEO) {
val videoUri = Uri.fromFile(nextFile)
binding.videoView.setVideoURI(videoUri)
diff --git a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt
index 94179b6..44a4c83 100644
--- a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt
+++ b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt
@@ -1,59 +1,57 @@
package devs.org.calculator.utils
import android.content.Context
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.IntentSenderRequest
+import android.view.LayoutInflater
+import android.widget.EditText
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import devs.org.calculator.callbacks.DialogActionsCallback
+import devs.org.calculator.R
class DialogUtil(private val context: Context) {
- private lateinit var intentSenderLauncher: ActivityResultLauncher
- fun showMaterialDialogWithNaturalButton(
- title: String,
- message: String,
- positiveButton: String,
- negativeButton: String,
- neutralButton: String,
- callback: DialogActionsCallback
- ) {
- MaterialAlertDialogBuilder(context)
- .setTitle(title)
- .setMessage(message)
- .setPositiveButton(positiveButton) { dialog, _ ->
- // Handle positive button click
- callback.onPositiveButtonClicked()
- dialog.dismiss()
- }
- .setNegativeButton(negativeButton) { dialog, _ ->
- // Handle negative button click
- callback.onNegativeButtonClicked()
- dialog.dismiss()
- }
- .setNeutralButton(neutralButton) { dialog, _ ->
- callback.onNaturalButtonClicked()
- dialog.dismiss()
- }
- .show()
- }
+
fun showMaterialDialog(
title: String,
message: String,
- positiveButton: String,
- negativeButton: String,
- callback: DialogActionsCallback
+ positiveButtonText: String,
+ neutralButtonText: String,
+ callback: DialogCallback
) {
MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
- .setPositiveButton(positiveButton) { _, _ ->
- // Handle positive button click
- callback.onPositiveButtonClicked()
- }
- .setNegativeButton(negativeButton) { dialog, _ ->
- // Handle negative button click
- callback.onNegativeButtonClicked()
- dialog.dismiss()
- }
+ .setPositiveButton(positiveButtonText) { _, _ -> callback.onPositiveButtonClicked() }
+ .setNegativeButton(neutralButtonText) { _, _ -> callback.onNegativeButtonClicked() }
.show()
}
+
+ fun createInputDialog(
+ title: String,
+ hint: String,
+ callback: InputDialogCallback
+ ) {
+ val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
+ val editText = dialogView.findViewById(R.id.editText)
+ editText.hint = hint
+
+ MaterialAlertDialogBuilder(context)
+ .setTitle(title)
+ .setView(dialogView)
+ .setPositiveButton(R.string.create) { _, _ ->
+ callback.onPositiveButtonClicked(editText.text.toString())
+ }
+ .setNegativeButton(R.string.cancel) { dialog, _ ->
+ dialog.dismiss()
+ }
+ .create()
+ .show()
+ }
+
+ interface DialogCallback {
+ fun onPositiveButtonClicked()
+ fun onNegativeButtonClicked()
+ fun onNaturalButtonClicked()
+ }
+
+ interface InputDialogCallback {
+ fun onPositiveButtonClicked(input: String)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/utils/FileManager.kt b/app/src/main/java/devs/org/calculator/utils/FileManager.kt
index 72b0138..28380f8 100644
--- a/app/src/main/java/devs/org/calculator/utils/FileManager.kt
+++ b/app/src/main/java/devs/org/calculator/utils/FileManager.kt
@@ -41,31 +41,43 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
fun getHiddenDirectory(): File {
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
if (!dir.exists()) {
- dir.mkdirs()
+ val created = dir.mkdirs()
+ if (!created) {
+ throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
+ }
// Create .nomedia file to hide from media scanners
- File(dir, ".nomedia").createNewFile()
+ val nomediaFile = File(dir, ".nomedia")
+ if (!nomediaFile.exists()) {
+ nomediaFile.createNewFile()
+ }
}
return dir
}
-
-
fun getFilesInHiddenDir(type: FileType): List {
val hiddenDir = getHiddenDirectory()
val typeDir = File(hiddenDir, type.dirName)
- return if (typeDir.exists()) {
- typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
- } else {
- emptyList()
+ if (!typeDir.exists()) {
+ typeDir.mkdirs()
+ File(typeDir, ".nomedia").createNewFile()
}
+ return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
+ }
+ fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List {
+ val typeDir = File(folder)
+ if (!typeDir.exists()) {
+ typeDir.mkdirs()
+ File(typeDir, ".nomedia").createNewFile()
+ }
+ return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
}
- private fun copyFileToHiddenDir(uri: Uri, type: FileType): File? {
+ private fun copyFileToHiddenDir(uri: Uri, type: FileType, currentDir: File? = null): File? {
return try {
val contentResolver = context.contentResolver
// Get the target directory
- val targetDir = File(Environment.getExternalStorageDirectory(), "$HIDDEN_DIR/${type.dirName}")
+ val targetDir = currentDir ?: File(Environment.getExternalStorageDirectory(), "$HIDDEN_DIR/${type.dirName}")
targetDir.mkdirs()
File(targetDir, ".nomedia").createNewFile()
@@ -258,8 +270,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, "Unable to open settings. Please grant permission manually.", Toast.LENGTH_SHORT).show()
}
- } else {
- Toast.makeText(activity, "Permission already granted", Toast.LENGTH_SHORT).show()
}
} else {
// For Android 10 and below
@@ -275,13 +285,14 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
suspend fun processMultipleFiles(
uriList: List,
fileType: FileType,
- callback: FileProcessCallback
+ callback: FileProcessCallback,
+ currentDir: File? = null
) {
withContext(Dispatchers.IO) {
val copiedFiles = mutableListOf()
for (uri in uriList) {
try {
- val file = copyFileToHiddenDir(uri, fileType)
+ val file = copyFileToHiddenDir(uri, fileType, currentDir)
file?.let { copiedFiles.add(it) }
} catch (e: Exception) {
e.printStackTrace()
@@ -297,12 +308,22 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
}
}
+ fun getFileType(file: File): FileType {
+ val extension = file.extension.lowercase()
+ return when (extension) {
+ "jpg", "jpeg", "png", "gif", "bmp", "webp" -> FileType.IMAGE
+ "mp4", "avi", "mkv", "mov", "wmv", "flv", "webm", "3gp" -> FileType.VIDEO
+ "mp3", "wav", "flac", "aac", "ogg", "m4a" -> FileType.AUDIO
+ else -> FileType.DOCUMENT
+ }
+ }
enum class FileType(val dirName: String) {
IMAGE(IMAGES_DIR),
VIDEO(VIDEOS_DIR),
AUDIO(AUDIO_DIR),
- DOCUMENT(DOCS_DIR)
+ DOCUMENT(DOCS_DIR),
+ ALL("all")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/devs/org/calculator/utils/FolderManager.kt b/app/src/main/java/devs/org/calculator/utils/FolderManager.kt
new file mode 100644
index 0000000..053cf12
--- /dev/null
+++ b/app/src/main/java/devs/org/calculator/utils/FolderManager.kt
@@ -0,0 +1,75 @@
+package devs.org.calculator.utils
+
+import android.content.Context
+import android.os.Environment
+import java.io.File
+
+class FolderManager(private val context: Context) {
+ companion object {
+ const val HIDDEN_DIR = ".CalculatorHide"
+ }
+
+ fun createFolder(parentDir: File, folderName: String): Boolean {
+ val newFolder = File(parentDir, folderName)
+ return if (!newFolder.exists()) {
+ newFolder.mkdirs()
+ // Create .nomedia file to hide from media scanners
+ File(newFolder, ".nomedia").createNewFile()
+ true
+ } else {
+ false
+ }
+ }
+
+ fun deleteFolder(folder: File): Boolean {
+ return try {
+ if (folder.exists() && folder.isDirectory) {
+ // Delete all files in the folder first
+ folder.listFiles()?.forEach { file ->
+ if (file.isFile) {
+ file.delete()
+ }
+ }
+ // Then delete the folder itself
+ folder.delete()
+ } else {
+ false
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+
+ fun getFoldersInDirectory(directory: File): List {
+ return if (directory.exists() && directory.isDirectory) {
+ directory.listFiles()?.filter { it.isDirectory && it.name != ".nomedia" } ?: emptyList()
+ } else {
+ emptyList()
+ }
+ }
+
+ fun getFilesInFolder(folder: File): List {
+ return if (folder.exists() && folder.isDirectory) {
+ folder.listFiles()?.filter { it.isFile && it.name != ".nomedia" } ?: emptyList()
+ } else {
+ emptyList()
+ }
+ }
+
+ fun moveFileToFolder(file: File, targetFolder: File): Boolean {
+ return try {
+ if (!targetFolder.exists()) {
+ targetFolder.mkdirs()
+ File(targetFolder, ".nomedia").createNewFile()
+ }
+ val newFile = File(targetFolder, file.name)
+ file.copyTo(newFile, overwrite = true)
+ file.delete()
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/anim/fab_close.xml b/app/src/main/res/anim/fab_close.xml
new file mode 100644
index 0000000..3463d4a
--- /dev/null
+++ b/app/src/main/res/anim/fab_close.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/anim/fab_open.xml b/app/src/main/res/anim/fab_open.xml
new file mode 100644
index 0000000..fe55db7
--- /dev/null
+++ b/app/src/main/res/anim/fab_open.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/anim/rotate_close.xml b/app/src/main/res/anim/rotate_close.xml
new file mode 100644
index 0000000..65cd550
--- /dev/null
+++ b/app/src/main/res/anim/rotate_close.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/app/src/main/res/anim/rotate_open.xml b/app/src/main/res/anim/rotate_open.xml
new file mode 100644
index 0000000..11ac20c
--- /dev/null
+++ b/app/src/main/res/anim/rotate_open.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/app/src/main/res/drawable/add_image.xml b/app/src/main/res/drawable/add_image.xml
new file mode 100644
index 0000000..120d2c4
--- /dev/null
+++ b/app/src/main/res/drawable/add_image.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/document_add.xml b/app/src/main/res/drawable/document_add.xml
new file mode 100644
index 0000000..26f5edb
--- /dev/null
+++ b/app/src/main/res/drawable/document_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 0000000..9080205
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_audio.xml b/app/src/main/res/drawable/ic_audio.xml
index 605c2df..fecd069 100644
--- a/app/src/main/res/drawable/ic_audio.xml
+++ b/app/src/main/res/drawable/ic_audio.xml
@@ -1,9 +1,10 @@
+
+ android:fillColor="#FFFFFF"
+ android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
\ 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
new file mode 100644
index 0000000..86e0ece
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 0000000..6e66940
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_create_new_folder.xml b/app/src/main/res/drawable/ic_create_new_folder.xml
new file mode 100644
index 0000000..84b40c3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_create_new_folder.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..f25a97f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_document.xml b/app/src/main/res/drawable/ic_document.xml
index b910720..7826c6d 100644
--- a/app/src/main/res/drawable/ic_document.xml
+++ b/app/src/main/res/drawable/ic_document.xml
@@ -1,9 +1,10 @@
+
+ android:fillColor="#FFFFFF"
+ android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml
new file mode 100644
index 0000000..aeafcac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_folder_add.xml b/app/src/main/res/drawable/ic_folder_add.xml
new file mode 100644
index 0000000..4fe3f47
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder_add.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_image.xml b/app/src/main/res/drawable/ic_image.xml
index 55c6b6d..87ffba1 100644
--- a/app/src/main/res/drawable/ic_image.xml
+++ b/app/src/main/res/drawable/ic_image.xml
@@ -1,9 +1,10 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_no_items.xml b/app/src/main/res/drawable/ic_no_items.xml
new file mode 100644
index 0000000..5b9a7b0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_no_items.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_video.xml b/app/src/main/res/drawable/ic_video.xml
new file mode 100644
index 0000000..088ec55
--- /dev/null
+++ b/app/src/main/res/drawable/ic_video.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/music_add.xml b/app/src/main/res/drawable/music_add.xml
new file mode 100644
index 0000000..d426695
--- /dev/null
+++ b/app/src/main/res/drawable/music_add.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml
index 629314a..92d7c2f 100644
--- a/app/src/main/res/drawable/play.xml
+++ b/app/src/main/res/drawable/play.xml
@@ -2,7 +2,7 @@
android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
-
diff --git a/app/src/main/res/drawable/video_add.xml b/app/src/main/res/drawable/video_add.xml
new file mode 100644
index 0000000..37e67d2
--- /dev/null
+++ b/app/src/main/res/drawable/video_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/wrong.xml b/app/src/main/res/drawable/wrong.xml
new file mode 100644
index 0000000..e5da27d
--- /dev/null
+++ b/app/src/main/res/drawable/wrong.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_audio_gallery.xml b/app/src/main/res/layout/activity_audio_gallery.xml
deleted file mode 100644
index 27e8f96..0000000
--- a/app/src/main/res/layout/activity_audio_gallery.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_documents.xml b/app/src/main/res/layout/activity_folders.xml
similarity index 51%
rename from app/src/main/res/layout/activity_documents.xml
rename to app/src/main/res/layout/activity_folders.xml
index 0c349c9..77d9ef6 100644
--- a/app/src/main/res/layout/activity_documents.xml
+++ b/app/src/main/res/layout/activity_folders.xml
@@ -1,10 +1,6 @@
+ android:layout_height="match_parent">
\ 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
index ad7006d..c9ccbe7 100644
--- a/app/src/main/res/layout/activity_gallery.xml
+++ b/app/src/main/res/layout/activity_gallery.xml
@@ -3,58 +3,143 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
-
android:layout_height="match_parent">
-
+ android:layout_height="match_parent">
-
-
-
+
-
-
-
+ android:orientation="vertical"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_margin="16dp"
+ android:contentDescription="@string/add_files"
+ app:srcCompat="@drawable/ic_add" />
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..6dcf5e5
--- /dev/null
+++ b/app/src/main/res/layout/activity_hidden.xml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_image_gallery.xml b/app/src/main/res/layout/activity_image_gallery.xml
deleted file mode 100644
index 2eeda20..0000000
--- a/app/src/main/res/layout/activity_image_gallery.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_video_gallery.xml b/app/src/main/res/layout/activity_video_gallery.xml
deleted file mode 100644
index 8514256..0000000
--- a/app/src/main/res/layout/activity_video_gallery.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_input.xml b/app/src/main/res/layout/dialog_input.xml
new file mode 100644
index 0000000..a3f6a6a
--- /dev/null
+++ b/app/src/main/res/layout/dialog_input.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..5939ad4
--- /dev/null
+++ b/app/src/main/res/layout/item_folder.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..aee1cfe
--- /dev/null
+++ b/app/src/main/res/layout/list_item_file.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_folder.xml b/app/src/main/res/layout/list_item_folder.xml
new file mode 100644
index 0000000..ab177b0
--- /dev/null
+++ b/app/src/main/res/layout/list_item_folder.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ 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 0fb397c..e1c441a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -5,6 +5,12 @@
Add Audio
Add Video
Add Files
+ Share
+ File Options
+ Rename File
+ Share File
+ Add Document
+ Failed to hide Audios
Failed to hide Documents
No files selected
Documents hidden successfully
@@ -46,6 +52,11 @@
IMAGE
VIDEO
AUDIO
+ Delete
+ Create
+ Delete Folder
+ Rename
+ Cannot Delete Folder
DOCUMENT
No audio player found!
No suitable app found to open this document!
@@ -55,4 +66,18 @@
No Items Available, Add one by clicking on the
Now Enter \'=\' button
Enter 123456
+ Create Folder
+ Enter folder name
+ Folder already exists
+ Folder Options
+ Rename Folder
+ Enter new folder name
+ Failed to create folder
+ Are you sure you want to delete this folder?
+ Yes
+ Error loading files
+ Delete Items
+ Are you sure you want to delete selected items?
+ Items deleted successfully
+ Some items could not be deleted
\ No newline at end of file