Копаемся в встроенном приложении камеры старого Xiaomi. Часть 1

История

Всем привет! Шел 2023 год, и я не на шутку решил задуматься о смене своего верного бойца Xiaomi Mi A1 2017 года выпуска на что-то посвежее. Об успешности этого старичка можно говорить много — начиная тем что он первый Xiaomi на Android One и заканчивая огромной популярностью в свои годы.

Но сегодня хочется поговорить о другом. О камере. Сразу отмечу, что камера у этого устройства не была его сильным преимуществом даже в далеком 2017 году.

Пример интерфейса камеры на Xiaomi Mi A1

Пример интерфейса камеры на Xiaomi Mi A1

Но за годы пользования мне приглянулся встроенный фильтр в стандартном приложении камеры — «Ломо».

84d0cf0be0b2e9f4145a5fde9b96132d.jpg

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

И вот купил я себе Redmi Note 12 Pro, вроде и камера получше, и фильтры всякие разные, а того самого привычного «Ломо» мне стало очень не хватать. Ну, значит попытаемся его портировать!

Инструменты

Для доведения дела до конца мне понадобились следующие инструменты –

1. MT Manager — удобный базовый транслятор apk в smali инструкции, а также инструмент для подписи приложения

2. APK Editor — простой инструмент для работы с apk (переименование, замена ресурсов)

3. ADB — отладочный мост для андроид

4. ApkDecompiler — онлайн сервис для декомпиляции приложений

5. Patchelf — утилита для
переименования символов в нативных библиотеках

Первая попытка — попытаемся установить в лоб

Сам файл приложения можно с легкостью сковырнуть по пути /system/system/priv-app/MiuiCamera/MiuiCamera.apk на смартфоне Mi A1. Исходный файл приложения можно найти тут.

Приступим к установке?

Вот и установили...

Вот и установили…

Получаем ожидаемый результат — совпадение имен пакетов apk, из-за чего установка не удалась. Заметим, что на новеньком Redmi приложение имеет не только одинаковое название пакета, но и является приложением потомком, если так можно сказать, приложения установленном на нашем старичке.

Вторая попытка — переименовываем пакет

У нас на пути проблема — имя пакета уже занято в списке установленных приложений. Первое очевидное ее решение –, а давайте переименуем пакет для нашего приложения? Используя APK Editor так и поступим.

Окно редактирования пакета в ApkEditor

Окно редактирования пакета в ApkEditor

Хочу обратить внимание на то, что изначальное название пакета — com.android.camera, а заменить мы его пытаемся на com.android.camerb, тем самым поменяв лишь одну букву. Вообще, принцип минимальных изменений предпочтителен в патчах, т.к. чем меньше мы изменили, тем меньше шансы что мы что-то сломаем (даже в случае переименования, ведь никто не гарантирует что где-то мы не собьем привязку к длине имени пакета, например). Об этом нам также дружелюбно напоминает окно Apk Editor.

После переименования пакета мы собьем в нем хеш-суммы для файлов classes.dex и AndroidManifest.xml, что значит невозможность его повторной установки. Благо эту проблему нельзя назвать серьезной — можно заново подписать приложение используя MT Manager прямо на устройстве.

Пункт меню

Пункт меню «Подпись» в приложении MT Manager

После подписи новый apk файл будет создан прямо в папке с изначальным пакетом. Пытаемся установить приложение, запускаем и…

Приложение вылетело еще до того, как мы хоть что-то успели увидеть

Приложение вылетело еще до того, как мы хоть что-то успели увидеть

Третья попытка — заглядываем внутрь

Чтобы выяснить конкретную проблему падения приложения включаем на смартфоне режим разработчика и отладку по USB. На компьютер устанавливаем средство ADB и начинаем наше расследование.

Запускать ADB будем прямо из консоли Powershell. После того как подключили смартфон по USB и подтвердили сопряжение ADB, запускаем команду на отлавливание ошибок в Powershell — .\adb logcat *:E > err1.txt. Теперь снова пытаемся запустить приложение. Камера закономерно вылетела, как и раньше, но теперь у нас есть больше улик в файле err1.txt. Останавливаем запись логов чтобы не флудить (банальным Ctrl+C) в Powershell и отправляемся на раскопки.

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

