diff --git a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt index aaef6e0..69fe197 100644 --- a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt @@ -53,7 +53,7 @@ abstract class BaseGalleryActivity : AppCompatActivity() { binding.recyclerView.layoutManager = GridLayoutManager(this, 3) adapter = FileAdapter( fileType, - this + this, this ) binding.recyclerView.adapter = adapter } @@ -61,6 +61,7 @@ abstract class BaseGalleryActivity : AppCompatActivity() { protected fun loadFiles() { val files = fileManager.getFilesInHiddenDir(fileType) adapter.submitList(files) + adapter.notifyDataSetChanged() } abstract fun openPreview() 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 1c40cd6..d86f35b 100644 --- a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt @@ -1,10 +1,13 @@ package devs.org.calculator.activities +import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder import devs.org.calculator.adapters.ImagePreviewAdapter import devs.org.calculator.databinding.ActivityPreviewBinding +import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.FileManager import java.io.File @@ -27,23 +30,30 @@ class PreviewActivity : AppCompatActivity() { currentPosition = intent.getIntExtra("position", 0) + type = intent.getStringExtra("type").toString() - filetype = when(type){ + clickListeners() + + when(type){ "IMAGE" ->{ - FileManager.FileType.IMAGE + filetype = FileManager.FileType.IMAGE + binding.title.text = "Preview Images" } "VIDEO" ->{ - FileManager.FileType.VIDEO + filetype = FileManager.FileType.VIDEO + binding.title.text = "Preview Videos" } "AUDIO" ->{ - FileManager.FileType.AUDIO + filetype = FileManager.FileType.AUDIO + binding.title.text = "Preview Audios" } else -> { - FileManager.FileType.DOCUMENT + filetype = FileManager.FileType.DOCUMENT + binding.title.text = "Preview Docomnts" } } files = fileManager.getFilesInHiddenDir(filetype) @@ -51,10 +61,31 @@ class PreviewActivity : AppCompatActivity() { setupImagePreview() } + private fun clickListeners() { + binding.delete.setOnClickListener{ + var fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem]) + if (fileUri != null) { + DialogUtil(this, this).showMaterialDialog( + "Delete File", + "Are you sure you want to delete this file ?", + "Delete", + "Cancel", + fileUri!! + ) + } + } + binding.unHide.setOnClickListener{ + DialogUtil(this, this).showMaterialDialog("Unhide File","Are you sure you want to unhide this file ?", "Unhide", "Cancel") + + } + } + private fun setupImagePreview() { adapter = ImagePreviewAdapter(this, files,filetype) binding.viewPager.adapter = adapter + val fileUri = Uri.fromFile(files[currentPosition]) + val filesName = FileManager.FileName(this).getFileNameFromUri(fileUri!!).toString() binding.viewPager.setCurrentItem(currentPosition, false) } @@ -62,4 +93,6 @@ class PreviewActivity : AppCompatActivity() { onBackPressed() return true } + + } \ 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 ea93512..2331a59 100644 --- a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt +++ b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt @@ -2,21 +2,28 @@ package devs.org.calculator.adapters 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.ImageView +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 com.google.android.material.dialog.MaterialAlertDialogBuilder import devs.org.calculator.R +import devs.org.calculator.activities.BaseGalleryActivity import devs.org.calculator.activities.PreviewActivity +import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.FileManager -import kotlinx.coroutines.NonDisposableHandle.parent +import kotlinx.coroutines.launch import java.io.File +import kotlin.collections.remove -class FileAdapter(private val fileType: FileManager.FileType, var context: Context) : +class FileAdapter(private val fileType: FileManager.FileType, var context: Context, private var lifecycleOwner: LifecycleOwner) : ListAdapter(FileDiffCallback()) { private val selectedItems = mutableSetOf() @@ -72,8 +79,43 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte context.startActivity(intent) } + itemView.setOnLongClickListener{ + + val fileUri = FileManager.FileManager().getContentUri(context, file) + val filesName = FileManager.FileName(context).getFileNameFromUri(fileUri!!).toString() + + MaterialAlertDialogBuilder(context) + .setTitle("Details") + .setMessage("File Name: $filesName\\n\\nYou can delete or unghide this file\", \"Delete") + .setPositiveButton("Delete") { dialog, _ -> + // Handle positive button click + lifecycleOwner.lifecycleScope.launch{ + FileManager(context, context as LifecycleOwner).deletePhotoFromExternalStorage(fileUri) + } + + val currentList = currentList.toMutableList() + currentList.remove(file) + submitList(currentList) + dialog.dismiss() + } + .setNegativeButton("Unhide") { dialog, _ -> + // Handle negative button click + + dialog.dismiss() + } + .show() + + return@setOnLongClickListener true + } + } } + fun reloadList(file: File){ + val currentList = currentList.toMutableList() + currentList.remove(file) + submitList(currentList) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { val view = LayoutInflater.from(parent.context) diff --git a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt new file mode 100644 index 0000000..627e063 --- /dev/null +++ b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt @@ -0,0 +1,121 @@ +package devs.org.calculator.utils + +import android.app.RecoverableSecurityException +import android.content.Context +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class DialogUtil(private val context: Context, private var lifecycleOwner: LifecycleOwner) { + private lateinit var intentSenderLauncher: ActivityResultLauncher + fun showMaterialDialog( + title: String, + message: String, + positiveButton: String, + negativeButton: String, + ) { + MaterialAlertDialogBuilder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(positiveButton) { dialog, _ -> + // Handle positive button click + dialog.dismiss() + } + .setNegativeButton(negativeButton) { dialog, _ -> + // Handle negative button click + dialog.dismiss() + } + .show() + } + fun showMaterialDialog( + title: String, + message: String, + positiveButton: String, + negativeButton: String, + uri: Uri + ) { + MaterialAlertDialogBuilder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(positiveButton) { dialog, _ -> + // Handle positive button click + if (positiveButton == "Delete") { + lifecycleOwner.lifecycleScope.launch { + deletePhotoFromExternalStorage(uri) + } + }else{ + // copy file to a visible directory + + } + } + .setNegativeButton(negativeButton) { dialog, _ -> + // Handle negative button click + dialog.dismiss() + } + .show() + } + + suspend fun deletePhotoFromExternalStorage(photoUri: Uri) { + withContext(Dispatchers.IO) { + try { + // First try to delete using DocumentFile + val documentFile = DocumentFile.fromSingleUri(context, photoUri) + if (documentFile?.exists() == true && documentFile.canWrite()) { + val deleted = documentFile.delete() + withContext(Dispatchers.Main) { + if (deleted) { + Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show() + } + } + return@withContext + } + + // If DocumentFile approach fails, try content resolver + try { + context.contentResolver.delete(photoUri, null, null) + withContext(Dispatchers.Main) { + Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() + } + } catch (e: SecurityException) { + // Handle security exception for Android 10 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val intentSender = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + MediaStore.createDeleteRequest(context.contentResolver, listOf(photoUri)).intentSender + } + else -> { + val recoverableSecurityException = e as? RecoverableSecurityException + recoverableSecurityException?.userAction?.actionIntent?.intentSender + } + } + intentSender?.let { sender -> + intentSenderLauncher.launch( + IntentSenderRequest.Builder(sender).build() + ) + } + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Toast.makeText( + context, + "Error deleting file: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + } + } + } +} \ 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 7266681..0c0207d 100644 --- a/app/src/main/java/devs/org/calculator/utils/FileManager.kt +++ b/app/src/main/java/devs/org/calculator/utils/FileManager.kt @@ -15,6 +15,7 @@ import androidx.activity.result.IntentSenderRequest import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import devs.org.calculator.adapters.FileAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -53,23 +54,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life } } - fun hideFile(uri: Uri, type: FileType): File { - val inputStream = context.contentResolver.openInputStream(uri) - val targetDir = File(getHiddenDirectory(), type.dirName) - targetDir.mkdirs() - - val fileName = "${System.currentTimeMillis()}_${uri.lastPathSegment}" - val targetFile = File(targetDir, fileName) - - inputStream?.use { input -> - targetFile.outputStream().use { output -> - input.copyTo(output) - } - } - - return targetFile - } - fun copyFileToHiddenDir(uri: Uri, type: FileType): File? { return try { val contentResolver = context.contentResolver @@ -112,7 +96,10 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life } } - private suspend fun deletePhotoFromExternalStorage(photoUri: Uri) { + + + + suspend fun deletePhotoFromExternalStorage(photoUri: Uri) { withContext(Dispatchers.IO) { try { // First try to delete using DocumentFile @@ -166,25 +153,50 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life } } - private fun deleteOriginalFile(uri: Uri) { - try { + + + class FileName(private val context: Context) { + fun getFileNameFromUri(uri: Uri): String? { val contentResolver = context.contentResolver - when { - DocumentsContract.isDocumentUri(context, uri) -> { - DocumentsContract.deleteDocument(contentResolver, uri) + var fileName: String? = null + + if (uri.scheme == "content") { + val cursor = contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + if (nameIndex != -1) { + fileName = it.getString(nameIndex) + } + } } - isMediaStoreUri(uri) -> { - contentResolver.delete(uri, null, null) + } else if (uri.scheme == "file") { + fileName = File(uri.path ?: "").name + } + + return fileName + } + + } + class FileManager(){ + fun getContentUri(context: Context, file: File): Uri? { + val projection = arrayOf(MediaStore.MediaColumns._ID) + val selection = "${MediaStore.MediaColumns.DATA} = ?" + val selectionArgs = arrayOf(file.absolutePath) + val queryUri = MediaStore.Files.getContentUri("external") + + context.contentResolver.query(queryUri, projection, selection, selectionArgs, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + return Uri.withAppendedPath(queryUri, id.toString()) } } - } catch (e: Exception) { - e.printStackTrace() + return null } } - private fun isMediaStoreUri(uri: Uri): Boolean { - return uri.authority?.startsWith("com.android.providers.media") == true - } + + enum class FileType(val dirName: String) { IMAGE(IMAGES_DIR), diff --git a/app/src/main/res/layout/activity_gallery.xml b/app/src/main/res/layout/activity_gallery.xml index d34ccb4..938a2ce 100644 --- a/app/src/main/res/layout/activity_gallery.xml +++ b/app/src/main/res/layout/activity_gallery.xml @@ -16,7 +16,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:layout_margin="16dp" + android:layout_marginEnd="25dp" + android:layout_marginBottom="35dp" android:contentDescription="Add file" android:src="@android:drawable/ic_input_add" /> diff --git a/app/src/main/res/layout/activity_gallery_base.xml b/app/src/main/res/layout/activity_gallery_base.xml index e8f010e..22e0082 100644 --- a/app/src/main/res/layout/activity_gallery_base.xml +++ b/app/src/main/res/layout/activity_gallery_base.xml @@ -16,7 +16,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" - android:layout_margin="16dp" + android:layout_marginEnd="25dp" + android:layout_marginBottom="35dp" android:src="@android:drawable/ic_input_add" /> \ No newline at end of file