Changes For Folder Feature

This commit is contained in:
Binondi
2025-06-01 21:37:13 +05:30
parent ab737511a7
commit f8575da2a9
17 changed files with 559 additions and 293 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
package devs.org.calculator.activities
import android.app.AlertDialog
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -9,29 +9,33 @@ import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.adapters.FolderAdapter
import devs.org.calculator.callbacks.DialogActionsCallback
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.databinding.ActivityHiddenBinding
import devs.org.calculator.databinding.ProccessingDialogBinding
import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
import devs.org.calculator.utils.FolderManager
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
class HiddenActivity : AppCompatActivity() {
@@ -45,19 +49,49 @@ class HiddenActivity : AppCompatActivity() {
private val fileManager = FileManager(this, this)
private val folderManager = FolderManager(this)
private val dialogUtil = DialogUtil(this)
private var customDialog: androidx.appcompat.app.AlertDialog? = null
private val STORAGE_PERMISSION_CODE = 101
private val PICK_FILE_REQUEST_CODE = 102
private var currentFolder: File? = null
private var folderAdapter: FolderAdapter? = null
val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
private var dialogShowTime: Long = 0
private val MINIMUM_DIALOG_DURATION = 1700L
private fun showCustomDialog(i: Int) {
val dialogView = ProccessingDialogBinding.inflate(layoutInflater)
customDialog = MaterialAlertDialogBuilder(this)
.setView(dialogView.root)
.setCancelable(false)
.create()
dialogView.title.text = "Hiding $i files"
customDialog?.show()
dialogShowTime = System.currentTimeMillis()
}
private fun dismissCustomDialog() {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - dialogShowTime
if (elapsedTime < MINIMUM_DIALOG_DURATION) {
val remainingTime = MINIMUM_DIALOG_DURATION - elapsedTime
Handler(Looper.getMainLooper()).postDelayed({
customDialog?.dismiss()
customDialog = null
}, remainingTime)
} else {
customDialog?.dismiss()
customDialog = null
}
}
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)
@@ -70,6 +104,7 @@ class HiddenActivity : AppCompatActivity() {
binding.addAudio.visibility = View.GONE
binding.addDocument.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE
binding.deleteSelected.visibility = View.GONE
binding.fabExpend.setOnClickListener {
if (isFabOpen) {
@@ -84,6 +119,13 @@ class HiddenActivity : AppCompatActivity() {
binding.addImage.setOnClickListener { openFilePicker("image/*") }
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
binding.addAudio.setOnClickListener { openFilePicker("audio/*") }
binding.back.setOnClickListener {
if (currentFolder != null) {
pressBack()
} else {
super.onBackPressed()
}
}
binding.addDocument.setOnClickListener { openFilePicker("*/*") }
binding.addFolder.setOnClickListener {
dialogUtil.createInputDialog(
@@ -101,50 +143,104 @@ class HiddenActivity : AppCompatActivity() {
fileManager.askPermission(this)
listFoldersInHiddenDirectory()
setupDeleteButton()
pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val clipData = result.data?.clipData
val uriList = mutableListOf<Uri>()
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()) {
showCustomDialog(uriList.size)
lifecycleScope.launch {
if (currentFolder != null){
FileManager(this@HiddenActivity, this@HiddenActivity)
.processMultipleFiles(uriList, currentFolder!!,
object : FileProcessCallback {
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
Toast.makeText(this@HiddenActivity, "${copiedFiles.size} ${getString(R.string.documents_hidden_successfully)}", Toast.LENGTH_SHORT).show()
openFolder(currentFolder!!)
dismissCustomDialog()
}
override fun onFileProcessFailed() {
Toast.makeText(this@HiddenActivity,
getString(R.string.failed_to_hide_files), Toast.LENGTH_SHORT).show()
dismissCustomDialog()
}
})
}else{
Toast.makeText(
this@HiddenActivity,
getString(R.string.there_was_a_problem_in_the_folder),
Toast.LENGTH_SHORT
).show()
dismissCustomDialog()
}
}
} else {
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
}
}
}
askPermissiom()
}
private fun askPermissiom() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
if (!Environment.isExternalStorageManager()){
val intent = Intent().setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
}
else {
checkAndRequestStoragePermission()
}
}
private fun checkAndRequestStoragePermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
STORAGE_PERMISSION_CODE
)
}
}
private fun openFilePicker(mimeType: String) {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = mimeType
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
startActivityForResult(intent, PICK_FILE_REQUEST_CODE)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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)
}
}
pickImageLauncher.launch(intent)
}
private fun listFoldersInHiddenDirectory() {
@@ -163,7 +259,16 @@ class HiddenActivity : AppCompatActivity() {
openFolder(clickedFolder)
},
onFolderLongClick = { folder ->
// go to selection mode
// Enter selection mode
binding.fabExpend.visibility = View.GONE
binding.addFolder.visibility = View.GONE
binding.deleteSelected.visibility = View.VISIBLE
},
onSelectionModeChanged = { isSelectionMode ->
if (!isSelectionMode) {
binding.deleteSelected.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE
}
}
)
binding.recyclerView.adapter = folderAdapter
@@ -191,6 +296,7 @@ class HiddenActivity : AppCompatActivity() {
// Read files in the clicked folder and update RecyclerView
val files = folderManager.getFilesInFolder(folder)
Log.d("HiddenActivity", "Found ${files.size} files in ${folder.name}")
binding.folderName.text = folder.name
if (files.isNotEmpty()) {
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
@@ -253,7 +359,7 @@ class HiddenActivity : AppCompatActivity() {
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
binding.addFolder.visibility = View.VISIBLE
isFabOpen = true
Handler(Looper.getMainLooper()).postDelayed({
@@ -279,31 +385,6 @@ class HiddenActivity : AppCompatActivity() {
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
@@ -318,102 +399,70 @@ class HiddenActivity : AppCompatActivity() {
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}")
private fun setupDeleteButton() {
binding.deleteSelected.setOnClickListener {
val selectedFolders = folderAdapter?.getSelectedItems() ?: emptyList()
if (selectedFolders.isNotEmpty()) {
dialogUtil.showMaterialDialog(
getString(R.string.delete_items),
getString(R.string.are_you_sure_you_want_to_delete_selected_items),
getString(R.string.delete),
getString(R.string.cancel),
object : DialogUtil.DialogCallback {
override fun onPositiveButtonClicked() {
var allDeleted = true
selectedFolders.forEach { folder ->
if (!folderManager.deleteFolder(folder)) {
allDeleted = false
}
}
if (allDeleted) {
Toast.makeText(this@HiddenActivity, getString(R.string.folder_deleted_successfully), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@HiddenActivity, getString(R.string.some_items_could_not_be_deleted), Toast.LENGTH_SHORT).show()
}
folderAdapter?.clearSelection()
binding.deleteSelected.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE
listFoldersInHiddenDirectory()
}
} 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)
override fun onNegativeButtonClicked() {
// Do nothing
}
override fun onNaturalButtonClicked() {
// Do nothing
}
}
)
}
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
}
}
private fun pressBack(){
currentFolder = null
if (isFabOpen) {
closeFabs()
}
if (folderAdapter != null) {
binding.recyclerView.adapter = folderAdapter
}
binding.folderName.text = getString(R.string.hidden_space)
listFoldersInHiddenDirectory()
binding.fabExpend.visibility = View.GONE
binding.addImage.visibility = View.GONE
binding.addVideo.visibility = View.GONE
binding.addAudio.visibility = View.GONE
binding.addDocument.visibility = View.GONE
binding.addFolder.visibility = View.VISIBLE
}
@Deprecated("This method has been deprecated in favor of using the\n {@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}.\n The OnBackPressedDispatcher controls how back button events are dispatched\n to one or more {@link OnBackPressedCallback} objects.")
override fun onBackPressed() {
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
pressBack()
} else {
super.onBackPressed()
}

View File

@@ -16,6 +16,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.R
import devs.org.calculator.activities.PreviewActivity
import devs.org.calculator.utils.FileManager
@@ -215,18 +216,20 @@ class FileAdapter(
private fun showFileOptionsDialog(file: File) {
val options = arrayOf(
context.getString(R.string.delete),
context.getString(R.string.un_hide),
context.getString(R.string.rename),
context.getString(R.string.delete),
context.getString(R.string.share)
)
AlertDialog.Builder(context)
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.file_options))
.setItems(options) { dialog, which ->
when (which) {
0 -> deleteFile(file)
0 -> unHideFile(file)
1 -> renameFile(file)
2 -> shareFile(file)
2 -> deleteFile(file)
3 -> shareFile(file)
}
dialog.dismiss()
}
@@ -234,6 +237,20 @@ class FileAdapter(
.show()
}
private fun unHideFile(file: File) {
FileManager(context, lifecycleOwner).unHideFile(
file = file,
onSuccess = {
fileOperationCallback?.onFileDeleted(file)
},
onError = { errorMessage ->
Toast.makeText(context, "Failed to unhide: $errorMessage", Toast.LENGTH_SHORT).show()
}
)
}
private fun deleteFile(file: File) {
if (file.delete()) {
fileOperationCallback?.onFileDeleted(file)
@@ -249,7 +266,7 @@ class FileAdapter(
selectAll()
}
AlertDialog.Builder(context)
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.rename_file))
.setView(inputEditText)
.setPositiveButton(context.getString(R.string.rename)) { dialog, _ ->

View File

@@ -3,6 +3,7 @@ package devs.org.calculator.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -12,21 +13,55 @@ import java.io.File
class FolderAdapter(
private val onFolderClick: (File) -> Unit,
private val onFolderLongClick: (File) -> Unit
private val onFolderLongClick: (File) -> Unit,
private val onSelectionModeChanged: (Boolean) -> Unit
) : ListAdapter<File, FolderAdapter.FolderViewHolder>(FolderDiffCallback()) {
class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
private val selectedItems = mutableSetOf<Int>()
private var isSelectionMode = false
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit) {
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
val selectedView: ImageView = itemView.findViewById(R.id.selected)
val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
folderNameTextView.text = folder.name
itemView.setOnClickListener { onFolderClick(folder) }
selectedView.visibility = if (isSelected) View.VISIBLE else View.GONE
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
itemView.setOnClickListener {
if (isSelectionMode) {
toggleSelection(adapterPosition)
} else {
onFolderClick(folder)
}
}
itemView.setOnLongClickListener {
onFolderLongClick(folder)
if (!isSelectionMode) {
isSelectionMode = true
onSelectionModeChanged(true)
onFolderLongClick(folder)
toggleSelection(adapterPosition)
}
true
}
}
private fun toggleSelection(position: Int) {
if (selectedItems.contains(position)) {
selectedItems.remove(position)
if (selectedItems.isEmpty()) {
isSelectionMode = false
onSelectionModeChanged(false)
}
} else {
selectedItems.add(position)
}
notifyItemChanged(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
@@ -36,7 +71,20 @@ class FolderAdapter(
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val folder = getItem(position)
holder.bind(folder, onFolderClick, onFolderLongClick)
holder.bind(folder, onFolderClick, onFolderLongClick, selectedItems.contains(position))
}
fun getSelectedItems(): List<File> {
return selectedItems.mapNotNull { position ->
if (position < itemCount) getItem(position) else null
}
}
fun clearSelection() {
selectedItems.clear()
isSelectionMode = false
onSelectionModeChanged(false)
notifyDataSetChanged()
}
private class FolderDiffCallback : DiffUtil.ItemCallback<File>() {
@@ -45,9 +93,7 @@ class FolderAdapter(
}
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
return oldItem.name == newItem.name &&
oldItem.lastModified() == newItem.lastModified() &&
oldItem.length() == newItem.length()
return oldItem.name == newItem.name
}
}
}

View File

@@ -25,6 +25,7 @@ import kotlinx.coroutines.withContext
import java.io.File
import android.Manifest
import androidx.core.content.FileProvider
import devs.org.calculator.R
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
@@ -72,14 +73,12 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
}
private fun copyFileToHiddenDir(uri: Uri, type: FileType, currentDir: File? = null): File? {
private fun copyFileToHiddenDir(uri: Uri, folderName: File, currentDir: File? = null): File? {
return try {
val contentResolver = context.contentResolver
// Get the target directory
val targetDir = currentDir ?: File(Environment.getExternalStorageDirectory(), "$HIDDEN_DIR/${type.dirName}")
targetDir.mkdirs()
File(targetDir, ".nomedia").createNewFile()
// Get the target directory (i am using the current opened folder as target folder)
val targetDir = folderName
// Create target file
val mimeType = contentResolver.getType(uri)
@@ -153,6 +152,64 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
null
}
}
fun unHideFile(file: File, onSuccess: (() -> Unit)? = null, onError: ((String) -> Unit)? = null) {
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
try {
// Create target directory (Downloads)
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
targetDir.mkdirs()
// Create target file with same name or timestamp
val targetFile = File(targetDir, file.name)
// If file with same name exists, add timestamp
val finalTargetFile = if (targetFile.exists()) {
val nameWithoutExt = file.nameWithoutExtension
val extension = file.extension
File(targetDir, "${nameWithoutExt}_${System.currentTimeMillis()}.${extension}")
} else {
targetFile
}
// Copy file content
file.copyTo(finalTargetFile, overwrite = false)
// Verify copy success
if (finalTargetFile.exists() && finalTargetFile.length() > 0) {
// Delete original hidden file
if (file.delete()) {
// Trigger media scan for the new file
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
context.sendBroadcast(mediaScanIntent)
withContext(Dispatchers.Main) {
Toast.makeText(context, context.getString(R.string.file_unhidden_successfully), Toast.LENGTH_SHORT).show()
onSuccess?.invoke() // Call success callback
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(context, "File copied but failed to remove from hidden folder", Toast.LENGTH_SHORT).show()
onError?.invoke("Failed to remove from hidden folder")
}
}
} else {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Failed to copy file", Toast.LENGTH_SHORT).show()
onError?.invoke("Failed to copy file")
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Error unhiding file: ${e.message}", Toast.LENGTH_LONG).show()
onError?.invoke(e.message ?: "Unknown error")
}
e.printStackTrace()
}
}
}
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
withContext(Dispatchers.IO) {
@@ -284,7 +341,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
suspend fun processMultipleFiles(
uriList: List<Uri>,
fileType: FileType,
fileType: File,
callback: FileProcessCallback,
currentDir: File? = null
) {

View File

@@ -24,13 +24,11 @@ class FolderManager(private val context: Context) {
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

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillAlpha="0.15" android:fillColor="#00363853" android:pathData="M17.88,18.6C17.88,19.926 16.806,21 15.48,21C14.155,21 13.08,19.926 13.08,18.6C13.08,17.274 14.155,16.2 15.48,16.2C16.806,16.2 17.88,17.274 17.88,18.6Z"/>
<path android:fillAlpha="0.15" android:fillColor="#00363853" android:pathData="M9.48,12C9.48,13.325 8.405,14.4 7.08,14.4C5.755,14.4 4.68,13.325 4.68,12C4.68,10.675 5.755,9.6 7.08,9.6C8.405,9.6 9.48,10.675 9.48,12Z"/>
<path android:fillAlpha="0.15" android:fillColor="#00363853" android:pathData="M17.88,5.4C17.88,6.725 16.806,7.8 15.48,7.8C14.155,7.8 13.08,6.725 13.08,5.4C13.08,4.075 14.155,3 15.48,3C16.806,3 17.88,4.075 17.88,5.4Z"/>
<path android:fillColor="#00000000" android:pathData="M18.48,18.537H21M4.68,12L3,12.044M4.68,12C4.68,13.325 5.755,14.4 7.08,14.4C8.405,14.4 9.48,13.325 9.48,12C9.48,10.675 8.405,9.6 7.08,9.6C5.755,9.6 4.68,10.675 4.68,12ZM10.169,12.044H21M12.801,5.551L3,5.551M21,5.551H18.48M3,18.537H12.801M17.88,18.6C17.88,19.926 16.806,21 15.48,21C14.155,21 13.08,19.926 13.08,18.6C13.08,17.274 14.155,16.2 15.48,16.2C16.806,16.2 17.88,17.274 17.88,18.6ZM17.88,5.4C17.88,6.725 16.806,7.8 15.48,7.8C14.155,7.8 13.08,6.725 13.08,5.4C13.08,4.075 14.155,3 15.48,3C16.806,3 17.88,4.075 17.88,5.4Z" android:strokeColor="@color/textColor" android:strokeLineCap="round" android:strokeWidth="1.5"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M512,512m-448,0a448,448 0,1 0,896 0,448 448,0 1,0 -896,0Z"
android:fillColor="#00ffffff"/>
<path
android:pathData="M738.1,311.5L448,601.6l-119.5,-119.5 -59.7,59.7 179.2,179.2 349.9,-349.9z"
android:fillColor="@color/textColor"/>
</vector>

View File

@@ -14,6 +14,8 @@
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp"
android:id="@+id/toolBar"
android:layout_marginTop="25dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -29,20 +31,29 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Folder Name"
android:text="@string/hidden_space"
android:textSize="18sp"
android:layout_weight="1"
android:id="@+id/folderName"/>
<ImageButton
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_setting"
android:scaleType="fitCenter"
android:background="#00000000"
android:padding="9dp"
android:id="@+id/settings"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toBottomOf="@+id/toolBar">
</androidx.recyclerview.widget.RecyclerView>
@@ -112,8 +123,8 @@
app:fabCustomSize="51dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/addDocument"
app:layout_constraintEnd_toEndOf="@+id/fabExpend"
app:layout_constraintStart_toStartOf="@+id/fabExpend" />
app:layout_constraintEnd_toEndOf="@+id/addDocument"
app:layout_constraintStart_toStartOf="@+id/addDocument" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/addDocument"
@@ -125,8 +136,8 @@
app:fabCustomSize="54dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/fabExpend"
app:layout_constraintEnd_toEndOf="@+id/addFolder"
app:layout_constraintStart_toStartOf="@+id/addFolder" />
app:layout_constraintEnd_toEndOf="@+id/fabExpend"
app:layout_constraintStart_toStartOf="@+id/fabExpend" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/addFolder"
@@ -138,8 +149,8 @@
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
app:fabCustomSize="57dp"
/>
app:fabCustomSize="57dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabExpend"
android:layout_width="wrap_content"
@@ -153,4 +164,16 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/deleteSelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_delete"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
app:fabCustomSize="57dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="8dp">
<devs.org.calculator.views.SquareImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"/>
</com.google.android.material.card.MaterialCardView>

View File

@@ -1,48 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_height="wrap_content">
<ImageView
android:id="@+id/folderIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:src="@drawable/ic_folder" />
<LinearLayout
android:id="@+id/selectedLayer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha="0.2"
android:background="?attr/colorControlNormal"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/folderName"
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textSize="14sp" />
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</LinearLayout>
<ImageView
android:id="@+id/folderIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:src="@drawable/ic_folder" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/folderName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textSize="14sp" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<ImageView
android:id="@+id/selected"
android:layout_width="35dp"
android:layout_height="35dp"
android:padding="3dp"
android:visibility="gone"
android:src="@drawable/selected"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:paddingVertical="15dp"
android:paddingHorizontal="10dp"
android:gravity="center_horizontal"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hiding_files"
android:padding="8dp"
android:textSize="20sp"/>
<com.airbnb.lottie.LottieAnimationView
android:layout_width="match_parent"
android:layout_height="200dp"
app:lottie_fileName="hiding_files.json"
app:lottie_autoPlay="true"
android:scaleX="1.2"
android:scaleY="1.2"
android:scaleType="centerCrop"
app:lottie_loop="true"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="25dp"
android:layout_marginBottom="15dp"
android:indeterminate="true"/>
</LinearLayout>

View File

@@ -8,27 +8,33 @@
android:layout_margin="4dp"
app:cardCornerRadius="8dp">
<com.jsibbold.zoomage.ZoomageView
android:id="@+id/imageView"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:zoomage_restrictBounds="false"
app:zoomage_animateOnReset="true"
app:zoomage_autoResetMode="UNDER"
app:zoomage_autoCenter="true"
android:visibility="gone"
app:zoomage_zoomable="true"
app:zoomage_translatable="true"
app:zoomage_minScale="0.6"
app:zoomage_maxScale="8"
/>
android:gravity="center"
android:orientation="vertical">
<com.jsibbold.zoomage.ZoomageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:zoomage_restrictBounds="false"
app:zoomage_animateOnReset="true"
app:zoomage_autoResetMode="UNDER"
app:zoomage_autoCenter="true"
android:visibility="gone"
app:zoomage_zoomable="true"
app:zoomage_translatable="true"
app:zoomage_minScale="0.6"
app:zoomage_maxScale="8"
/>
<VideoView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:id="@+id/videoView"/>
<VideoView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:visibility="gone"
android:layout_gravity="center"
android:id="@+id/videoView"/>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/audioBg"

View File

@@ -10,13 +10,9 @@
<string name="rename_file">Rename File</string>
<string name="share_file">Share File</string>
<string name="add_document">Add Document</string>
<string name="failed_to_hide_audio">Failed to hide Audios</string>
<string name="failed_to_hide_documents">Failed to hide Documents</string>
<string name="no_files_selected">No files selected</string>
<string name="documents_hidden_successfully"> Documents hidden successfully</string>
<string name="failed_to_hide_unhide_photo">Failed to hide/unhide photo</string>
<string name="images_hidden_successfully"> Images hidden successfully</string>
<string name="failed_to_hide_images">Failed to hide images</string>
<string name="documents_hidden_successfully">Files hidden successfully</string>
<string name="failed_to_hide_files">Failed to hide files</string>
<string name="storage_permissions_granted">Storage permissions granted</string>
<string name="storage_permissions_denied">Storage permissions denied</string>
<string name="preview_images">Preview Images</string>
@@ -47,37 +43,22 @@
<string name="answer_cannot_be_empty">Answer cannot be empty!</string>
<string name="password_successfully_reset">Password successfully reset.</string>
<string name="invalid_answer">Invalid answer!</string>
<string name="videos_hidden_successfully"> Videos hidden successfully</string>
<string name="failed_to_hide_videos">Failed to hide videos</string>
<string name="image">IMAGE</string>
<string name="video">VIDEO</string>
<string name="audio">AUDIO</string>
<string name="delete">Delete</string>
<string name="create">Create</string>
<string name="delete_folder">Delete Folder</string>
<string name="rename">Rename</string>
<string name="cannot_delete_folder">Cannot Delete Folder</string>
<string name="document">DOCUMENT</string>
<string name="no_audio_player_found">No audio player found!</string>
<string name="no_suitable_app_found_to_open_this_document">No suitable app found to open this document!</string>
<string name="unknown_file">Unknown File</string>
<string name="details"> DETAILS</string>
<string name="audio_hidded_successfully">Audios hidden successfully</string>
<string name="no_items_available_add_one_by_clicking_on_the_plus_button">No Items Available, Add one by clicking on the</string>
<string name="now_enter_button">Now Enter \'=\' button</string>
<string name="enter_123456">Enter 123456</string>
<string name="create_folder">Create Folder</string>
<string name="enter_folder_name">Enter folder name</string>
<string name="folder_already_exists">Folder already exists</string>
<string name="folder_options">Folder Options</string>
<string name="rename_folder">Rename Folder</string>
<string name="enter_new_folder_name">Enter new folder name</string>
<string name="failed_to_create_folder">Failed to create folder</string>
<string name="are_you_sure_you_want_to_delete_this_folder">Are you sure you want to delete this folder?</string>
<string name="yes">Yes</string>
<string name="error_loading_files">Error loading files</string>
<string name="delete_items">Delete Items</string>
<string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected items?</string>
<string name="items_deleted_successfully">Items deleted successfully</string>
<string name="now_enter_button">Now Enter \'=\' button</string>
<string name="delete_items">Delete Folder</string>
<string name="are_you_sure_you_want_to_delete_selected_items">Are you sure you want to delete selected Folders?</string>
<string name="folder_deleted_successfully">Folder deleted successfully</string>
<string name="some_items_could_not_be_deleted">Some items could not be deleted</string>
<string name="hidden_space">Hidden Space</string>
<string name="there_was_a_problem_in_the_folder">There was a problem in the Folder</string>
<string name="file_unhidden_successfully">File Will Now Show In Gallery</string>
</resources>