Cómo migrar una db implementada en Windows y SQLite a Android sin decir grocerías
” Object-oriented no es el único patrón de diseño válido. A muchos programadores se les ha enseñado a pensar puramente en términos de objetos. Y, para ser justos, los objetos son a menudo una buena manera de descomponer un problema. Pero los objetos no son la única manera, y no siempre son la mejor manera de descomponer un problema. A veces el viejo y buen código de procedimientos es más fácil de escribir, más fácil de mantener y entender, y más rápido que el código orientado a objetos.“
Por qué SQLite está programado en C
En el post anterior, describo mis peripecias migrando un juego desarrollado en C++ y SDL2 a Android.
El juego es tilemovers y ya está publicado en itch.io. Puedes echarle una mirada.
Versión Android en el Google Store. Por favor descarga la versión Android y enviame tus comentarios.
En este post describo cómo fue el proceso de migrar el código relacionado a SQLite y la base de datos. Por qué mi juego tilemovers requiere una base de datos, es tema de otro post, baste decir, por ahora, que la idea detrás de programar videojuegos es la diversión intrinseca en hacerlo, es decir, para mí tiene/debe ser divertido hacerlo, y no hay nada más divertido que programar juegos que usen complicadas bases de datos. Si alquien se divierte jugando el juego, es un extra maravilloso, pero no requerido.
Como explico en el post anterior, si es la primera vez que programas una aplicación para Android (y para mobile en general) hay muchas preguntas obvias cuya respuesta desconoces (aunque es algo que un programador de mobile sabe desde el prescolar y Plaza Sésamo).
¿Dónde se guardan los archivos, y en particular, la base de datos de SQLite?
Es un asset como cualquier otro y se declara en la carpeta assets dentro de la estructura de archivos de Android manejada por Android Studio (en adelante AS). Es decir,
\app\src\main\assets
pero no puedes escribir ahí, es decir, esos archivos son read-only. Tienes que moverla al área de datos de la aplicación (se dice fácil pero es una epopeya: leer más adelante)
¿Y hay que pedir permiso para escribir en la base de datos?
Como dice la documentación oficial, la aplicación no requiere autorización para leer o escribir en los directorios asignados para almacenamiento interno (de nuevo, no estamos hablando de la carpeta assets, donde solo se puede leer)
¿Por qué no se puede abrir un asset usando ifstream o apuntadores a archivos?
De acuerdo a esta respuesta en stackoverflow, porque no. Pero en el ámbito de esta respuesta, si se puede utilizar sqlite.c (es decir no hay que migrar SQLite a la versión java/kotlin) pero la base de datos tiene que estar en el área de datos de la aplicación. (¿por qué estoy repitiendo esto tantas veces? Porque un programador avezado y con experiencia en otras plataformas va a encontrar bien complicado de entender que una aplicación tiene al mismo tiempo 2 áreas de datos asignadas, una read only, y la otra read-write-has-lo-que-quieras).
Luego de mi investigación (que puede no estar completa, y puede contener errores). Las alternativas a la mano para trabajar con bases de datos o archivos de datos en una aplicación Android son las siguientes:
- Necesitas trabajar con
AAssetManager, AssetManager_open, AAsset_read
para acceder a la base de datos tal como se explica aquí. Esta es una solución implementada en C usando NDK. Se llama SQLite-NDK. No hice esto, aunque dejo abierta la posibilidad para otros juegos, ver abajo. - La bases de datos almacenada en assets no se puede usar directamente, hay que instalarla en:
“data/data/your-package-name/databases” (ver punto 5 abajo) - Una idea es tener los datos en un archivo json o xml, crear la base de datos y luego cargar los datos (como se explica aquí).
- Otra sugerencia es usar el Content Provider
- Otra idea es utilizar una clase
DataBaseHelper derivada de SQLiteOpenHelper
para copiar la base de datos de assets al área de datos de la aplicación. Aquí se explica cómo es el proceso. Esta explicación al parecer es más clara. Este fue el procedimiento implementado. - Para obtener la ubicación donde se va a ubicar la base de datos se debe utilizar:
File dbFile = context.getDatabasePath(name_of_database_file);
(ver aquí, y ver documentación en developer.android.com, hay que usar esta función o la aplicación puede fallar en algunos teléfonos inteligentes nuevos, por ejemplo, Huawei)
Hay varios retos para hacer que todo esto funcione. Primero hay que averiguar cómo llamar código escrito en java desde la aplicación desarrollada en C/SDL2 (sí, en cierto momento surge la pregunta de por qué no migrar completamente a kotlin (el lenguaje nativo de aplicaciones android) y la respuesta es que la idea desde el comienzo es migrar una aplicación desarrollada en C++, en Windows usando SDL2 a Android).
La solución es simple tal como se explica aquí. El siguiente paso es cómo integrar una clase java a una aplicación desarrollada usando NDK, dentro de Android Studio (lo inverso es fácil y aparece aquí, es decir, como usar NDK desde una aplicación desarrollada en kotlin).
Lo interesante es que viendo ese artículo recordé que Android Studio tiene menus, muchos menus, y entre ellos hay un “Add Java class”, el cual funciona pero no necesariamente sobre el nombre del proyecto (como mi intuición me indica) sino sobre la carpeta del paquete que en el caso de mi aplicación basada en sdl se llama org.libsdl.app
Al agregar la clase, Android Studio pregunta a cuál paquete lo vamos a agregar, indicamos org.libsdl.app. Copiamos el contenido de nuestra clase al archivo creado. Este archivo es almacenado en app\src\main\java\org\libsdl\app
Nuestro archivo java es basicamente una clase ( DataBaseHelper) que verifica que la base de datos existe. Si no existe la copia desde assets al área de datos. La soluión completa está aquí.
La declaración de la clase luce así:
public class DataBaseHelper extends SQLiteOpenHelper
Ahora, como dije antes todo parace simple siempre y cuando puedas hacer llamadas a java desde tu aplicación desarrollada en C.
Para ello, básicamente tienes que usar esta llamada
jclass dbhelper = env->FindClass(“org/libsdl/app/DataBaseHelper”);
donde estás declarando un apuntador (creo que en el universo java tiene otro nombre) a la clase (que debes identificar colocando el path completo)
El primer problema es cómo consigues inicializar la variable “env”. Si comienzas a buscar en stackoverflow quedas en un lazo infinito porque para inicializarla, necesitas hacer;
JNIEnv *env;
g_JavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)
Y ahora tu problema es como inicializar la variable g_JavaVM, el apuntador a la máquina virtual. El cuento es largo y lo voy a omitir, al final la forma de hacerlo es inicializar la llamada en el momento de carga de JNI, lo que se hace a través de esta función:
JavaVM* g_JavaVM = NULL; extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { g_JavaVM = vm; return JNI_VERSION_1_6; }
Yo estuve varias horas tratando de utilizar Android_JNI_GetEnv(void); sin suerte, función que se encuentra declarada en SDL_android.h (en el código de la versión Android de SDL2) pero no funciona. La única forma que descubrí fue con la llamada JNI_OnLoad ().
Luego que tienes el apuntador a la clase debes crear una instancia de la clase (un objeto) lo que haces a través de lo siguiente:
jclass javaGlobalClass = reinterpret_cast<jclass>(env->NewGlobalRef(dbhelper));
jmethodID helperConstructor = env->GetMethodID(javaGlobalClass, “<init>”, “(Landroid/content/Context;)V”);
Con esto puedes hacer la llamada para la ejecución del constructor de la clase:
jmethodID helperConstructor = env->GetMethodID(javaGlobalClass, “<init>”, “(Landroid/content/Context;)V”);
Y con el contructor haces la llamada a createDataBase:
jmethodID createDataBaseMethod = env->GetMethodID(dbhelper, "createDataBase", "()V"); env->CallVoidMethod(obj, createDataBaseMethod);
Y con la base de datos, ya puedes acceder a ella utilizando las llamadas de SQLite de forma normal, tal como lo haces en cualquier programa C/C++ implementado con SDL2, que en nuestro caso es tilemovers.