diff --git a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt index 0af1f16..f8105ca 100644 --- a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt @@ -6,13 +6,17 @@ 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.utils.FileManager +import devs.org.calculator.utils.FileProcessCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File -class AudioGalleryActivity : BaseGalleryActivity() { +class AudioGalleryActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.AUDIO private lateinit var pickAudioLauncher: ActivityResultLauncher - private var selectedUri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,32 +24,43 @@ class AudioGalleryActivity : BaseGalleryActivity() { pickAudioLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - val uri = result.data?.data - if (uri != null) { - selectedUri = uri - try { - val file = fileManager.copyFileToHiddenDir(selectedUri!!, fileType) - if (file != null && file.exists()) { - Toast.makeText(this, "File hidden successfully", Toast.LENGTH_SHORT).show() - loadFiles() - } else { - Toast.makeText(this, "Failed to hide file", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + 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 { - Toast.makeText(this, "No audio selected", Toast.LENGTH_SHORT).show() + result.data?.data?.let { uriList.add(it) } // Single file selected + } + + if (uriList.isNotEmpty()) { + lifecycleScope.launch { + FileManager(this@AudioGalleryActivity, this@AudioGalleryActivity).processMultipleFiles(uriList, fileType,this@AudioGalleryActivity ) + } + } else { + Toast.makeText(this, "No files selected", Toast.LENGTH_SHORT).show() } } } } + override fun onFilesProcessedSuccessfully(copiedFiles: List) { + Toast.makeText(this@AudioGalleryActivity, "${copiedFiles.size} Audios hidden 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) @@ -57,4 +72,6 @@ class AudioGalleryActivity : BaseGalleryActivity() { override fun openPreview() { // Implement 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 index 69fe197..f82d5e8 100644 --- a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt @@ -22,21 +22,33 @@ abstract class BaseGalleryActivity : AppCompatActivity() { protected lateinit var binding: ActivityGalleryBinding protected lateinit var fileManager: FileManager protected lateinit var adapter: FileAdapter + protected 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 { + // Handle permission denial case + showPermissionDeniedDialog() + } + } abstract val fileType: FileManager.FileType override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupIntentSenderLauncher() - checkPermissions() binding = ActivityGalleryBinding.inflate(layoutInflater) setContentView(binding.root) fileManager = FileManager(this, this) + setupRecyclerView() - loadFiles() + checkPermissionsAndLoadFiles() } private fun setupIntentSenderLauncher() { @@ -51,52 +63,55 @@ abstract class BaseGalleryActivity : AppCompatActivity() { private fun setupRecyclerView() { binding.recyclerView.layoutManager = GridLayoutManager(this, 3) - adapter = FileAdapter( - fileType, - this, this - ) + adapter = FileAdapter(fileType, this, this) binding.recyclerView.adapter = adapter } - protected fun loadFiles() { - val files = fileManager.getFilesInHiddenDir(fileType) + 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) - adapter.notifyDataSetChanged() + } + + override fun onResume() { + super.onResume() + loadFiles() } abstract fun openPreview() - private fun checkPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - intent.addCategory("android.intent.category.DEFAULT") - intent.data = Uri.parse("package:${applicationContext.packageName}") - startActivityForResult(intent, 2296) - } - } else { - if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || - checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions( - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ), - 1001 - ) - } - } + private fun showPermissionDeniedDialog() { + // Show a dialog or a message informing the user about the importance of permissions } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == 2296) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (Environment.isExternalStorageManager()) { - // Permission granted - loadFiles() - } + if (requestCode == 2296 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Environment.isExternalStorageManager()) { + loadFiles() } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt b/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt index 149d78c..28e700a 100644 --- a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt @@ -6,51 +6,69 @@ 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.activities.AudioGalleryActivity import devs.org.calculator.utils.FileManager +import devs.org.calculator.utils.FileProcessCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File -class DocumentsActivity : BaseGalleryActivity() { +class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.DOCUMENT - private lateinit var pickDocumentLauncher: ActivityResultLauncher + private lateinit var pickLauncher: ActivityResultLauncher private var selectedUri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupFabButton() - pickDocumentLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - val uri = result.data?.data - if (uri != null) { - selectedUri = uri - try { - val file = fileManager.copyFileToHiddenDir(selectedUri!!, fileType) - if (file != null && file.exists()) { - Toast.makeText(this, "File hidden successfully", Toast.LENGTH_SHORT).show() - loadFiles() - } else { - Toast.makeText(this, "Failed to hide file", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + 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 { - Toast.makeText(this, "No document selected", Toast.LENGTH_SHORT).show() + 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, "No files selected", Toast.LENGTH_SHORT).show() } } } } + override fun onFilesProcessedSuccessfully(copiedFiles: List) { + Toast.makeText(this@DocumentsActivity, "${copiedFiles.size} Documents hidden successfully", Toast.LENGTH_SHORT).show() + loadFiles() + } + + override fun onFileProcessFailed() { + Toast.makeText(this@DocumentsActivity, "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) } - pickDocumentLauncher.launch(intent) + pickLauncher.launch(intent) } } diff --git a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt index 6786395..effcfd3 100644 --- a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt @@ -1,28 +1,33 @@ package devs.org.calculator.activities -import android.app.RecoverableSecurityException 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.MediaStore 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.documentfile.provider.DocumentFile +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import devs.org.calculator.utils.FileManager import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import android.Manifest +import devs.org.calculator.activities.AudioGalleryActivity +import devs.org.calculator.utils.FileProcessCallback -class ImageGalleryActivity : BaseGalleryActivity() { +class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.IMAGE + private val STORAGE_PERMISSION_CODE = 100 private lateinit var intentSenderLauncher: ActivityResultLauncher - private var selectedImageUri: Uri? = null private lateinit var pickImageLauncher: ActivityResultLauncher override fun onCreate(savedInstanceState: Bundle?) { @@ -32,36 +37,48 @@ class ImageGalleryActivity : BaseGalleryActivity() { intentSenderLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){ if (it.resultCode == RESULT_OK){ - Toast.makeText(this, "Photo Deleted Successfully", Toast.LENGTH_SHORT).show() +// Toast.makeText(this, "Photo Deleted Successfully", Toast.LENGTH_SHORT).show() }else{ - Toast.makeText(this, "Failed to delete photo", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Failed to hide/unhide photo", Toast.LENGTH_SHORT).show() } } pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - val uri = result.data?.data - if (uri != null) { - selectedImageUri = uri - try { - val file = fileManager.copyFileToHiddenDir(selectedImageUri!!, fileType) - if (file != null && file.exists()) { - Toast.makeText(this, "File hidden successfully", Toast.LENGTH_SHORT).show() - loadFiles() - } else { - Toast.makeText(this, "Failed to hide file", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + 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 { - Toast.makeText(this, "No image selected", Toast.LENGTH_SHORT).show() + result.data?.data?.let { uriList.add(it) } // Single file selected + } + + if (uriList.isNotEmpty()) { + lifecycleScope.launch { + FileManager(this@ImageGalleryActivity, this@ImageGalleryActivity) + .processMultipleFiles(uriList, fileType,this@ImageGalleryActivity ) + } + } else { + Toast.makeText(this, "No files selected", Toast.LENGTH_SHORT).show() } } } askPermissiom() } + override fun onFilesProcessedSuccessfully(copiedFiles: List) { + Toast.makeText(this@ImageGalleryActivity, "${copiedFiles.size} Images hidden successfully", Toast.LENGTH_SHORT).show() + loadFiles() + } + + override fun onFileProcessFailed() { + Toast.makeText(this@ImageGalleryActivity, "Failed to hide images", Toast.LENGTH_SHORT).show() + } + private fun setupIntentSenderLauncher() { intentSenderLauncher = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() @@ -80,7 +97,45 @@ class ImageGalleryActivity : BaseGalleryActivity() { } } else { - Toast.makeText(this, "Android Version Lower", Toast.LENGTH_SHORT).show() + 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, "Storage permissions granted", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "Storage permissions denied", Toast.LENGTH_SHORT).show() + } } } @@ -89,6 +144,7 @@ class ImageGalleryActivity : BaseGalleryActivity() { 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) 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 61f97c4..a911ecc 100644 --- a/app/src/main/java/devs/org/calculator/activities/MainActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/MainActivity.kt @@ -57,6 +57,7 @@ class MainActivity : AppCompatActivity() { binding.btnDot.setOnClickListener { addDecimal() } binding.btnEquals.setOnClickListener { calculateResult() } binding.btnPercent.setOnClickListener { calculatePercentage() } + binding.cut.setOnClickListener { cutNumbers() } } private fun launchBaseDirectoryPicker() { @@ -162,11 +163,23 @@ class MainActivity : AppCompatActivity() { hasDecimal = currentExpression.contains(".") updateDisplay() } catch (e: Exception) { - binding.display.text = "Error" + binding.display.text = "Invalid Value" } } private fun updateDisplay() { binding.display.text = currentExpression } + private fun cutNumbers() { + if (currentExpression.isNotEmpty()){ + if (currentExpression.length == 1){ + currentExpression = currentExpression.substring(0, currentExpression.length - 1) + currentExpression = "0" + }else currentExpression = currentExpression.substring(0, currentExpression.length - 1) + }else currentExpression = "0" + updateDisplay() + + } } + + 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 d86f35b..458764f 100644 --- a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt @@ -1,14 +1,15 @@ package devs.org.calculator.activities - import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope 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 kotlinx.coroutines.launch import java.io.File class PreviewActivity : AppCompatActivity() { @@ -29,70 +30,102 @@ class PreviewActivity : AppCompatActivity() { fileManager = FileManager(this, this) currentPosition = intent.getIntExtra("position", 0) - - type = intent.getStringExtra("type").toString() - clickListeners() - - when(type){ - "IMAGE" ->{ - filetype = FileManager.FileType.IMAGE - binding.title.text = "Preview Images" - } - - "VIDEO" ->{ - filetype = FileManager.FileType.VIDEO - binding.title.text = "Preview Videos" - } - - "AUDIO" ->{ - filetype = FileManager.FileType.AUDIO - binding.title.text = "Preview Audios" - } - - else -> { - filetype = FileManager.FileType.DOCUMENT - binding.title.text = "Preview Docomnts" - } - } + setupFileType() files = fileManager.getFilesInHiddenDir(filetype) setupImagePreview() + clickListeners() } - 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!! - ) + private fun setupFileType() { + when (type) { + "IMAGE" -> { + filetype = FileManager.FileType.IMAGE + binding.title.text = "Preview Images" + } + "VIDEO" -> { + filetype = FileManager.FileType.VIDEO + binding.title.text = "Preview Videos" + } + "AUDIO" -> { + filetype = FileManager.FileType.AUDIO + binding.title.text = "Preview Audios" + } + else -> { + filetype = FileManager.FileType.DOCUMENT + binding.title.text = "Preview Documents" } - } - 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) + adapter = ImagePreviewAdapter(this, filetype) + adapter.images = files // Set initial data binding.viewPager.adapter = adapter - val fileUri = Uri.fromFile(files[currentPosition]) - val filesName = FileManager.FileName(this).getFileNameFromUri(fileUri!!).toString() binding.viewPager.setCurrentItem(currentPosition, false) + + val fileUri = Uri.fromFile(files[currentPosition]) + val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri).toString() + binding.title.text = fileName + } + + private fun clickListeners() { + binding.delete.setOnClickListener { + val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem]) + if (fileUri != null) { + MaterialAlertDialogBuilder(this) + .setTitle("Delete File") + .setMessage("Are you sure you want to Delete this file?") + .setPositiveButton("Delete") { dialog, _ -> + lifecycleScope.launch { + FileManager(this@PreviewActivity, this@PreviewActivity).deletePhotoFromExternalStorage(fileUri) + removeFileFromList(binding.viewPager.currentItem) + } + dialog.dismiss() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .show() + } + } + + binding.unHide.setOnClickListener { + val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem]) + if (fileUri != null) { + MaterialAlertDialogBuilder(this) + .setTitle("Unhide File") + .setMessage("Are you sure you want to Unhide this file?") + .setPositiveButton("Unhide") { dialog, _ -> + lifecycleScope.launch { + FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri) + removeFileFromList(binding.viewPager.currentItem) + } + dialog.dismiss() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .show() + } + } + } + + private fun removeFileFromList(position: Int) { + val updatedFiles = files.toMutableList().apply { removeAt(position) } + files = updatedFiles + adapter.images = updatedFiles // Update adapter with the new list + + // Update the ViewPager's position + if (!updatedFiles.isNotEmpty()) finish() + } override fun onSupportNavigateUp(): Boolean { onBackPressed() return true } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt index 6de8b31..3f4680b 100644 --- a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt @@ -6,12 +6,18 @@ 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.activities.ImageGalleryActivity import devs.org.calculator.utils.FileManager +import devs.org.calculator.utils.FileProcessCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File -class VideoGalleryActivity : BaseGalleryActivity() { +class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.VIDEO - private lateinit var pickVideoLauncher: ActivityResultLauncher + private lateinit var pickLauncher: ActivityResultLauncher private var selectedUri: Uri? = null @@ -19,39 +25,52 @@ class VideoGalleryActivity : BaseGalleryActivity() { super.onCreate(savedInstanceState) setupFabButton() - pickVideoLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - val uri = result.data?.data - if (uri != null) { - selectedUri = uri - try { - val file = fileManager.copyFileToHiddenDir(selectedUri!!, fileType) - if (file != null && file.exists()) { - Toast.makeText(this, "File hidden successfully", Toast.LENGTH_SHORT).show() - loadFiles() - } else { - Toast.makeText(this, "Failed to hide file", Toast.LENGTH_SHORT).show() - } - } catch (e: Exception) { - Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + 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 { - Toast.makeText(this, "No video selected", Toast.LENGTH_SHORT).show() + 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, "No files selected", Toast.LENGTH_SHORT).show() } } } } + override fun onFilesProcessedSuccessfully(copiedFiles: List) { + Toast.makeText(this@VideoGalleryActivity, "${copiedFiles.size} Videos hidden successfully", Toast.LENGTH_SHORT).show() + loadFiles() + } + + override fun onFileProcessFailed() { + Toast.makeText(this@VideoGalleryActivity, "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) } - pickVideoLauncher.launch(intent) + pickLauncher.launch(intent) } } 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 2331a59..44123cb 100644 --- a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt +++ b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt @@ -86,7 +86,7 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte MaterialAlertDialogBuilder(context) .setTitle("Details") - .setMessage("File Name: $filesName\\n\\nYou can delete or unghide this file\", \"Delete") + .setMessage("File Name: $filesName\n\nYou can delete or Unhide this file.") .setPositiveButton("Delete") { dialog, _ -> // Handle positive button click lifecycleOwner.lifecycleScope.launch{ @@ -100,7 +100,11 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte } .setNegativeButton("Unhide") { dialog, _ -> // Handle negative button click - + FileManager(context, context as LifecycleOwner).copyFileToNormalDir(fileUri) + val currentList = currentList.toMutableList() + currentList.remove(file) + submitList(currentList) + dialog.dismiss() dialog.dismiss() } .show() @@ -110,11 +114,6 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte } } - fun reloadList(file: File){ - val currentList = currentList.toMutableList() - currentList.remove(file) - submitList(currentList) - } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { 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 b261c8b..f860353 100644 --- a/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt +++ b/app/src/main/java/devs/org/calculator/adapters/ImagePreviewAdapter.kt @@ -6,28 +6,35 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.MediaController +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 class ImagePreviewAdapter( private val context: Context, - private val images: List, private var fileType: FileManager.FileType ) : RecyclerView.Adapter() { + // Use AsyncListDiffer for managing the list + private val differ = AsyncListDiffer(this, FileDiffCallback()) + + // Expose data management through differ + var images: List + get() = differ.currentList + set(value) = differ.submitList(value) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { - val binding = ViewpagerItemsBinding.inflate(LayoutInflater.from(context), parent, false - ) + val binding = ViewpagerItemsBinding.inflate(LayoutInflater.from(context), parent, false) return ImageViewHolder(binding) } override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val imageUrl = images[position] holder.bind(imageUrl) - } override fun getItemCount(): Int = images.size @@ -39,31 +46,27 @@ class ImagePreviewAdapter( binding.imageView.visibility = View.GONE binding.videoView.visibility = View.VISIBLE - // Set up the VideoView with the current video file val videoUri = Uri.fromFile(file) binding.videoView.setVideoURI(videoUri) binding.videoView.start() - // Create and attach MediaController val mediaController = MediaController(context) mediaController.setAnchorView(binding.videoView) binding.videoView.setMediaController(mediaController) - // Handle the "Next" button logic mediaController.setPrevNextListeners( - { // Next button clicked - val nextPosition = (adapterPosition + 1) % images.size // Loop to start if last + { + val nextPosition = (adapterPosition + 1) % images.size playVideoAtPosition(nextPosition) }, - { // Previous button clicked + { val prevPosition = if (adapterPosition - 1 < 0) images.size - 1 else adapterPosition - 1 playVideoAtPosition(prevPosition) } ) - // Play next video automatically when the current one finishes binding.videoView.setOnCompletionListener { - val nextPosition = (adapterPosition + 1) % images.size // Loop to start if last + val nextPosition = (adapterPosition + 1) % images.size playVideoAtPosition(nextPosition) } } @@ -92,7 +95,5 @@ class ImagePreviewAdapter( } } } - - - } + 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 627e063..062b4ce 100644 --- a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt +++ b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt @@ -74,7 +74,7 @@ class DialogUtil(private val context: Context, private var lifecycleOwner: Lifec val deleted = documentFile.delete() withContext(Dispatchers.Main) { if (deleted) { - Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() +// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show() } 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 0c0207d..51dab0f 100644 --- a/app/src/main/java/devs/org/calculator/utils/FileManager.kt +++ b/app/src/main/java/devs/org/calculator/utils/FileManager.kt @@ -6,6 +6,8 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Environment +import android.provider.ContactsContract +import android.provider.ContactsContract.Directory import android.provider.DocumentsContract import android.provider.MediaStore import android.webkit.MimeTypeMap @@ -15,6 +17,7 @@ import androidx.activity.result.IntentSenderRequest import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import devs.org.calculator.activities.VideoGalleryActivity import devs.org.calculator.adapters.FileAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -95,6 +98,46 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life null } } + fun copyFileToNormalDir(uri: Uri): File? { + return try { + val contentResolver = context.contentResolver + + // Get the target directory + val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + targetDir.mkdirs() + + // Create target file + val mimeType = contentResolver.getType(uri) + val extension = MimeTypeMap.getSingleton() + .getExtensionFromMimeType(mimeType) ?: "" + val fileName = "${System.currentTimeMillis()}.${extension}" + val targetFile = File(targetDir, fileName) + + // Copy file using DocumentFile + contentResolver.openInputStream(uri)?.use { input -> + targetFile.outputStream().use { output -> + input.copyTo(output) + } + } + + // Verify copy success + if (!targetFile.exists() || targetFile.length() == 0L) { + throw Exception("File copy failed") + } + + // Media scan the new file to hide it + val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) + mediaScanIntent.data = Uri.fromFile(targetDir) + context.sendBroadcast(mediaScanIntent) + lifecycleOwner.lifecycleScope.launch { + deletePhotoFromExternalStorage(uri) + } + targetFile + } catch (e: Exception) { + e.printStackTrace() + null + } + } @@ -108,9 +151,9 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life val deleted = documentFile.delete() withContext(Dispatchers.Main) { if (deleted) { - Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() +// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() } else { - Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Failed to hide/unhide file", Toast.LENGTH_SHORT).show() } } return@withContext @@ -120,7 +163,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life try { context.contentResolver.delete(photoUri, null, null) withContext(Dispatchers.Main) { - Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() +// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show() } } catch (e: SecurityException) { // Handle security exception for Android 10 and above @@ -145,7 +188,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life withContext(Dispatchers.Main) { Toast.makeText( context, - "Error deleting file: ${e.message}", + "Error hiding/unhiding file: ${e.message}", Toast.LENGTH_LONG ).show() } @@ -196,6 +239,31 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life } + suspend fun processMultipleFiles( + uriList: List, + fileType: FileType, + callback: FileProcessCallback + ) { + withContext(Dispatchers.IO) { + val copiedFiles = mutableListOf() + for (uri in uriList) { + try { + val file = copyFileToHiddenDir(uri, fileType) + file?.let { copiedFiles.add(it) } + } catch (e: Exception) { + e.printStackTrace() + } + } + withContext(Dispatchers.Main) { + if (copiedFiles.isNotEmpty()) { + callback.onFilesProcessedSuccessfully(copiedFiles) + } else { + callback.onFileProcessFailed() + } + } + } + } + enum class FileType(val dirName: String) { diff --git a/app/src/main/java/devs/org/calculator/utils/FileProcessCallback.kt b/app/src/main/java/devs/org/calculator/utils/FileProcessCallback.kt new file mode 100644 index 0000000..0462a3a --- /dev/null +++ b/app/src/main/java/devs/org/calculator/utils/FileProcessCallback.kt @@ -0,0 +1,8 @@ +package devs.org.calculator.utils + +import java.io.File + +interface FileProcessCallback { + fun onFilesProcessedSuccessfully(copiedFiles: List) + fun onFileProcessFailed() +} diff --git a/app/src/main/res/drawable/backspace.xml b/app/src/main/res/drawable/backspace.xml new file mode 100644 index 0000000..7145fa9 --- /dev/null +++ b/app/src/main/res/drawable/backspace.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index acecba5..5e82a64 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -47,18 +47,6 @@ app:cornerRadius="15dp" style="@style/Widget.MaterialComponents.Button.OutlinedButton"/> - - + style="@style/Widget.MaterialComponents.Button.OutlinedButton"/> + + + #FF000000 + #FFFFFFFF + + + @android:color/system_accent1_600 + @android:color/system_accent1_0 + @android:color/system_accent1_100 + @android:color/system_accent1_900 + + @android:color/system_accent2_600 + @android:color/system_accent2_0 + @android:color/system_accent2_100 + @android:color/system_accent2_900 + + @android:color/system_neutral1_50 + @android:color/system_neutral1_900 + #ffffff + \ No newline at end of file diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index b7235a5..0000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml deleted file mode 100644 index 48244b8..0000000 --- a/app/src/main/res/values-v31/themes.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9a3c1bf..b41dde7 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -16,4 +16,5 @@ @android:color/system_neutral1_50 @android:color/system_neutral1_900 + #000000 \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f5b3a26..0962bc9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,6 +5,7 @@ @android:color/system_accent1_600 @android:color/system_accent1_700 @color/white + @font/ubuntu_regular