Сложности перевода: баг, который говорил по-русски и ломал моё приложение

в 9:00, , рубрики: android, java, lipo manager, orm, ruvds_перевод, sqlite, Веб-разработка, локализация приложений

Сложности перевода: баг, который говорил по-русски и ломал моё приложение - 1


Шпион всматривается в экраны

Несколько лет назад я работал над Lipo Manager, добавляя кое-какие долгожданные функции. Это довольно простое приложение, но вполне достаточное для управления батареями LiPos. Некоторые из вносимых мной изменений отвечали запросу сообщества. Это были визуальные доработки, оптимизация, мультиязычность, обновления зависимостей и исправление периодически возникавших исключений нулевого указателя.

Со всеми этими задачами я справился за день и, проведя несколько тестов, выпустил новую версию...

▍ «Не могу войти в приложение»

Спустя несколько дней мне в Telegram написал один из пользователей:

Сложности перевода: баг, который говорил по-русски и ломал моё приложение - 2

«Я обновил телефон, и приложение перестало работать».

Хмм…

Каждый разработчик знает, что для устранения бага сначала его нужно попытаться воспроизвести, для чего желательно знать среду, в которой он происходит. Я запросил у пользователя информацию о его системе, которую он с готовностью предоставил.

Первым делом я хотел знать, не являлась ли его версия Android слишком новой (бетой) или слишком старой. Мне нужно было проверить, не произошла ли ошибка в версии, с которой я приложение не тестировал, и нет ли проблем с библиотекой, которую использует приложение. К моему удивлению, его телефон работал на Android 13. Именно та версия и API, с которыми я в основном всё и тестировал.

Нужно было копать глубже.

▍ Проверка логов в Play Console

Google предоставляет разработчикам множество инструментов для управления приложениями, опубликованными в Play Store. Один из них — это Android Vitals. Он собирает информацию о каждом установленном приложении, и в случае исключений, сохраняет все трейсы выполнения, делая их доступными для разработчика наряду со множеством дополнительных деталей.

Я не буду давать комментарий по поводу того, нарушает ли это конфиденциальность, но при возникновении проблем будет весьма непрактичным просить пользователя подключиться к телефону через ADB (Android Debug Bridge), извлечь трейсы и отправить эту информацию вам. Так что в целом это очень полезный инструмент.

Полученные с устройств пары пользователей логи показали следующее:

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

Проблема с базой данных? Сначала я подумал, что проблема может быть связана с обновлением SugarORM. Эта библиотека объектно-реляционного отображения используется приложением для управления базой данных. Однако, поскольку у меня никаких проблем не было, я сомневался, что проблема непосредственно в ней. Что же являлось причиной?

▍ «Одиссея» началась

Хорошо. Нам нужно просто воспроизвести проблему и отследить её вплоть до изначального бага, так? Элементарно. Банальщина. Много времени не займёт.

15 часов спустя

(К сожалению) всё оказалось в норме. Ничто не могло вызвать сбой. Этого я и ожидал.

Поиск в интернете ситуацию особо не прояснял, так как все указывали на причины, вроде проблем с правами доступа к файлам или сбоев синхронизации. Но в моём случае ничто из этого не было похоже на причину и особого смысла не имело. Хотя уверенности у меня ни в чём не было, так как воспроизвести проблему не получалось.

А поскольку воспроизвести её не получалось, то и откатить изменения я не мог. В первую очередь потому что не знал, давно ли эта проблема вообще появилась.

В Google тоже предоставили мне версию Android, модель телефона и так далее. Но даже с помощью эмуляции точных прежних характеристик мне не удалось что-либо выяснить.

К этому моменту я уже попробовал сломать приложение всеми возможными способами и, как это ни парадоксально, меня даже бесило, что оно такое надёжное. Я повреждал базу данных; вставлял тысячи записей; изолировал всё, что связано с БД, от остального приложения; многократно симулировал смену версий…но ничто из этого не вызывало проблему, которая меня интересовала.

Метод BatteryService.loadAllBatteries() был слишком прост и не содержал особой логики. Кроме того, при старте приложения он запускался одним из первых, поэтому поводов для возникновения состояния гонки или чего-то подобного здесь было мало.

С каждым разом степень моего отчаяния росла, и я ещё раз заглянул в логи ADB. Это сообщение казалось всё более оскорбительным:

SQLiteException: not an Error. <i>Всё в порядке</i>.

Тут я понял, что застрял. Я потратил слишком много времени на всё это и начал испытывать чувство поражения, не зная, что ещё пробовать.

Решив на этом остановиться, я начал всё закрывать…

▍ Поворот сюжета

