Lost in Translation: The Bug That Spoke Russian and Crashed My App

(Actualizado el )
Spy looking at screens.
Spy looking at screens.
Other languages: Español

Introduction

A few years ago, I was working on Lipo Manager to add some long-pending features. The app is quite simple but more than enough for managing LiPos (a type of battery). Some of these changes were driven by community feedback, requesting features that hadn’t been implemented yet: visual improvements, optimization, multi-language support, dependency updates, and fixing the occasional NullPointerException.

After a day of work, I managed to finish everything and, after a few tests, I released the new version.

“I Can’t Enter the App”

A few days later, I received a message from a user via Telegram:

Screenshot of a user explaining issues accessing the app after updating their phone.

“I updated my phone and the app stopped working.”

Hmm…

One of the first steps every developer knows is that to solve a bug, you must first try to replicate it. Therefore, it’s useful to know the environment in which it occurs. I asked for system information, to which he readily agreed.

The first thing I wanted to know was whether the user’s Android version was either very new (beta) or very old. Generally, I wanted to verify if it was a version I hadn’t tested and if there was any issue with a library the app uses. To my surprise, his phone was running Android 13. Exactly the same version and APIs I had tested the app with the most.

I needed to dig deeper.

Checking Logs in the Play Console

Google provides developers with many tools to manage apps published on the Play Store. One of them is Android Vitals, which collects information about each installation, and in case of any exceptions, Android collects all traces and makes them available to the developer along with many more details.

I won’t comment on whether this breaches privacy or not, but when things go wrong, it’s impractical to ask the user to connect via ADB to their phone, extract traces, and send them to us. So, in the end, it’s a very useful tool.

The traces from a couple of users showed me this:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.dropvoid.lipomanager/com.dropvoid.lipomanager.MainActivity}: android.database.sqlite.SQLiteException: not an error (code 0 SQLITE_OK)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
	...
	at android.app.ActivityThread.main(ActivityThread.java:8176)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: android.database.sqlite.SQLiteException: not an error (code 0 SQLITE_OK)
	at android.database.sqlite.SQLiteConnection.nativeRegisterLocalizedCollators(Native Method)
	at android.database.sqlite.SQLiteConnection.setLocaleFromConfiguration(SQLiteConnection.java:460)
	at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:272)
	...
	at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:1067)
	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:931)
	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:920)
	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:373)
	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
	at com.orm.SugarDb.getDB(SugarDb.java:38)
	at com.orm.SugarRecord.getSugarDataBase(SugarRecord.java:35)
	at com.orm.SugarRecord.find(SugarRecord.java:201)
	at com.orm.SugarRecord.listAll(SugarRecord.java:127)
	at com.dropvoid.lipomanager.services.BatteryService.loadAllBatteries(BatteryService.java:21)
	...

A database issue? My first thought was that the problem might be related to updating SugarORM. This library is used by the app as an ORM to manage the database. However, since I hadn’t experienced any issues, I doubted it was directly responsible. So, what was the condition causing this?

The Odyssey Begins

Alright. We just need to replicate the problem and start following the trail to the bug, right? Simple. A routine. It would be solved soon.

+15 hours later

(Unfortunately) everything was fine. There was no way to make this explode. And that was all I wanted.

The internet wasn’t shedding much light, as everyone seemed to point to causes like file permission issues or sync failures among others. None of these seemed to be the cause or make sense in my case. In any case, I couldn’t know because there was no way to replicate the problem.

Since I couldn’t replicate it, I couldn’t roll back the changes either. Mostly because I wasn’t even sure if this problem had been around for a long time.

Google also provided me with the Android version, phone model, etc. Not even by emulating exactly the previous characteristics could I make any progress.

At this point, I had abused the application in every possible way, and ironically, I was annoyed that it was so robust; I had corrupted the database, inserted thousands of records, isolated everything related to the database from the rest of the application, simulated dozens of version migrations… none of that caused the problem I was looking for.

