Android Embarcado: Acessando arquivos via Java Native Interface usando Kotlin e C++

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”.

Java Native Interface

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.

Java Native Interface

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.

Java Native Interface

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:

  1. MainActivity.kt – Arquivo em Kotlin da Activity da nossa tela
  2. native-lib.cpp – Arquivo C++ onde serão implementadas as funções de acesso a arquivos
  3. 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++:

Java Native Interface

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:

  1. TextView para ler texto e expor logs do uso do aplicativo.
  2. Botão de Escrita.
  3. Botão de Leitura.
  4. Botão de Remoção.
  5. Uma caixa de edição de texto para inserir o caminho e nome do arquivo.
  6. 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:

Java Native Interface

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:

Java Native Interface

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 Permission 

https://devofandroid.blogspot.com/2022/05/manage-external-storage-permission.html

[2] JNI-101 — Introduction to Java Native Interface

https://medium.com/@sarafanshul/jni-101-introduction-to-java-native-interface-8a1256ca4d8e

[3] C++ Files and Streams

https://www.tutorialspoint.com/cplusplus/cpp_files_streams.htm

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Software » Android Embarcado: Acessando arquivos via Java Native Interface usando Kotlin e C++

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: