Introdução
Em sistemas embarcados, é comum a necessidade de acessar arquivos de forma eficiente e confiável. O Android é uma plataforma popular para sistemas embarcados, e oferece uma variedade de recursos para acessar arquivos, incluindo a Java Native Interface (JNI).
A JNI é uma API que permite que o código Java interaja com código nativo, escrito em linguagem de programação C ou C++. Isso torna possível acessar recursos do sistema operacional e do hardware que não estão diretamente disponíveis para o código Java.
Java Native Interface
Em um sistema embarcado, o código Java é responsável pelo processamento de dados e pela apresentação de informações para o usuário. No entanto, o código Java não tem acesso direto a recursos do sistema operacional e do hardware.
A JNI permite que o código Java acesse esses recursos, por meio do código nativo, tornando possível ao código Java realizar tarefas como:
- Ler e escrever arquivos.
- Acessar dispositivos de entrada e saída.
- Utilizar recursos de rede.
- Controlar o hardware.
Para usar a JNI, é necessário ter um conhecimento básico de C ou C++. Além disso, é necessário que o sistema operacional do dispositivo embarcado forneça suporte a JNI.
Pré-requisitos
Para desenvolver um aplicativo com JNI em uma plataforma Android, é necessário atender aos seguintes pré-requisitos:
- Um celular Android com Android 7 (N, API nível 24) ou superior.
- Conhecimento básico de C/C++.
- Conhecimentos básicos de Java/Kotlin (nesse artigo é usado Kotlin).
- Conhecimentos básicos na IDE Android Studio.
Ao final de cada sessão ou subsessão que houve modificações no código será disponibilizado o link do github para o commit daquelas mesma para uma melhor visão do que deve ser feito.
Criando aplicativo no Android Studio
Criando o projeto
No Android Studio vá em “File > New > New Project”, isso abrirá a tela de criação de novos projetos. Em “Phone and Tablet” escolha a opção “Native C++” e aperte “Next”.

Dê o nome da sua aplicação, no nosso caso será “FileAccessJNI”, com pacote de nome “vendor.alvenan.fileaccessjni”. Escolha o caminho em que seu projeto ficará e a linguagem que vai usar (neste tutorial, usaremos Kotlin). A API mínima que usaremos é a 10 para mostrarmos as duas formas de pedir acesso, o motivo será explicado melhor na sessão 5.

Em “C++ Standard” você pode escolher a versão do C++ que irá utilizar no seu projeto. Nesse nosso exemplo, vamos usar a “Toolchain Default”, mas caso o seu projeto tenha algum pré-requisito de versão utilize a versão correta.

Entendendo os arquivos criados
Tendo o projeto criado, agora precisamos entender para que serve cada arquivo que iremos usar. Existem três principais, os quais são:
- MainActivity.kt – Arquivo em Kotlin da Activity da nossa tela
- native-lib.cpp – Arquivo C++ onde serão implementadas as funções de acesso a arquivos
- activity_main.xml – Arquivo XML onde será desenhada a nossa interface gráfica
Na forma padrão em que o projeto é criado, a interface do projeto possui um TextView para mostrar texto:
activity_main.xml
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
A biblioteca C++ implementa a função stringFromJNI() que cria uma string com o texto “Hello from C++” e o retorna quando função é chamada.
native-lib.cpp
extern "C" JNIEXPORT jstring JNICALL
Java_vendor_alvenan_fileaccessjni_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
E a MainActivity chama a função stringFromJNI(), recebe a string que vem do C++ e preenche o TextView na interface gráfica.
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Example of a call to a native method
binding.sampleText.text = stringFromJNI()
}
Como resultado, temos a tela do aplicativo com o texto gerado na biblioteca C++:
Criando a interface gráfica do aplicativo
Para o nosso programa de acesso a arquivos, precisamos de algumas interfaces a mais, que serão listadas a seguir:
- TextView para ler texto e expor logs do uso do aplicativo.
- Botão de Escrita.
- Botão de Leitura.
- Botão de Remoção.
- Uma caixa de edição de texto para inserir o caminho e nome do arquivo.
- Uma caixa de edição de texto para inserir o que deve ser escrito no arquivo.
Como o foco deste artigo não é ensinar a desenhar interfaces gráficas (e nem sou especialista nessa área) então abaixo deixo o link com a change que foi feita no arquivo xml da tela do projeto para a interface que iremos utilizar durante a nossa implementação.
Link para o commit:
https://github.com/alvenan/FileAccessJNI/commit/d8d8c74e4a0528dc7f5ea44352d3ad455a37b0ac
Com isso nossa interface gráfica ficará da seguinte forma:
Declarando interfaces
No arquivo MainActivity.kt, adicione as declarações de interfaces como abaixo
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var textBox: TextView
private lateinit var filePath: EditText
private lateinit var message: EditText
private lateinit var writeBtn: Button
private lateinit var readBtn: Button
private lateinit var deleteBtn: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textBox = findViewById(R.id.sample_text)
filePath = findViewById(R.id.pathet)
message = findViewById(R.id.textet)
writeBtn = findViewById(R.id.writebtn)
readBtn = findViewById(R.id.readbtn)
deleteBtn = findViewById(R.id.deletebtn)
}
Link para o commit:
https://github.com/alvenan/FileAccessJNI/commit/5efaadfe3d4e17b5d8aa1e4bca20b07fc87f4f7a
Solicitando permissões ao Sistema
Adicionando permissões ao arquivo de manifesto
A primeira coisa a ser feita é adicionar ao arquivo de manifesto quais permissões o aplicativo irá necessitar. Essa declaração é importante para que, quando tanto a Google Play Store quanto o próprio usuário precisam saber todas as permissões, exista uma lista fácil informando todas elas.
Abaixo está o código AndroidManifest.xml, onde são adicionadas três permissões. As duas primeiras são para tratamento em dispositivos com versões inferiores ao Android 11 onde é necessário pedir acesso separadamente para leitura (READ_EXTERNAL_STORAGE) e escrita (WRITE_EXTERNAL_STORAGE) em arquivos e a terceira permissão é para tratamento em sistemas com Android 11 (R) ou superior onde é necessário pedido de apenas uma permissão de acesso à arquivos (MANAGE_EXTERNAL_STORAGE).
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--Permissions for the Android below 11 (R)-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--Permission for the Android 11 (R) and above-->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
Link para o commit:
https://github.com/alvenan/FileAccessJNI/commit/42e445e8d1f83073a6c585b81d626430229c9dc7
Solicitando permissão durante o uso do aplicativo
Para iniciar nossa solicitação, criaremos duas novas variáveis dentro do bloco companion object, no qual as variáveis declaradas são tratadas como estáticas:
MainActivity.kt
companion object {
private const val STORAGE_PERMISSION_CODE = 100
private const val TAG = "FileAccessApp - Java"
Agora, faremos a chamada do click do botão de escrita dentro da função onCreate(), com a condição para caso o aplicativo não tenha permissão de acesso. Neste primeiro momento, como ainda não temos ainda as funções de acessos a arquivos de fato, vamos apenas continuar imprimindo o texto “Hello from C++”.
MainActivity.kt
writeBtn.setOnClickListener {
if (checkPermission()) {
textBox.text = stringFromJNI()
} else {
Log.i(TAG,"Acesso ao armazenamento não liberado, solicitando.")
requestPermission()
}
}
readBtn.setOnClickListener {
if (checkPermission()) {
textBox.text = stringFromJNI()
} else {
Log.i(TAG,"Acesso ao armazenamento não liberado, solicitando.")
requestPermission()
}
}
deleteBtn.setOnClickListener {
if (checkPermission()) {
textBox.text = stringFromJNI()
} else {
Log.i(TAG,"Acesso ao armazenamento não liberado, solicitando.")
requestPermission()
}
}
Nesse pedaço de código, existem duas funções ainda não implementadas, a checkPermission() e a requestPermission(), as quais possuem nomes sugestivos de seu uso. A seguir, será mostrado, então, como é feita a verificação se o aplicativo tem permissão e como solicitá-la.
Para a verificação de permissão, como estamos tratando para todas as versões de Android é necessário fazer uma condição para qual versão estamos tratando. Caso seja igual ou superior ao Android 11 (R), o código irá verificar no ambiente do sistema se o aplicativo já tem esse tipo de acesso Caso contrário, ele irá verificar se as permissões WRITE_EXTERNAL_STORAGE e READ_EXTERNAL_STORAGE já foram concedidas. Estas retornarão um valor booleano indicando o resultado se é necessário pedir acesso.
MainActivity.kt
private fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Environment.isExternalStorageManager()
else {
val write =
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
val read =
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
write == PackageManager.PERMISSION_GRANTED && read == PackageManager.PERMISSION_GRANTED
}
}
Após verificar que o dispositivo não possui permissão, o aplicativo deve entrar em modo de solicitação. Nossa função de solicitação, assim como a verificação, possui também uma condição para versão, onde para Android 11 ou superior é necessário lançar uma janela de configuração para que o usuário manualmente ative a permissão. Enquanto para versões abaixo do Android 11 só é necessário a fazer a solicitação direta o que mostrará apenas um pop-up para o usuário escolher se permite ou não.
MainActivity.kt
private fun requestPermission() {
Log.d(TAG, "Solicitando permissão")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
intent.data = Uri.fromParts("package", this.packageName, null)
storageActivityResultLauncher.launch(intent)
} else {
ActivityCompat.requestPermissions(
this, arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
), STORAGE_PERMISSION_CODE
)
}
}
Agora é necessário mostrar um feedback de resposta da escolha do usuário. A seguir é a forma feita necessariamente após a storageActivityResultLauncher.launch(intent) ter sido chamado na função mostrada acima.
MainActivity.kt
@RequiresApi(Build.VERSION_CODES.R)
private val storageActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
Log.i(TAG, "storageActivityResultLauncher: ")
if (Environment.isExternalStorageManager()) {
Log.i(TAG,"storageActivityResultLauncher: Permissão de acesso ao armazenamento concedida.")
} else {
Log.i(TAG,"storageActivityResultLauncher: Permissão de acesso ao armazenamento negada....")
Toast.makeText(this, "Permissão de acesso ao armazenamento negada....", Toast.LENGTH_SHORT).show()
}
}
E a função a seguir faz o feedback para versões abaixo do Android 11.
MainActivity.kt
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty()) {
val write = grantResults[0] == PackageManager.PERMISSION_GRANTED
val read = grantResults[1] == PackageManager.PERMISSION_GRANTED
if (write && read) Log.d(TAG, "onRequestPermissionsResult: Permissão de acesso ao armazenamento concedida.")
else {
Log.d(TAG, "onRequestPermissionsResult: Permissão de acesso ao armazenamento negada....")
Toast.makeText(this, "Permissão de acesso ao armazenamento negada....", Toast.LENGTH_SHORT).show()
}
}
}
}
Link para o commit:
https://github.com/alvenan/FileAccessJNI/commit/e1645d9458842d6bdbff807abaa72b3587238d48
Abaixo vão as figuras de como ficam as telas de solicitação de permissão após apertar um dos 3 botões:
Acessando arquivos via C++
Agora que toda a parte de interface e segurança está pronta, podemos focar no acesso a arquivos e adicionar as funções necessárias. Primeiramente deve-se remover a indicação de função externa stringFromJNI() pois ela não será mais usada nesse projeto e em seguida indicar à Activity quais funções externas à ela serão implementadas em C++, e que ela terá acesso.
MainActivity.kt
external fun readFile(yourFilepath: String): String
external fun writeFile(yourFilepath: String, text: String): String
external fun removeFile(yourFilepath: String): String
Então agora no arquivo native-lib.cpp, a função stringFromJNI() deve ser removida, pois ela não será mais usada. E as seguintes bibliotecas devem ser incluídas:
native-lib.cpp
#include <fstream>
#include <android/log.h>
#include <sstream>
#define TAG "FileAccessApp - C++"
Em seguida, as funções readFile(), writeFile() e removeFile() devem ser implementadas. A função readFile() lê o conteúdo de um arquivo especificado pelo parâmetro yourFilepath e retorna o conteúdo como uma string. Primeiro, ele verifica se o arquivo pode ser aberto, se sim, ele lê o conteúdo do arquivo em um buffer e converte o buffer em uma string. Finalmente, fecha o arquivo e retorna a string. Se o arquivo não puder ser aberto, retorna uma mensagem de erro.
native-lib.cpp
extern "C" JNIEXPORT jstring JNICALL
Java_vendor_alvenan_fileaccessjni_MainActivity_readFile(
JNIEnv *env,
jobject,
jstring yourFilepath) {
jstring ret;
std::ifstream file(env->GetStringUTFChars(yourFilepath, 0));
jstring status_msg;
std::ostringstream buf;
if (file.is_open()) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Openned file succesfully! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
buf << file.rdbuf();
ret = env->NewStringUTF(buf.str().c_str());
file.close();
} else {
__android_log_print(ANDROID_LOG_ERROR, TAG, "Cannot open the file! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
ret = env->NewStringUTF("Cannot open the file!");
}
return ret;
}
A função writeFile() escreve o conteúdo do parâmetro jtext em um arquivo especificado pelo parâmetro yourFilepath. Primeiro, a função verifica se o arquivo pode ser aberto. Se puder, ela escreve o texto no arquivo e fecha o arquivo. Em seguida, retorna uma mensagem de sucesso. Se o arquivo não puder ser aberto, retorna uma mensagem de erro.
native-lib.cpp
extern "C" JNIEXPORT jstring JNICALL
Java_vendor_alvenan_fileaccessjni_MainActivity_writeFile(
JNIEnv *env,
jobject,
jstring yourFilepath,
jstring jtext) {
std::ofstream file(env->GetStringUTFChars(yourFilepath, 0));
jstring status_msg;
if (file.is_open()) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Openned file succesfully! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
file << env->GetStringUTFChars(jtext, 0);
file.close();
status_msg = env->NewStringUTF("Message successfully written!");
} else {
__android_log_print(ANDROID_LOG_ERROR, TAG, "Cannot open the file! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
status_msg = env->NewStringUTF("Cannot open the file!");
}
return status_msg;
}
A função removeFile() exclui o arquivo especificado pelo parâmetro yourFilepath. Primeiro, ela verifica se o arquivo pode ser excluído. Se puder, a função exclui o arquivo e retorna uma mensagem de sucesso. Se o arquivo não puder ser excluído, ela retorna uma mensagem de erro.
native-lib.cpp
extern "C" JNIEXPORT jstring JNICALL
Java_vendor_alvenan_fileaccessjni_MainActivity_removeFile(
JNIEnv *env,
jobject,
jstring yourFilepath) {
jstring ret;
jstring status_msg;
if (!std::remove(env->GetStringUTFChars(yourFilepath, 0))) {
__android_log_print(ANDROID_LOG_INFO, TAG, "Removed file successfully! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
ret = env->NewStringUTF("Removed file successfully!");
} else {
__android_log_print(ANDROID_LOG_ERROR, TAG, "Cannot delete the file! Path: %s",
env->GetStringUTFChars(yourFilepath, 0));
ret = env->NewStringUTF("Cannot remove the file!");
}
return ret;
}
E voltando à Activity, para cada interface de botão, deve-se colocar a sua função própria como mostrado e pegar os textos escritos pelo usuário nas textBoxes.
MainActivity.kt
writeBtn.setOnClickListener {
if (checkPermission()) {
Log.i(TAG,"Writing the message in a file "+ filePath.text.toString() +" through JNI")
textBox.text = writeFile(filePath.text.toString(), message.text.toString())
} else {
Log.i(TAG,"Storage permission was not granted, request")
requestPermission()
}
}
MainActivity.kt
readBtn.setOnClickListener {
if (checkPermission()) {
Log.i(TAG,"Reading the message from the file " + filePath.text.toString() + " through JNI")
textBox.text = readFile(filePath.text.toString())
} else {
Log.i(TAG,"Storage permission was not granted, request")
requestPermission()
}
}
MainActivity.kt
deleteBtn.setOnClickListener {
if (checkPermission()) {
Log.i(TAG,"Deleting the file " + filePath.text.toString() + " through JNI")
textBox.text = removeFile(filePath.text.toString())
} else {
Log.i(TAG,"Storage permission was not granted, request")
requestPermission()
}
}
Link para o commit:
https://github.com/alvenan/FileAccessJNI/commit/cb9d676fbca45ce3c1cdbbd716eef61f0cf3b42e
Resultado
O funcionamento dos códigos aqui apresentados pode ser visto na prática no vídeo abaixo:
Link para o projeto completo: https://github.com/alvenan/FileAccessJNI
Conclusão
O desenvolvimento de apps Android com o uso de JNI pode ser uma tarefa desafiadora, mas também muito gratificante. A JNI permite que os desenvolvedores acessem recursos do sistema operacional e do hardware que não estão diretamente disponíveis para o código Java. Isso pode ser útil para uma variedade de tarefas, como:
- Ler e escrever arquivos
- Acessar dispositivos de entrada e saída
- Utilizar recursos de rede
- Controlar o hardware
Para desenvolver um app Android com JNI, é necessário atender a alguns pré-requisitos, como ter um celular Android com Android 7 (N, API nível 24) ou superior, ter conhecimentos básicos de C/C++ e Java/Kotlin, e usar um IDE que suporte o desenvolvimento de apps Android.
Aqui estão algumas dicas para o desenvolvimento de apps Android com JNI:
- Comece com um projeto simples e vá aumentando a complexidade à medida que você ganha experiência.
- Documente seu código para que você possa entender o que ele faz.
- Use depuração para encontrar e corrigir erros.
Com um pouco de prática, você será capaz de desenvolver apps Android poderosos e sofisticados com o uso de JNI.
Referências
[1] Manage External Storage Permissionhttps://devofandroid.blogspot.com/2022/05/manage-external-storage-permission.html
[2] JNI-101 — Introduction to Java Native Interfacehttps://medium.com/@sarafanshul/jni-101-introduction-to-java-native-interface-8a1256ca4d8e
[3] C++ Files and Streamshttps://www.tutorialspoint.com/cplusplus/cpp_files_streams.htm





