devfemibadmus / whatsapp-status-saver

Crafted using Flutter for a seamless UI/UX, backed by Kotlin for efficient performance. Our primary focus is on delivering fast responses, aesthetic appeal, and a lightweight user experience in this WhatsApp status saver app.
https://play.google.com/store/apps/details?id=com.blackstackhub.whatsappstatus
Apache License 2.0
7 stars 1 forks source link

failed action_open_document_tree now required #4

Closed devfemibadmus closed 4 months ago

devfemibadmus commented 4 months ago

we using manage external storage instead of other well tried action_open_document_tree but didn't work https://github.com/devfemibadmus/whatsapp-status-saver#bug-flutter-fileimagevideo-not-working-with-android-action_open_document_tree-but-works-fine-in-kotlin so i skipped but now playstore reject app and said we should use smth else instead not manage_external_storage

if you can fix this please feel free to fork this repo

below is other code i try with but seems i am not getting smth right, now i am leaving to contributors


    private fun getStatusFilesInfo(appType: String): List<Map<String, Any>> {
        val statusFilesInfo = mutableListOf<Map<String, Any>>()
        Log.d(TAG, "statusFilesInfo")
        Log.d(TAG, "appType: ${appType}")

        val baseDirectoryPath = when (appType) {
            "SAVED" -> getSavedStatusesPath()
            "WHATSAPP" -> getWhatsAppPath()
            "WHATSAPP4B" -> getWhatsApp4BPath()
            else -> null
        }
        /*
        baseDirectoryPath?.let { path ->
            if (!isPermissionGranted(path)) {
                Log.d(TAG, "perission ${path} new")
                requestSpecificFolderAccess(Uri.parse(path))
            }else{
                Log.d(TAG, "perission ${path} granted")
            }
            STATUS_DIRECTORY = Uri.parse(path)
        }
        Log.d(TAG, "${baseDirectoryPath}")
        Log.d(TAG, "${baseDirectoryPath}")
        */

        Log.d(TAG, "baseDirectoryPath: ${baseDirectoryPath}")
        Log.d(TAG, "STATUS_DIRECTORY: ${STATUS_DIRECTORY}")

        baseDirectoryPath?.let { path ->
            val dirFile = File(path)
            Log.e(TAG, "dirFile: ${dirFile}")
            Log.e(TAG, "dirFile: ${dirFile.exists()}")
            if (dirFile.exists() && dirFile.isDirectory && dirFile.listFiles(FileFilter{file ->file.isFile && file.canRead()}) != null) {
                Log.e(TAG, "dirFile: ${dirFile}")
                Log.e(TAG, "dirFile: ${dirFile.listFiles()}")
                val files = dirFile.listFiles(FileFilter{file ->file.isFile && file.canRead()})
                files?.forEach { file ->
                    Log.e(TAG, "file.absolutePath: ${file.absolutePath}")
                    val fileInfo = mutableMapOf<String, Any>()
                    fileInfo["name"] = file.name
                    fileInfo["path"] = file.absolutePath
                    fileInfo["size"] = file.length()
                    fileInfo["format"] = getFileFormat(file.name)
                    fileInfo["source"] = appType
                    fileInfo["mediaByte"] = ByteArray(0)
                    statusFilesInfo.add(fileInfo)
                }
            } else {
                Log.e(TAG, "Directory does not exist or is empty")
            }
        }
        /*

        STATUS_DIRECTORY?.let { directoryUri ->
            Log.d(TAG, "directoryUri ${directoryUri}")
            val dirFile = DocumentFile.fromTreeUri(context, directoryUri)
            if (dirFile?.exists() == true) {
                Log.e(TAG, "dirFile: ${dirFile}")
                Log.e(TAG, "dirFile: ${dirFile.exists()}")
                val files = dirFile.listFiles()
                files?.filter { !it.name.isNullOrBlank() }?.forEach { file ->
                    val fileInfo = mutableMapOf<String, Any>()
                    fileInfo["name"] = file.name ?: "UnknownFileName"
                    fileInfo["path"] = getFilePathFromUri(file.uri).toString() // file.uri.toString()
                    fileInfo["size"] = file.length()
                    fileInfo["format"] = getFileFormat(file.name ?: "UnknownFileName")
                    fileInfo["source"] = appType
                    fileInfo["mediaByte"] = ByteArray(0)
                    statusFilesInfo.add(fileInfo)
                }
            } else {
                Log.e(TAG, "Directory does not exist")
            }
        }
        */

        return statusFilesInfo
    }

private fun isPermissionGranted(): Boolean {
    return try {
        val directoryUri = STATUS_DIRECTORY
        Log.e(TAG, "STATUS_DIRECTORY: $directoryUri")

        if (directoryUri != null) {
            val directory = DocumentFile.fromTreeUri(context, directoryUri)
            val tempFile = directory?.createFile("text/plain", "temp.txt")
            val fileCreated = tempFile != null

            // Attempt to delete the temporary file
            val fileDeleted = tempFile?.delete() ?: false
            Log.e(TAG, "directory exists: ${fileCreated && fileDeleted}")
            if(fileCreated && fileDeleted){
                true
            }
        } else {
            Log.e(TAG, "STATUS_DIRECTORY is null")
            false
        }
    } catch (e: Exception) {
        Log.e(TAG, "Exception occurred: ${e.message}", e)
        false
    }
}