Мне оставалось закрыть всего несколько окон, когда перед моими глазами оказалась страница Google Play. Тогда я решил последний раз взглянуть на детали устройства того пользователя с мыслью, что мог упустить какой-то нюанс. После всех этих страданий я уже почти заучил их на память: устройство, версии, трейсы, установленные приложения, функциональность, страна…страна?

Вот страну пользователя я как раз проглядел, и она оставалась единственным фактором, который я совсем не рассматривал. Какое значение могло иметь то, что пользователь русский? Неужели русский телефон чем-то отличался? Что ж, попробуем провести ещё несколько проверок. Мало ли…

  • Меняем язык телефона на русский.
    Сложности перевода: баг, который говорил по-русски и ломал моё приложение - 3

    Боже, помоги мне, когда нужно будет сменить его обратно на английский.

  • Открываем приложение…
Сложности перевода: баг, который говорил по-русски и ломал моё приложение - 4

Неужели! Вот оно, наконец!

▍ Врата отворились

Испытывая смесь негодования и радости, я смог воспроизвести проблему. Теперь я получил дополнительную информацию для продолжения расследования, но это было уже не важно. Я понимал, что проблема заключалась в кодировке символов, и хотел просто заставить приложение исправно работать.

Теперь запрос к Google быстро привёл меня к этому вопросу на Stack Overflow.

В моём коде одной из первых процедур шла загрузка приложения с использованием языка устройства. И при наличии нужного файла с переводом, программа загружалась именно на этом языке.

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

Если устройство работает на русском, Locale.getDefault() возвращает "русский”, что по какой-то причине не нравится SQLite.

Итоговым решением стала ручная проверка этого конкретного случая:

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

    // Sql падает при запуске, когда язык установлен на «русский».
    // Меняем его на RU и устанавливаем вручную.
    if (languageCode.equals("русский")) {
        languageCode = "ru";
    }

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

Три строки кода исправили огромную проблему, которая принесла мне столько головной боли. Опять.

Я провёл дополнительные тесты и оказалось, что из почти 100 поддерживаемых Android языков только кодировка русского вызывала сбой SQLite. Заметьте, не китайского, который мы считаем одним из самых сложных (к тому же я проверил переключение на этот язык гораздо раньше всего остального).

После применения патча я, наконец, выкатил обновление, и все были счастливы.

Сложности перевода: баг, который говорил по-русски и ломал моё приложение - 5
Перевод

Ребята, приношу извинения за то, что приложение так долго не работало.

Я в течение многих часов пытался выяснить, в чём проблема, потому что воссоздать ошибку не получалось, а предоставленная Play Store информация была недостаточно ясной. Было похоже на какую-то чёрную магию.

Только сегодня утром я, НАКОНЕЦ, выяснил, в чём было дело. Приложение падает, когда язык телефона установлен на «Русский». Почему? Честно говоря, понятия не имею.
Похоже, его кодировка как-то влияет на запросы к базе данных.

Обновлённая версия будет готова через несколько часов :')
Ещё раз извиняюсь за задержку.

▍ Заключение

Этот баг стал, пожалуй, самым неприятным из всех, с какими мне приходилось иметь дело. Здесь я оказался под влиянием двух основных усложняющих факторов. Во-первых, я не знаком с нативной разработкой приложений. Во-вторых, сама ошибка сильно сбивала с толку, никак не проясняя своей причины. По правде говоря, я не уверен, кто конкретно виноват в этой проблеме: то ли я, так как не проверил кодировку символов, то ли Android/SugarORM, так как не учли этот случай.

Если вы начинаете разработку приложения, не советую использовать для хранения постоянных данных ORM. Android запустил собственный инструмент (ROOM). Возможно, если бы я знал это изначально, то и проблемы бы не возникло.

И напоследок. Если вы вдруг окажетесь в подобном недоумении при тщетных попытках найти баг в своей программе, то спросите, не говорит ли он по-русски.

▍ Обновление

После размещения этой истории на Hacker News я получил много обратной связи, и, благодаря сообществу, смог более глубоко рассмотреть проблему. В итоге стало ясно, что при реализации управления языками я допустил большую ошибку, которая и стала её причиной.

Я использовал getDisplayLanguage() вместо getLanguage(). Первая возвращает текстовую форму названия языка, а вторая — языковой код, который и должен использоваться в таких случаях. Можно сказать «чудо, что такое решение вообще сработало».

Ещё раз повторюсь, я не Android-разработчик, так что под влиянием спешки и усталости выбрал вроде бы рабочий вариант, не уделив достаточно внимания изучению лучших практик или выяснению, получаю ли я конкретно код языка. Кроме того, представленный фрагмент программы — это упрощённый вид более сложной системы, включающей другие сервисы, обработку персистентности и прочее, что сделало причину проблемы менее очевидной. Спасибо!

Автор: Дмитрий Брайт

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js