Сделав поиск по ключевому слову camerb мы увидим в файле логов следующее:

E/AndroidRuntime( 8734): FATAL EXCEPTION: Thread-12
E/AndroidRuntime( 8734): Process: com.android.camerab, PID: 8734
E/AndroidRuntime( 8734): java.lang.NumberFormatException: null
E/AndroidRuntime( 8734): 	at java.lang.Integer.parseInt(Integer.java:483)
E/AndroidRuntime( 8734): 	at java.lang.Integer.parseInt(Integer.java:556)
E/AndroidRuntime( 8734): 	at android.hardware.Camera$Parameters.getInt(Camera.java:2611)
E/AndroidRuntime( 8734): 	at android.hardware.Camera$Parameters.getJpegThumbnailSize(Camera.java:2728)
E/AndroidRuntime( 8734): 	at com.android.camerab.module.CameraModule.updateCameraParametersPreference(CameraModule.java:3877)
E/AndroidRuntime( 8734): 	at com.android.camerab.module.CameraModule.setCameraParameters(CameraModule.java:4185)
E/AndroidRuntime( 8734): 	at com.android.camerab.module.CameraModule$CameraStartUpThread.run(CameraModule.java:376)

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

Для декомпиляции я буду использовать бесплатный онлайн ресурс http://www.javadecompilers.com/apk. Переходим на сайт и выгружаем наше приложение. В моем случае весь процесс декомпиляции занял меньше минуты и вот у нас на руках весь исходный java код.

Сразу направляемся к проблемному месту — вызову getJpegThumbnailSize() внутри метода updateCameraParametersPreference() в классе com.android.camerb.module.CameraModule.

if (21 <= Build.VERSION.SDK_INT) {
    Camera.Size optimalSize2 = Util.getOptimalJpegThumbnailSize(this.mParameters.getSupportedJpegThumbnailSizes(), ((double) pictureSize.width) / ((double) pictureSize.height));
    if (!this.mParameters.getJpegThumbnailSize().equals(optimalSize2)) {
        this.mParameters.setJpegThumbnailSize(optimalSize2.width, optimalSize2.height);
    }
    Log.v("Camera", "Thumbnail size is " + optimalSize2.width + "x" + optimalSize2.height);
}

Из комментариев и общей структуры становится понятно, что блок выше — есть установка нужного размера превьюшек для камеры, причем вызываем мы этот блок только с 5.0 андроида и выше.

Мне в начале вообще показалось, что тут ошибка в условии, и должно быть 21 >= Build.VERSION.SDK_INT, ведь Google пометил этот класс параметров устаревшим начиная с 21 API.

В любом случае, учитывая, что вся работа блока — это установка размеров превью, думаю, что мы можем просто вырезать данную логику без последствий, ведь без изначального значения это поле не останется.

Для точечного патчинга будем править smali байткод с помощью того-же MT Manager прямо с нашего смартфона.

Заглядываем в соответствующий класс и видим знакомый If:

.line 3870
sget v25, Landroid/os/Build$VERSION;->SDK_INT:I

const/16 v26, 0x15
move/from16 v0, v26
move/from16 v1, v25

if-gt v0, v1, :cond_1fb

Последняя инструкция как раз выполняет прыжок за пределы блока If, проверяя что v0 (ака v26 ака 0×15 ака 21) больше чем v1 (ака v25 ака SDK_INT). Давайте для простоты заменим ее на if-nez v0, :cond_1fb. Что фактически будет делать переход всегда, ведь 21 всегда не равно нулю.

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

Нажимаем сохранить, компилировать, пересобрать и вот у нас уже пропатченное приложение в котором пропущен проблемный участок.

Ну теперь нам должно повезти? Устанавливаем приложение и пытаемся его запустить…

Очень жаль, что снова…

Очень жаль, что снова…

Четвертая попытка — странности ArrayCopy

Повторная отладка показывает нам в чем проблема:

