From ab737511a7d09da2ba1b91e24c42dbd0458845c9 Mon Sep 17 00:00:00 2001 From: Binondi Date: Sun, 1 Jun 2025 16:47:45 +0530 Subject: [PATCH] Changes For Folder Feature --- .idea/AndroidProjectSystem.xml | 6 + app/src/main/AndroidManifest.xml | 39 +- .../activities/AudioGalleryActivity.kt | 86 ---- .../activities/BaseGalleryActivity.kt | 162 ------- .../activities/DocumentsActivity.kt | 77 ---- .../calculator/activities/HiddenActivity.kt | 421 ++++++++++++++++++ .../activities/HiddenVaultActivity.kt | 12 - .../activities/ImageGalleryActivity.kt | 160 ------- .../org/calculator/activities/MainActivity.kt | 31 +- .../calculator/activities/PreviewActivity.kt | 21 +- .../activities/VideoGalleryActivity.kt | 82 ---- .../org/calculator/adapters/FileAdapter.kt | 410 +++++++++++------ .../calculator/adapters/FileDiffCallback.kt | 42 ++ .../org/calculator/adapters/FolderAdapter.kt | 53 +++ .../adapters/ImagePreviewAdapter.kt | 10 +- .../devs/org/calculator/utils/DialogUtil.kt | 84 ++-- .../devs/org/calculator/utils/FileManager.kt | 51 ++- .../org/calculator/utils/FolderManager.kt | 75 ++++ app/src/main/res/anim/fab_close.xml | 9 + app/src/main/res/anim/fab_open.xml | 9 + app/src/main/res/anim/rotate_close.xml | 7 + app/src/main/res/anim/rotate_open.xml | 7 + app/src/main/res/drawable/add_image.xml | 9 + app/src/main/res/drawable/document_add.xml | 9 + app/src/main/res/drawable/ic_add.xml | 10 + app/src/main/res/drawable/ic_audio.xml | 5 +- app/src/main/res/drawable/ic_back.xml | 12 + app/src/main/res/drawable/ic_close.xml | 10 + .../res/drawable/ic_create_new_folder.xml | 10 + app/src/main/res/drawable/ic_delete.xml | 10 + app/src/main/res/drawable/ic_document.xml | 5 +- app/src/main/res/drawable/ic_folder.xml | 10 + app/src/main/res/drawable/ic_folder_add.xml | 13 + app/src/main/res/drawable/ic_image.xml | 3 +- app/src/main/res/drawable/ic_no_items.xml | 10 + app/src/main/res/drawable/ic_video.xml | 10 + app/src/main/res/drawable/music_add.xml | 41 ++ app/src/main/res/drawable/play.xml | 2 +- app/src/main/res/drawable/video_add.xml | 9 + app/src/main/res/drawable/wrong.xml | 9 + .../res/layout/activity_audio_gallery.xml | 10 - ...ity_documents.xml => activity_folders.xml} | 6 +- app/src/main/res/layout/activity_gallery.xml | 163 +++++-- app/src/main/res/layout/activity_hidden.xml | 156 +++++++ .../res/layout/activity_image_gallery.xml | 10 - .../res/layout/activity_video_gallery.xml | 10 - app/src/main/res/layout/dialog_input.xml | 22 + app/src/main/res/layout/item_folder.xml | 48 ++ app/src/main/res/layout/list_item_file.xml | 60 +++ app/src/main/res/layout/list_item_folder.xml | 15 + app/src/main/res/values/strings.xml | 25 ++ 51 files changed, 1671 insertions(+), 895 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml delete mode 100644 app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt delete mode 100644 app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt delete mode 100644 app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt create mode 100644 app/src/main/java/devs/org/calculator/activities/HiddenActivity.kt delete mode 100644 app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt delete mode 100644 app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt create mode 100644 app/src/main/java/devs/org/calculator/adapters/FileDiffCallback.kt create mode 100644 app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt create mode 100644 app/src/main/java/devs/org/calculator/utils/FolderManager.kt create mode 100644 app/src/main/res/anim/fab_close.xml create mode 100644 app/src/main/res/anim/fab_open.xml create mode 100644 app/src/main/res/anim/rotate_close.xml create mode 100644 app/src/main/res/anim/rotate_open.xml create mode 100644 app/src/main/res/drawable/add_image.xml create mode 100644 app/src/main/res/drawable/document_add.xml create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_back.xml create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_create_new_folder.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_folder.xml create mode 100644 app/src/main/res/drawable/ic_folder_add.xml create mode 100644 app/src/main/res/drawable/ic_no_items.xml create mode 100644 app/src/main/res/drawable/ic_video.xml create mode 100644 app/src/main/res/drawable/music_add.xml create mode 100644 app/src/main/res/drawable/video_add.xml create mode 100644 app/src/main/res/drawable/wrong.xml delete mode 100644 app/src/main/res/layout/activity_audio_gallery.xml rename app/src/main/res/layout/{activity_documents.xml => activity_folders.xml} (51%) create mode 100644 app/src/main/res/layout/activity_hidden.xml delete mode 100644 app/src/main/res/layout/activity_image_gallery.xml delete mode 100644 app/src/main/res/layout/activity_video_gallery.xml create mode 100644 app/src/main/res/layout/dialog_input.xml create mode 100644 app/src/main/res/layout/item_folder.xml create mode 100644 app/src/main/res/layout/list_item_file.xml create mode 100644 app/src/main/res/layout/list_item_folder.xml 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