private fun requestSpecificFolderAccess(callback: (Boolean) -> Unit) {
    folderAccessCallback = callback
    val baseDirectory = getBaseDirectoryUri()
    val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // For Android 10 (Q) and above
        val sm = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent()
    } else {
        // For versions below Android 10 (Q)
        Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    }
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, baseDirectory)
    startActivityForResult(intent, PICK_DIRECTORY_REQUEST_CODE)
}

private fun getBaseDirectoryUri(): Uri {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // For Android 10 (Q) and above
        Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia/document/primary%3AAndroid%2Fmedia")
    } else {
        // For versions below Android 10 (Q)
        Uri.fromFile(File("/storage/emulated/0/Android/media"))
    }
}

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            val treeUri: Uri? = resultData?.data
            treeUri?.let {
                contentResolver.takePersistableUriPermission(
                    it,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                )
                STATUS_DIRECTORY = it
                Log.d(TAG, "Selected directory: $STATUS_DIRECTORY")
                folderAccessCallback?.invoke(true)
            }
        } else {
            folderAccessCallback?.invoke(false)
        }
    }

    private fun getSavedStatusesPath(): String {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Status Saver").absolutePath
        } else {
            File(Environment.getExternalStorageDirectory(), "Pictures/Status Saver").absolutePath
        }
    }

    private fun getWhatsAppPath(): String {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            File(Environment.getExternalStorageDirectory(), "/Android/media/com.whatsapp/WhatsApp/Media/.Statuses").absolutePath
        } else {
            File(Environment.getExternalStorageDirectory(), "/WhatsApp/Media/.Statuses").absolutePath
        }
    }

    private fun getWhatsApp4BPath(): String {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            File(Environment.getExternalStorageDirectory(), "/Android/media/com.whatsapp.w4b/WhatsApp Business/Media/.Statuses").absolutePath
        } else {
            File(Environment.getExternalStorageDirectory(), "/WhatsApp Business/Media/.Statuses").absolutePath
        }
    }

    private fun saveStatus(sourceFilePath: String): String {
        val sourceFile = File(sourceFilePath)

        return if (sourceFile.exists()) {
            try {
                val galleryDirectory = File(getSavedStatusesPath())

                if (!galleryDirectory.exists()) {
                    galleryDirectory.mkdirs()
                }

                val originalFileName = sourceFile.name
                val originalExtension = getExtension(sourceFile)

                val newImageFile = File(galleryDirectory, originalFileName)

                // If a file with the same name already exists, return "Already Saved"
                if (newImageFile.exists()) {
                     return "Already Saved"
                }

                FileInputStream(sourceFile).use { inputStream ->
                    FileOutputStream(newImageFile).use { outputStream ->
                        val buffer = ByteArray(4 * 1024)
                        var bytesRead: Int
                        while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
                            outputStream.write(buffer, 0, bytesRead)
                        }
                    }
                }

                // File saved successfully
                "Status Saved"
                } catch (e: IOException) {
                    // e.printStackTrace()
                    // Error saving file
                    "Not Saved"
                }
        } else {
            // Source file doesn't exist
            "Not Saved"
        }
    }

    private fun deleteStatus(filePath: String): String {
        return try {
            val uri = MediaStore.Files.getContentUri("external")
            val selection = "${MediaStore.Files.FileColumns.DATA} = ?"
            val selectionArgs = arrayOf(filePath)

            val deletedCount = contentResolver.delete(uri, selection, selectionArgs)

            if (deletedCount > 0) {
                "Deleted"
            } else {
                "Not deleted"
            }
        } catch (e: Exception) {
            // Handle any exceptions
            "Error: ${e.message}"
        }
    }

@JohnMiltonHacks

devfemibadmus commented 4 months ago

checking https://github.com/pg598595/ScopedStorageDemo i had similar code missing FLAG_GRANT_PERSISTABLE_URI_PERMISSION

i should be able to fix this with below code from above link

class OpenFolderActivity : AppCompatActivity() {

    var fileList = ArrayList<DocumentFile>()
    val adpater = FileViewAdpater(this, fileList)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_open_folder)

        val layoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
        rvTree.layoutManager = layoutManager

    }

    fun openDocTree(view: View) {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
            flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                    Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
        }
        startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

            fileList.clear()
            val directoryUri = data?.data ?: return

            contentResolver.takePersistableUriPermission(
                directoryUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
            loadDirectory(directoryUri)

        }
    }

    fun loadDirectory(directoryUri: Uri) {
        val documentsTree = DocumentFile.fromTreeUri(application, directoryUri) ?: return
        val childDocuments = documentsTree.listFiles().asList()

        for (i in 0 until childDocuments.size) {
            Log.i("TAG", "${childDocuments[i].type}")
            fileList.add(childDocuments[i])

        }
        rvTree.adapter = adpater

    }

}