E AndroidRuntime: Process: com.android.camerb, PID: 27794
E AndroidRuntime: java.lang.IllegalAccessError: Method 'void java.lang.System.arraycopy(int[], int, int[], int, int)' is 
                  inaccessible to class 'com.android.camerb.IntArray' (declaration of 'com.android.camerb.IntArray' appears 
                  in /data/app/~~UjUVTw3OgP_-lbbsTgEHZg==/com.android.camerb-zsFlOpkreTHIR8orTBjIsQ==/base.apk)
E AndroidRuntime: 	at com.android.camerb.IntArray.toArray(IntArray.java:42)
E AndroidRuntime: 	at com.android.camerb.preferences.IconListPreference.filterUnsupported(IconListPreference.java:115)
E AndroidRuntime: 	at com.android.camerb.ui.HdrButton.filterPreference(HdrButton.java:211)
E AndroidRuntime: 	at com.android.camerb.ui.HdrButton.initializeXml(HdrButton.java:46)
E AndroidRuntime: 	at com.android.camerb.ui.HdrButton.onCreate(HdrButton.java:62)
E AndroidRuntime: 	at com.android.camerb.ui.V6RelativeLayout.onCreate(V6RelativeLayout.java:35)
E AndroidRuntime: 	at com.android.camerb.ui.V6ModuleUI.onCreate(V6ModuleUI.java:29)
E AndroidRuntime: 	at com.android.camerb.ui.UIController.onCreate(UIController.java:65)
E AndroidRuntime: 	at com.android.camerb.module.CameraModule.onCreate(CameraModule.java:1840)
E AndroidRuntime: 	at com.android.camerb.Camera.onCreate(Camera.java:95)

Подождите, но ведь System.arraycopy это статичный публичный метод. Как он может быть недоступен из какого-то класса?

Оказывается, может, если имеет неверную сигнатуру. Дело в том, что если обратиться к исходникам андроид нацеленным на 21 API (которое имеет наш старенький Mi A1), то мы обнаружим, что там эта перегрузка arraycopy(int[], int, int[], int, int)действительно присутствует, и имеет видимость public. Однако если глянуть тот-же метод, но уже в 30 API (который использует наш преемник — новенький Redmi Note 12), то там есть только канонический arraycopy(Object[], int, Object[], int, int), а все аналогичные перегрузки уже помечены private, поэтому и недоступны нам.

Для нас это значит, что придется заменить все подобные перегрузки на канонический arraycopy(Object[], int, Object[], int, int). Приступим! Открываем MT Manager:

Поиск в Smali коде через MT Manager

Поиск в Smali коде через MT Manager

А вот и наши кандидаты (и еще несколько ниже). Их всех нам нужно заменить на arraycopy(Ljava/lang/Object;ILjava/lang/Object;II). Можно руками по одному вызову, а можно, например, заменой:

Замена в Smali коде

Замена в Smali коде

