Копаемся в встроенном приложении камеры старого Xiaomi. Часть 1
История
Всем привет! Шел 2023 год, и я не на шутку решил задуматься о смене своего верного бойца Xiaomi Mi A1 2017 года выпуска на что-то посвежее. Об успешности этого старичка можно говорить много — начиная тем что он первый Xiaomi на Android One и заканчивая огромной популярностью в свои годы.
Но сегодня хочется поговорить о другом. О камере. Сразу отмечу, что камера у этого устройства не была его сильным преимуществом даже в далеком 2017 году.
Пример интерфейса камеры на Xiaomi Mi A1
Но за годы пользования мне приглянулся встроенный фильтр в стандартном приложении камеры — «Ломо».
Пожалуй, этот фильтр — настоящее спасение для многих снимков для камеры этого устройства и сопоставимых по качеству. Пролистав галерею я обнаружил, что четверть снимков была сделана именно с ним.
И вот купил я себе 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
Хочу обратить внимание на то, что изначальное название пакета — 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
А вот и наши кандидаты (и еще несколько ниже). Их всех нам нужно заменить на arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)
. Можно руками по одному вызову, а можно, например, заменой:
Замена в 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:
Седьмая попытка — в поисках фильтров
Наше положение стало несколько сложнее. Раньше нас за руку вели логи из-за которых падало приложение, и всегда был ориентир куда смотреть — какой класс указан при падении туда и смотрим. А сейчас мы остались один на один со всем приложением и 3.5 мб исходного кода.
В таких ситуациях нам нужны хоть какие-то зацепки, предлагаю за такую взять имя фильтра — «Ломо». Т.к. приложение у нас многоязычное, искать эту зацепку изначально следует в resources.arsc
, просматриваем ресурсы с помощью MT Manager:
Ищем ресурс через MT Manager
Наша первая зацепка — pref_camera_coloreffect_entry_instagram_rise
. Пробуем его искать в исходниках:
Нашли упоминание в исходнике 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
По непонятной причине на кнопку фильтра нужно нажимать по верхней границе, однако получилось! Нам удалось вернуть фильтр. Давайте сделаем первый снимок с ним:
Камера очень неожиданно закрылась при попытке снимка. Перезаходим в приложение и видим, что фото к сожалению, не было сохранено. А ведь мы были так близки к успеху!
Восьмая попытка — роднимся с 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! Конечное приложение можно скачать себе тут.
Надеюсь, Вы получили удовольствие от данного погружения. Ну, а в следующей статье попробуем приоткрыть завесу этого фильтра и разберемся в его внутреннем устройстве. До встречи!