Internal Storage + FileProvider com Jetpack Compose
Neste post você vai saber em qual ocasião pode ser usado um ContentProvider.
Desde o início dos meus estudos de Android me perguntei em qual contexto teria a necessidade de usar um ContentProvider. E, de repente (3 anos depois), estamos aqui.
O exemplo que vou abordar exigirá que você acompanhe primeiro esse artigo: https://www.geeksforgeeks.org/generate-pdf-file-in-android-using-jetpack-compose, pois foi com ele que aprendi a gerar um pdf usando Jetpack Compose.
No entanto, para pedir permissão, recomendo usar o primeiro exemplo desse artigo: https://betterprogramming.pub/jetpack-compose-request-permissions-in-two-ways-fd81c4a702c
O código desse exemplo com a alteração resultará nesse :
@Composable
fun pdfGenerator() {
val ctx = LocalContext.current
val permission = Manifest.permission.WRITE_STORAGE_PERMISSION
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
generatePdf()
} else {
// Fazer algo
}
}
Column(...) {
Text(...)
Spacer(...)
Button(
...,
onClick = {
checkAndRequestCameraPermission(ctx, permission, launcher)
}) {
Text(...)
}
}
}
fun checkAndRequestCameraPermission(
context: Context,
permission: String,
launcher: ManagedActivityResultLauncher<String, Boolean>
) {
val permissionCheckResult = ContextCompat.checkSelfPermission(context, permission)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
generatePdf()
} else {
// Pedir permissão
launcher.launch(permission)
}
}
Claro, para permissão de geração de pdf você vai usar um Manifest.permission.WRITE_EXTERNAL_STORAGE ao invés de Manifest.permission.CAMERA
Agora que você já sabe como fazer tudo isso, o cliente notou que o pdf está sendo salvo diretamente no diretório externo do dispositivo. Ele não curtiu, pois afinal de contas só quer visualizar esse arquivo ou encaminhar, ao invés de atolar o armazenamento externo do seu device com arquivos pdf.
E qual o motivo disso está acontecendo?
Usando o exemplo do geekForGeeks, você estará imprimindo o documento justamente no armazenamento externo exatamente aqui :
fun generatePdf() {
...
// Criando um arquivo com o diretório externo como parent
val file: File = File(Environment.getExternalStorageDirectory(), "GFG.pdf")
try {
// Imprimindo o documento no @file
pdfDocument.writeTo(FileOutputStream(file))
} catch (e: Exception) {
e.printStackTrace(
}
...
}
Como resolver esse problema imprimindo o documento no armazenamento interno do dispositivo?
Para acessar os pacotes do armazenamento interno do dispositivo você vai precisar de um context, no meu caso, preferi utilizar o diretório de cache. E para criar um arquivo vamos precisar do File class.
Tá bom, apressadinho! Vamos direto ao ponto:
// Arquivo de cache
val cacheDirectory = context.cacheDir
// Nome do pacote principal
val sales_packageName = "sales"
// Nome do pacote para guardar os documentos
val documents_packageName = "documents"
// Diretório que você quer criar
val newDirectory = File(cacheDirectory, sales_packageName + File.separator + documents_packageName)
if(!directory.exists()) {
// Se o directory não existe, vamos usar a mkdirs() para
// criar um novo diretório, essa condição é importante para o software
// não ficar criando sempre o mesmo diretório, o que seria desnecessário.
directory.mkdirs()
}
Observação: Entenda que os nomes dos pacotes são apenas exemplos, se você não quiser usar dois pacotes para organizar seus arquivos, fique à vontade.
Por fim, o código já citado sobre a impressão do documento no arquivo ficará assim:
fun generatePdf(context: Context, saleId:String) {
(...)
val cacheDirectory = context.cacheDir
val sales_packageName = “sales”
val documents_packageName = “documents”
val newDirectory = File(cacheDirectory, sales_packageName + File.separator + documents_packageName)
if(!newDirectory.exists()) {
newDirectory.mkdirs()
}
val file = File(newDirectory, "${saleId}.pdf")
try {
// Imprimindo o documento no @file
pdfDocument.writeTo(FileOutputStream(file))
} catch (e: Exception) {
e.printStackTrace(
}
(...)
}
Perceba que estou passando um context no parâmetro do método generatePdf(context: Context). Além disso, passo o valor do id de uma venda, pois, no meu caso, estou salvando o pdf de comprovante de venda de acordo com o id da venda, ficando generatePdf(context: Context, saledId: String). No entanto, você pode usar qualquer valor para definir o nome do seu arquivo.
Agora que podemos salvar o pdf no armazenamento interno do dispositivo, que tal abrirmos esse arquivo em quaisquer aplicativos que possam ler documentos desse tipo?
(...)
override fun onCreate(savediN: Bundle) {
setContent {
SuaComposableScreen(
toPdfReader = { saleId ->
val directory = File(cacheDir.path + File.separator + "sales" + File.separator + "documents")
val file = File(directory, "${saleId}.pdf")
val uri = Uri.fromFile(file)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/pdf")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent)
}
)
}
}
Observação: Como você está no context da sua Activity, pode dar um startActivity() sem medo. Você também pode refatorar esse código usando-o dentro de uma extension
fun Context.intentToReadDocument(saleId: String) {
val directory = File(cacheDir.path + File.separator + "sales" + File.separator + "documents")
val file = File(directory, "${saleId}.pdf")
val uri = Uri.fromFile(file)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/pdf")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent)
}
Oops, você deve ter levado uma FileUriExposedException :(
O que pode ser resolvido utilizando o FileProvider — um ContentProvider — para permitir que outros aplicativos possam ter acesso a esse arquivo.
Usando o FileProvider
Primeiro crie um arquivo xml, nomeie como quiser, mas aqui vou nomear como : provider_paths
Neste exemplo, quero acessar meus arquivos em cache — no nosso cacheDir, portanto vou usar a tag : cache-path
provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name=“cache” path="."/>
</paths>
Depois adicione o provider no nível da sua application no pacote AndroidManifest.xml, o qual ficará assim :
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...
<application
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
Agora, vamos adicionar o ContentProvider à nossa função intentToPdfReader()
, gerando a URI com as devidas permissões.
fun Context.intentToReadDocument(saleId: String) {
val directory = File(cacheDir.path + File.separator + "sales" + File.separator + "documents")
val file = File(directory, "${saleId}.pdf")
val uriByProvider = FileProvider.getUriForFile(
this,
this.applicationContext.packageName + ".provider",
file
)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uriByProvider, "application/pdf")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent)
}
Parabéns! :D Agora você sabe como acessar e criar arquivos no diretório de cache da sua aplicação e utilizar o FileProvider para permitir que outras aplicações possam fazer a leitura dos seus arquivos internos.
Você pode inspecionar a geração de arquivos no armazenamento interno indo em Device Explorer > data > sua.application > cache > seuDocumentoCriado.pdf . No meu caso, ficou : Device Explorer > data > minha.application / cache / sales / documents / saleId.pdf