Аналогичную замену проводим для arraycopy([BI[BII).

Пытаемся собрать и установить приложение. Запускаем… И видим падение (

Пятая попытка — заглядываем еще дальше

Проделываем трюк с отловом ошибки еще раз и в этот раз видим уже другую проблему. Движение есть, но пока мы не знаем в какую сторону)

E/AndroidRuntime(16927): java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
E/AndroidRuntime(16927): 	at com.android.camerb.ui.V6SettingsStatusBar.updateZoom(V6SettingsStatusBar.java:133)
E/AndroidRuntime(16927): 	at com.android.camerb.ui.V6SettingsStatusBar.updateStatus(V6SettingsStatusBar.java:100)
E/AndroidRuntime(16927): 	at com.android.camerb.ui.V6SettingsStatusBar.onCameraOpen(V6SettingsStatusBar.java:168)
E/AndroidRuntime(16927): 	at com.android.camerb.ui.V6ModuleUI.onCameraOpen(V6ModuleUI.java:56)
E/AndroidRuntime(16927): 	at com.android.camerb.ui.UIController.onCameraOpen(UIController.java:93)
E/AndroidRuntime(16927): 	at com.android.camerb.module.CameraModule$MainHandler.handleMessage(CameraModule.java:473)
E/AndroidRuntime(16927): 	at android.os.Handler.dispatchMessage(Handler.java:102)
E/AndroidRuntime(16927): 	at android.os.Looper.loop(Looper.java:154)
E/AndroidRuntime(16927): 	at android.app.ActivityThread.main(ActivityThread.java:6176)

В этот раз приложение пытается получить элемент в списке который равен null. Переходим в соответствующий исходный файл и видим следующую картину:

public void updateZoom() {
    Camera.Parameters parameters = CameraManager.instance().getStashParameters();
    if (((((ActivityBase) this.mContext).getUIController().getZoomButton().getVisibility() == 8) ||
     CameraSettings.isSwitchCameraZoomMode()) && parameters != null) {
        int value = parameters.getZoomRatios().get(CameraSettings.readZoom(CameraSettingPreferences.instance())).intValue();
        if (value > 100) {
            int value2 = value / 10;
            String text = "x " + (value2 / 10) + "." + (value2 % 10);
            boolean same = this.mZoomTextView.getText().equals(text);
            this.mZoomTextView.setText(text);
            setSubViewVisible(this.mZoomTextView, 0, same);
            return;
        }
    }
    setSubViewVisible(this.mZoomTextView, 8, true);
}

Проблема происходит на 5 строчке, getZoomRatios() возвращает null из API. Вероятно, дело опять в методах которые помечены устаревшими. Мы видим, что все для чего используется получаемое значение, это для отрисовки текста в области статуса во время зума, например, в виде «x 2.5» в случае приближения в 2.5 раза.

Предлагаю применить тот-же подход и просто вырезать эту логику, чтобы не встречать ее вновь. Видите этот && parameters != null в конце условия? Давайте переделаем его чтобы оно перестало выполняться. Отправляемся в MT Manager:

.line 130
.local v0, "parameters":Landroid/hardware/Camera$Parameters;

...

if-nez v5, :cond_25

invoke-static {}, Lcom/android/camerb/CameraSettings;->isSwitchCameraZoomMode()Z

move-result v8

if-eqz v8, :cond_7e

:cond_25
if-eqz v0, :cond_7e

Последняя инструкция как раз отвечает за часть условия && parameters != null. Заменяем ее инструкцией goto :cond_7e, чтобы переход был безусловным. Отметим, что метка :cond_7e смотрит ровно за пределы тела If, отсюда и ее неоднократное упоминание выше (которое нужно чтобы «соскочить» если левый операнд && условия будет нулем).

Традиционно сохраняем, компилируем, подписываем, устанавливаем. Что нас ждет в этот раз? Сейчас узнаем — запускаем…

Уже что-то!

Уже что-то!

Шестая попытка — warning, не значит необязательный

У нас 2 новости. Хорошая — мы увидели интерфейс приложения, оно таки запустилось благодаря нашим правкам. Плохая — приложение камеры работает не показывая изображение с камеры.

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

W/System.err(22665): android.provider.Settings$SettingNotFoundException: user_rotation
W/System.err(22665): 	at android.provider.Settings$System.getIntForUser(Settings.java:2046)
W/System.err(22665): 	at android.provider.Settings$System.getInt(Settings.java:2036)
W/System.err(22665): 	at com.android.camerb.Util.checkLockedOrientation(Util.java:507)
W/System.err(22665): 	at com.android.camerb.Camera.onWindowFocusChanged(Camera.java:370)
W/System.err(22665): 	at com.android.internal.policy.DecorView.onWindowFocusChanged(DecorView.java:1457)
W/System.err(22665): 	at android.view.View.dispatchWindowFocusChanged(View.java:10258)

В этот раз устройство не может считать значение настройки user_rotation которая отвечает за ориентацию экрана. Благо мы без труда вместо чтения этой настройки можем подсунуть одну из констант, описанных в документации, а именно — 0.

Открываем исходный код чтобы прикинуть правки:

public static void checkLockedOrientation(Activity activity) {
    try {
        if (Settings.System.getInt(activity.getContentResolver(), "accelerometer_rotation") == 0) {
            mLockedOrientation = Settings.System.getInt(activity.getContentResolver(), "user_rotation");
        } else {
            mLockedOrientation = -1;
        }
    } catch (Settings.SettingNotFoundException e) {
        e.printStackTrace();
    }
}

Ошибка происходит в 4 строке. Давайте заменим попытку получения значения user_rotation на константный 0. Посмотрим, что из себя представляет этот метод в Smali:

.line 507
invoke-virtual {p0}, Landroid/app/Activity;->getContentResolver()Landroid/content/ContentResolver;

move-result-object v2

.line 508
const-string/jumbo v3, "user_rotation"

.line 507
invoke-static {v2, v3}, Landroid/provider/Settings$System;->getInt(Landroid/content/ContentResolver;

move-result v2

sput v2, Lcom/android/camerb/Util;->mLockedOrientation:I

Последние 4 инструкции получают значение user_rotation временно храня его в v2, потом перекладывая в переменную mLockedOrientation, ради которой и написан этот метод.

Заменим попытку получения user_rotation на константу 0:

.line 507
invoke-virtual {p0}, Landroid/app/Activity;->getContentResolver()Landroid/content/ContentResolver;

move-result-object v2

.line 508
const-string/jumbo v3, "user_rotation"

.line 507
const/4 v2, 0x0

sput v2, Lcom/android/camerb/Util;->mLockedOrientation:I

С помощью const/4 присваиваем v2 значение 0.

Пришло время испытать удачу еще раз. Устанавливаем, запускаем.

Успех! Мы видим изображение с камеры.

Успех! Мы видим изображение с камеры.

И вроде все хорошо, но подождите-ка, ради чего мы все это начали? Ради фильтра. А кнопки фильтров то и нет! А она должна быть слева снизу, вот где она была на Mi A1:

73528db418ec86574b018412d50da542.png

Седьмая попытка — в поисках фильтров

Наше положение стало несколько сложнее. Раньше нас за руку вели логи из-за которых падало приложение, и всегда был ориентир куда смотреть — какой класс указан при падении туда и смотрим. А сейчас мы остались один на один со всем приложением и 3.5 мб исходного кода.

В таких ситуациях нам нужны хоть какие-то зацепки, предлагаю за такую взять имя фильтра — «Ломо». Т.к. приложение у нас многоязычное, искать эту зацепку изначально следует в resources.arsc, просматриваем ресурсы с помощью MT Manager:

Ищем ресурс через MT Manager

Ищем ресурс через MT Manager

Наша первая зацепка — pref_camera_coloreffect_entry_instagram_rise. Пробуем его искать в исходниках:

e5799662990ed45c31012a23a3925b04.png

Нашли упоминание в исходнике EffectController.java, посмотрим поподробнее:

if (addEntry) {
    addEntryItem(R.string.pref_camera_coloreffect_entry_instagram_rise, id);
    this.mEffectImageIds.add(Integer.valueOf(R.drawable.camera_effect_image_instagram_rise));
    this.mEffectKeys.add("effect_instagram_rise_picture_taken_key");
}

Можем предположить, что он занимается добавлением элемента фильтра на панель. Но почему самой панели не видно? Давайте попробуем поискать ответ в начале метода:

public RenderGroup getEffectGroup(GLCanvas canvas, RenderGroup renderGroup, boolean wholeRender, boolean isSnapShotRender, int index) {
        boolean matchPartRender0;
        boolean matchPartRender1;
        Render gradienterEffectRender;
        if (!Device.isSupportedShaderEffect()) {
            return null;
        }
        boolean addEntry = canvas == null;
        boolean initOne = false;
        if (canvas == null) {
          ...

Видимо это метод который возвращает всю панель с эффектами. Подождите! А что это за первое условие у нас тут? Посмотрим в реализацию этой функции isSupportedShaderEffect():

public static boolean isSupportedShaderEffect() {
    return FeatureParser.getBoolean("support_camera_shader_effect", false);
}

Мы опять имеем дело с своего рода настройками, хорошо хоть эта не выкидывает исключение (хотя так мы нашли бы ее быстрее методом выше). Давайте попробуем интереса ради заменить возвращаемое значение на true, для этого посмотрим на метод в переваренном виде:

.method public static isSupportedShaderEffect()Z
.registers 2

.prologue

.line 102
const-string/jumbo vO, "support_camera_shader_effect"

const/4 v1, 0x0

invoke-static {v0, v1}, Lcom/android/camerb/aosp_porting/FeatureParser;->getBoolean(Ljava/lang/String;Z)

move-result v0

return v0
.end method

Просто заменим его на константный true:

.method public static isSupportedShaderEffect()Z
.registers 2

.prologue
.line 102
const-string/jumbo vO, "support_camera_shader_effect"

const/4 v1, 0x1

return v1
.end method

Пробуем собрать и установить. Запускаем:

Скриншот с Redmi

Скриншот с Redmi

По непонятной причине на кнопку фильтра нужно нажимать по верхней границе, однако получилось! Нам удалось вернуть фильтр. Давайте сделаем первый снимок с ним:

92855b0efc82ee080f8d1c6127941264.gif

Камера очень неожиданно закрылась при попытке снимка. Перезаходим в приложение и видим, что фото к сожалению, не было сохранено. А ведь мы были так близки к успеху!

Восьмая попытка — роднимся с native библиотеками

Остался последний рубеж! Вновь отлавливаем ошибку и в этот раз наблюдаем следующую картину:

E/Camera  (10251): ShaderNativeUtil load CameraEffectJNI.so failed.
E/Camera  (10251): java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.android.camerb-1/base.apk"],nativeLibraryDirectories=[/data/app/com.android.camerb-1/lib/x86, /system/lib, /vendor/lib]]] couldn't find "libCameraEffectJNI.so"
E/Camera  (10251): 	at java.lang.Runtime.loadLibrary0(Runtime.java:984)
E/Camera  (10251): 	at java.lang.System.loadLibrary(System.java:1562)
E/Camera  (10251): 	at com.android.camerb.effect.ShaderNativeUtil.(ShaderNativeUtil.java:14)
E/Camera  (10251): 	at com.android.camerb.effect.ShaderNativeUtil.initTexture(ShaderNativeUtil.java:26)

Видимо мы совсем забыли про нативные библиотеки. Дело в том, что для системных приложений бывают случаи, когда нужные библиотеки хранятся в /system/system/lib вместо папки lib внутри самого пакета приложения.

И когда мы скопировали пакет приложения из папки /system/system/priv-app/MiuiCamera/MiuiCamera.apk мы забыли скопировать библиотеки.

Какой у нас план действий? Изначально я хотел скопировать нужную библиотеку из системной папки и потом также совершить несколько набегов из-за зависимостей других библиотек этой библиотекой. Но потом я вспомнил про то, что на новеньком Redmi это приложение является «продолжением» старого приложения Mi A1, и я решил его подробнее изучить.

Процесс декомпиляции показал, что по сути, новое приложение — это сильно нарощенное старое. Более того, в нем даже остался весь исходный код отвечающий за эти старинные фильтры типа «Ломо», но весь этот функционал был отключен, и как его включить я не знаю. И о чудо — в пакете камеры от Redmi присутствует папка lib с знакомой нужной нам библиотекой libCameraEffectJNI.so.

Предлагаю особо не копаясь просто скопировать всю папку lib в старое приложение и попробовать запустить.

После установки и запуска мы вновь получаем ошибку, все еще связанную с нативными библиотеками:

E/AndroidRuntime( 5032): java.lang.UnsatisfiedLinkError: No implementation found for int[] com.android.camerb.effect.ShaderNativeUtil.initJpegTexture(byte[], int, int) (tried Java_com_android_camerb_effect_ShaderNativeUtil_initJpegTexture and Java_com_android_camerb_effect_ShaderNativeUtil_initJpegTexture___3BII)
E/AndroidRuntime( 5032): 	at com.android.camerb.effect.ShaderNativeUtil.initJpegTexture(Native Method)
E/AndroidRuntime( 5032): 	at com.android.camerb.effect.ShaderNativeUtil.initTexture(ShaderNativeUtil.java:26)
E/AndroidRuntime( 5032): 	at com.android.camerb.effect.renders.SnapshotEffectRender$EGLHandler.applyEffect(SnapshotEffectRender.java:743)
E/AndroidRuntime( 5032): 	at com.android.camerb.effect.renders.SnapshotEffectRender$EGLHandler.drawMainJpeg(SnapshotEffectRender.java:817)
E/AndroidRuntime( 5032): 	at com.android.camerb.effect.renders.SnapshotEffectRender$EGLHandler.handleMessage(SnapshotEffectRender.java:622)

На этот раз мы видим, что библиотека вроде как подгрузилась, а вот нужный метод в ней java часть найти не может. Если обратиться к логу, то мы увидим попытку доступа к символу (методу) Java_com_android_camerb_* в нативной библиотеке. Однако библиотека вряд ли содержит такой символ, ведь camerb это наше переименование, а в библиотеке скорее всего есть символ Java_com_android_camera_*, к которому и пытается получить доступ приложение.

Касательно этого символа initJpegTexture — он использован чтобы конвертировать GL текстуру в jpeg поток байт (чтобы затем этот поток сохранить в файл).

Для перенаправления есть два пути — первый, это написать промежуточную нативную библиотеку с переименованными «camerb» символами (например, libCameraEffectJNIProxy.so), которую и загрузить через System.loadLibrary. А уже внутри этой поддельной библиотеки внутри поддельных символов дергать настоящие символы из оригинальной библиотеки. Такой метод был бы предпочтителен, когда нам крайне нежелательно как-то трогать файл библиотеки.

Мы же будем использовать второй метод — воспользуемся утилитой для переименования символов Patchelf. Использование очень простое — нужно создать файл в котором в каждой строке будет пара значений типа <текущее название символа> <новое название символа>. Чтобы узнать какие символы присутствуют в нашей библиотеке можно использовать онлайн сервис Elfy. По итогу у меня получился примерно такой файл (для каждого символа я лишь заменил «a» на «b» в слове camera):

Java_com_android_camera_effect_ShaderNativeUtil_readGraphicBuffer Java_com_android_camerb_effect_ShaderNativeUtil_readGraphicBuffer
Java_com_android_camera_effect_ShaderNativeUtil_texChannelY Java_com_android_camerb_effect_ShaderNativeUtil_texChannelY
Java_com_android_camera_effect_ShaderNativeUtil_initJpegTexture Java_com_android_camerb_effect_ShaderNativeUtil_initJpegTexture
Java_com_android_camera_effect_ShaderNativeUtil_mergeWaterMarkRangeAlgo Java_com_android_camerb_effect_ShaderNativeUtil_mergeWaterMarkRangeAlgo
Java_com_android_camera_effect_ShaderNativeUtil_nv21CompressToJpeg Java_com_android_camerb_effect_ShaderNativeUtil_nv21CompressToJpeg
Java_com_android_camera_effect_ShaderNativeUtil_resizeGraphicBuffer Java_com_android_camerb_effect_ShaderNativeUtil_resizeGraphicBuffer
Java_com_android_camera_effect_ShaderNativeUtil_getWaterMarkRange Java_com_android_camerb_effect_ShaderNativeUtil_getWaterMarkRange
...

Запускаем утилиту командой patchelf --output libPatched.so --rename-dynamic-symbols map_file.txt libCameraEffectJNI.so и в файле libPatched.so получаем нашу новую пропатченную библиотеку, которую и копируем в lib папку.

Теперь пытаемся установить и запустить наше приложение…

Удалось сделать снимок!

Удалось сделать снимок!

Итог

Успех! С восьмой (ну или чуть больше) попытки мы смогли портировать любимый фильтр с старого устройства на новое. Конечно и UI выглядит странно (с высокой черной полоской снизу), и остальные фичи вроде панорамы в нем не работают (встречая нас уже знакомой проблемой не переименованных символов), но ведь и цель у нас была совсем другая. Вероятно, это первый Redmi Note 12 Pro который сделал фотографию с старым фильтром Mi A1! Конечное приложение можно скачать себе тут.

Надеюсь, Вы получили удовольствие от данного погружения. Ну, а в следующей статье попробуем приоткрыть завесу этого фильтра и разберемся в его внутреннем устройстве. До встречи!

© Habrahabr.ru