The BatteryService.loadAllBatteries() method was too simple and didn’t have much logic. Plus, it was one of the first methods called when opening the app, so there weren’t many chances of a race condition or something like that.

Every time my desperation grew and I re-read the traces, the message seemed more insulting: “SQLiteException: not an Error. Everything OK“.

At this point, I was stuck. I had wasted too much time on all of this, and I began to feel defeated. I didn’t know what else to do.

With resignation, I started to close everything down…

Plot Twist

I only had a few windows left to close. Right in front of me was the Google Play page, and I wanted to take one last look at the user’s device details in case I had missed something. I almost had it memorized after so much time struggling with it; device, versions, traces, installed apps, features, country… country?

The user’s country was the one detail I had overlooked, but now it turned out to be the only aspect I hadn’t considered at all. What importance did it have that this user was Russian? Was a Russian phone different? Well, let’s try it out and run a few more tests. If anything…

* Change the phone language to Russian.

Screenshot of language setting in Android. Russian and English. God help me when I have to switch this back to English.

* Open the app…

The bug

OMG THERE IT IS. FINALLY.

Doors Open

In a mix of frustration and happiness, I had managed to replicate the problem. Now I had more information to continue investigating, but it didn’t matter. Now I knew it was some kind of issue with character encoding, and all I wanted was to get this working.

Now I could search on Google and quickly found this Stack Overflow question.

In my code, one of the first routines was to load the device language to, if there’s a translation file available, load it as the app’s language.

public void updateAppLanguage(Context context) {
    String languageCode = Locale.getDefault().getDisplayLanguage();
    Locale locale = new Locale(languageCode);
    Locale.setDefault(locale);
    ...

If the device is in Russian, Locale.getDefault() returns “русский”, and it seems that SQLite doesn’t like this for some reason.

The definitive solution was to manually check this specific case:

public void updateAppLanguage(Context context) {
    String languageCode = Locale.getDefault().getDisplayLanguage();

    // Sql crashes at startup when the language is русский.
    // We switch to RU and manage it manually with languages.
    if (languageCode.equals("русский")) {
        languageCode = "ru";
    }

    Locale locale = new Locale(languageCode);
    Locale.setDefault(locale);
    ...

Three lines of code fixed the huge problem that had been giving me so many headaches. Once again.

I did more tests, and it seems that out of the over 100 languages Android supports, only Russian encoding broke SQLite. Not even Chinese, which we typically think of as the most complicated (and also I tested changing to this language much earlier before trying the other).

With the patch applied, I could finally release the update, and everyone was happy.

Message informing users that the bug has finally been fixed.

Conclusion

This bug has probably been one of the most frustrating ones I’ve dealt with. Two of the most influential factors were that I wasn’t familiar with native app development. The second factor was that the error itself was confusing and clarified very little. In fact, I’m not sure if I was at fault due to ignorance for not checking the charset, or if it was Android/SugarORM for not considering this case.

If you’re starting to develop an app, I don’t recommend any ORM for storing persistent data. Android launched its own (ROOM). Probably if I had known this from the beginning, I wouldn’t have had this problem.

And finally, if you ever find yourself lost and without a way out trying to find the bug in your program, ask it if it speaks Russian.


Update

After posting this on Hacker New, I received a lot of feedback, and thanks to the community, I was able to delve deeper into the issue to realize the colossal mistake I made when managing languages, which contributed to this problem.

I used getDisplayLanguage() instead of getLanguage(). The former returns the language text, while the latter returns the universal language code, which is what should be used in these cases. Well, “it was a miracle that this even worked.”

As I mentioned, I am not an Android developer, and due to haste and fatigue, I settled for what seemed to work initially without paying much attention to best practices or whether I was actually obtaining the language code or not. Additionally, the snippet shown is a simplification of a more complex system involving other services, persistence handling, and more, which made it less obvious. Thanks!