Internal Storage + FileProvider com Jetpack Compose

Neste post você vai saber em qual ocasião pode ser usado um ContentProvider.

Rodrigo Cavalcante
Android Dev BR

--

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

Visualização de Pdf

--

--