Пролог: покупка, которая казалась удачной
Всё началось с приобретения терминала сбора данных Urovo DT40 Pro. Выбирал я его осознанно: три физические кнопки сканера — основная и две боковые, Android 12 из коробки, хороший аккумулятор. По характеристикам — неплохая рабочая лошадка для склада или торгового зала.
Задача стояла простая: настроить интеграцию с «Магазином 15» от компании Cleverence. На сайте Cleverence в списке поддерживаемых устройств значилась модель DT40 (без приставки Pro). «Ну, — подумал я, — Pro-версия — это же просто небольшое обновление, наверняка всё совместимо».
Приложение установилось без проблем. Открываю — и в настройках вижу грустное сообщение: «No built-in barcode scanner found or integrated», т.е. встроенный сканер не определился софтом.
Сканер работал только в режиме эмуляции клавиатуры: наводишь на штрихкод, нажимаешь кнопку, и цифры просто вбиваются в активное текстовое поле, как если бы ты печатал их на обычной клавиатуре. Никакой программной интеграции, никакой обработки триггеров, никакой фоновой передачи данных.
Дальше — классика жанра. Переписка с поддержками Urovo и Cleverence не дала результатов. Производитель железа кивает на софт, разработчик софта — на железо не из списка совместимых. А терминал уже куплен, и он должен работать. Пришлось брать дело в свои руки. То, что началось как рядовая настройка, превратилось в детектив с перехватом системных событий, рутированием, изучением материалов сайта Cleverence и подменой идентификаторов устройства.
В статье описан путь поиска решения. Кому нужен финальный результат — сразу переходите к Части 7.
Часть 1. Почему не остановились на стандартном клавиатурном вводе?
Режим эмуляции клавиатуры, в котором сканер работал из коробки, устроен примитивно: сканер притворяется обычной USB-клавиатурой, «нажимающей» цифры штрихкода и Enter в конце. Для простых задач это годится, но для товароучёта — нет. Представьте: оператор работает с документом, курсор стоит в поле «Количество», а в этот момент он случайно нажимает кнопку сканера. Штрихкод вбивается в поле количества, документ испорчен. Или другая ситуация: нужно отсканировать товар, но активное поле вдруг потеряло фокус — и штрихкод улетает в пустоту.
Программная интеграция работает иначе. Приложение регистрирует приёмник Broadcast-сообщений или подключается к системному сервису сканера, и данные приходят асинхронно, независимо от того, какое поле активно. Приложение само решает, что делать с полученным штрихкодом: вставить в поле, обработать в фоне, передать на сервер. Именно так работают профессиональные ТСД с корпоративным софтом.
Мне нужно было понять: как сканер на самом деле передаёт данные и куда они деваются?
Часть 2. Углублённая разведка: лезем в системные логи
2.1. ADB logcat — окно в душу системы
Пошли смотреть системные логи через ADB. Подключил терминал к компьютеру и запустил фильтрацию:
adb logcat | grep -iE "barcode|scan|datawedge|urovo|decode|scanner"
Нажал кнопку сканера несколько раз — и вот оно. В логах обнаружилась масса интересного:
05-15 17:59:40.703 1253 1353 D ActivityManager: broadcastName action=ACTION_KEYCODE_SCAN_PRESSED05-15 17:59:40.719 1253 1353 D ActivityManager: broadcastName action=com.ubx.datawedge.SCANNER_DECODE_EVENT05-15 18:31:45.569 26903 26903 D ScannerSniffer: Action: urovo.rcv.message
Нам нужны два последних Broadcast-сообщения. Эти Intent’ы имеют флаг FLAG_RECEIVER_FOREGROUND — это значит, что система доставляет их только приёмникам, зарегистрированным из запущенного foreground-процесса. Кроме того, требовалось явно указать приоритет в IntentFilter.
2.2. Детальный анализ urovo.rcv.message / com.ubx.datawedge.SCANNER_DECODE_EVENT
Настроив правильную регистрацию с высоким приоритетом, я наконец поймал содержимое urovo.rcv.message или com.ubx.datawedge.SCANNER_DECODE_EVENT . Оказалось, что внутри этого Intent’а передаётся 18 полей. Вот их полный перечень, который я систематизировал и составил подробную таблицу. Поля сгруппированы по логическому смыслу — многие из них дублируют друг друга под разными именами:
|
Группа |
Поля |
Тип |
Пример значения |
Описание |
|
Данные (строка) |
barcode_string |
String |
8690506510022 |
Штрихкод в виде строки. Главное и самое удобное поле для получения данных |
|
com.ubx.datawedge.data_string |
||||
|
Данные (байты) |
com.ubx.datawedge.data_raw |
byte[] |
[38, 36, 39, 30, 35, 30, 36, 35, 31, 30, 30, 32, 32] |
Сырые байты штрихкода. Каждый байт — ASCII-код цифры. Избыточно, но есть |
|
barocode |
||||
|
barcode |
||||
|
Длина |
length |
Integer |
13 |
Длина штрихкода в символах |
|
com.ubx.datawedge.data_raw_length |
||||
|
Симвология (имя) |
symName |
String |
EAN-13, CODE-128, QR-Code |
Человекочитаемое название типа штрихкода |
|
com.ubx.datawedge.symbology_name |
||||
|
codetype |
||||
|
Симвология (число) |
symbologyId |
Byte/Integer |
11 (EAN-13), 5 (CODE-128) |
Внутренний код символогии Urovo |
|
com.ubx.datawedge.symbology_id |
||||
|
com.ubx.datawedge.barcode_id |
||||
|
Симвология (AIM) |
com.ubx.datawedge.aimcode |
byte[3] |
[93, 69, 51] → ]E3 |
AIM-идентификатор по стандарту ISO/IEC 15424. ]E3 = EAN-13 |
|
Время |
decodeTime |
Long |
222 |
Время декодирования в миллисекундах |
|
com.ubx.datawedge.data_dispatch_time |
||||
|
Счётчик |
DecodeNum |
Integer |
72 |
Порядковый номер сканирования с момента загрузки. Увеличивается на 1 каждый раз |
|
Интерфейс |
ScanOverlayButton |
Boolean |
false |
Нужно ли показывать виртуальную кнопку сканирования поверх экрана |
Важные замечания по таблице:
-
barcode_stringиcom.ubx.datawedge.data_stringвсегда идентичны — это дублирование одного и того же значения под двумя ключами. Достаточно читать любое из них. -
barocode,barcodeиcom.ubx.datawedge.data_rawтакже идентичны — три копии одного байтового массива. -
Симвология кодируется тремя способами одновременно: человекочитаемое имя (
symName), внутренний числовой код (symbologyId), и AIM-идентификатор (aimcode). Приложение может использовать любой из них. -
Счётчик
DecodeNum— интересная деталь. Он увеличивается при любом сканировании, независимо от того, было ли оно успешным. Это аппаратный счётчик, живущий до перезагрузки.
Часть 3. Пишем приложение-мост
3.1. Идея моста
Теперь, когда механизм сканера был понят досконально, оставалось выяснить, в каком формате «Магазин 15» ожидает данные, и научиться перенаправлять их. Логично было предположить, что приложение слушает какой-то Broadcast Intent, возможно — стандартный или проприетарный.
Я написал промежуточное приложение-мост, которое слушает urovo.rcv.message и пока просто отображает штрихкод на экране. Отправку в «Магазин 15» планировал добавить позже, когда выясню целевой Intent. Вот полный код моста:
public class ScannerBridgeActivity extends Activity { private static final String TAG = "ScannerBridge"; private TextView tvBarcode; private String lastBarcode = ""; private long lastScanTime = 0; private static final long DEBOUNCE_MS = 500; private final BroadcastReceiver scanReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { long now = System.currentTimeMillis(); if (now - lastScanTime < DEBOUNCE_MS) { Log.d(TAG, "Дебаунс: пропускаю повтор за " + (now - lastScanTime) + " мс"); return; } lastScanTime = now; // Извлекаем штрихкод из всех возможных полей String barcode = null; String source = "неизвестно"; if (intent.hasExtra("barcode_string")) { barcode = intent.getStringExtra("barcode_string"); source = "barcode_string"; } else if (intent.hasExtra("com.ubx.datawedge.data_string")) { barcode = intent.getStringExtra("com.ubx.datawedge.data_string"); source = "data_string"; } else if (intent.hasExtra("com.ubx.datawedge.data_raw")) { byte[] raw = intent.getByteArrayExtra("com.ubx.datawedge.data_raw"); if (raw != null) { barcode = new String(raw); source = "data_raw (байты → строка)"; } } String symbology = intent.hasExtra("symName") ? intent.getStringExtra("symName") : "неизвестно"; int decodeTime = intent.hasExtra("decodeTime") ? intent.getIntExtra("decodeTime", -1) : -1; if (barcode != null && !barcode.isEmpty() && !barcode.equals(lastBarcode)) { lastBarcode = barcode; String logMsg = String.format( "===== ШТРИХКОД =====\n" + "Значение: %s\n" + "Источник: %s\n" + "Тип: %s\n" + "Время декодирования: %d мс\n" + "========================", barcode, source, symbology, decodeTime ); Log.d(TAG, logMsg); updateDisplay(barcode, symbology, source, decodeTime); // Здесь будет отправка в Магазин 15 // sendToMobileSmarts(barcode); } else if (barcode != null && barcode.equals(lastBarcode)) { Log.d(TAG, "Дубликат штрихкода, пропускаю: " + barcode); } else { Log.w(TAG, "Не удалось извлечь штрихкод из Intent"); dumpIntentExtras(intent); } } }; private void dumpIntentExtras(Intent intent) { Bundle extras = intent.getExtras(); if (extras != null) { for (String key : extras.keySet()) { Log.w(TAG, "Неиспользованное поле: " + key + " = " + extras.get(key)); } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); tvBarcode = new TextView(this); tvBarcode.setTextSize(22); tvBarcode.setPadding(60, 60, 60, 60); tvBarcode.setGravity(Gravity.CENTER); tvBarcode.setText("Мост сканера\n\nОжидание...\n\nНажмите кнопку сканера"); tvBarcode.setTextColor(Color.BLACK); setContentView(tvBarcode); IntentFilter filter = new IntentFilter(); filter.addAction("urovo.rcv.message"); filter.setPriority(999); registerReceiver(scanReceiver, filter); Log.d(TAG, "Мост сканера запущен, слушаю urovo.rcv.message"); } private void updateDisplay(String barcode, String symbology, String source, int decodeTime) { runOnUiThread(() -> { String display = String.format( "=== ШТРИХКОД ===\n\n%s\n\n" + "Тип: %s\n" + "Длина: %d\n" + "Время: %d мс\n" + "Источник: %s", barcode, symbology, barcode.length(), decodeTime, source ); tvBarcode.setText(display); tvBarcode.setTextColor(Color.parseColor("#006600")); }); } @Override protected void onDestroy() { super.onDestroy(); try { unregisterReceiver(scanReceiver); } catch (IllegalArgumentException e) { } }}
3.2. Приём работает, но что дальше?
Приложение-мост уверенно ловило каждый скан, отображало штрихкод, тип символогии и время декодирования. Отсечка дребезга в 500 мс работала надёжно. Я убедился, что данные действительно приходят через urovo.rcv.message в читаемом виде.
Теперь передо мной стоял главный вопрос: какой Intent ожидает «Магазин 15»?
Можно было бы предположить, что раз «Магазин 15» — продукт Cleverence, то у них есть собственный проприетарный Intent. Или же они используют какой-то стандартный Android-механизм. Вариантов было несколько, и я начал методично их проверять.
Часть 4. Наивные попытки: Intent’ы, Broadcast’ы и первые разочарования
4.1. Гипотеза 1: android.intent.action.DECODE_DATA
Первая и самая очевидная гипотеза: «Магазин 15» слушает стандартный Android Intent android.intent.action.DECODE_DATA. Это стандартное действие для передачи декодированных данных сканера.
Проверил на DT40 Pro через ADB — отправил тестовый Intent:
adb shell am broadcast -a android.intent.action.DECODE_DATA --es barcode_string "1234567890123"
Результат: ничего. «Магазин 15» никак не реагировал.
Проверил с дополнительными ключами, которые нашёл в логах:
adb shell am broadcast -a android.intent.action.DECODE_DATA \ --es barcode_string "1234567890123" \ --ei length 13 \ --es symName "EAN-13" \ --ei symbologyId 11
Результат тот же — тишина.
4.2. Гипотеза 2: DataWedge-совместимость
Zebra DataWedge — это стандарт де-факто для сканеров штрихкодов в Android-индустрии. Многие приложения, работающие со сканерами Zebra, ожидают Intent’ы DataWedge. Я предположил, что Cleverence мог реализовать совместимость с этим стандартом.
DataWedge передаёт данные через Intent com.symbol.datawedge.data_string. Проверил:
adb shell am broadcast -a com.symbol.datawedge.data_string \ --es com.symbol.datawedge.data_string "1234567890123" \ --es com.symbol.datawedge.label_type "EAN-13"
И опять — никакой реакции.
4.3. Промежуточный итог: что мы знаем и чего не знаем
К этому моменту картина была такой:
-
Что мы знаем точно: сканер отправляет данные через Broadcast
urovo.rcv.messageиcom.ubx.datawedge.SCANNER_DECODE_EVENTс 18 полями, ключевое —barcode_string; -
Что мы не знаем: в каком формате и через какой механизм «Магазин 15» готов принимать данные от внешнего сканера;
-
Что подозреваем: скорее всего, приложение само опрашивает системный сервис сканера через AIDL, а не пассивно слушает Broadcast’ы.
Мост подтвердил, что данные уверенно приходят через urovo.rcv.message (см. Часть 3). Но оставался главный вопрос: как доставить их в «Магазин 15»? Поскольку ни один из Broadcast-Intent’ов не сработал, стало ясно, что нужно искать другой подход.
Часть 5. Accessibility Service: обходной манёвр
5.1. Идея автоматизации интерфейса
Идея была простая: раз мы не можем доставить данные программно, давайте имитируем действия пользователя.
Алгоритм:
-
Слушаем системный Broadcast
com.ubx.datawedge.SCANNER_DECODE_EVENT; -
При получении — программно тапаем по кнопке поиска в «Магазине 15»;
-
Дожидаемся открытия поля ввода;
-
Вставляем штрихкод;
-
Эмулируем нажатие Enter для запуска поиска.
5.2. Полный код ScanBridge с Accessibility Service
public class ScanBridgeService extends AccessibilityService { private static final String TAG = "ScanBridge"; private static final String SEARCH_BUTTON_DESC = "Поиск"; private static final String SEARCH_FIELD_ID = "search_src_text"; private String pendingBarcode = null; private long lastScanTime = 0; private final BroadcastReceiver scanReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String barcode = intent.getStringExtra("com.ubx.datawedge.data_string"); if (barcode == null) { barcode = intent.getStringExtra("barcode_string"); } if (barcode != null && !barcode.isEmpty()) { long now = System.currentTimeMillis(); if (now - lastScanTime < 500) return; lastScanTime = now; pendingBarcode = barcode; Log.d(TAG, "Получен штрихкод: " + barcode); // Запускаем процедуру вставки clickSearchButton(); } } }; @Override public void onServiceConnected() { super.onServiceConnected(); Log.d(TAG, "Accessibility Service подключен"); // Регистрируем приёмник сканера IntentFilter filter = new IntentFilter(); filter.addAction("com.ubx.datawedge.SCANNER_DECODE_EVENT"); filter.setPriority(999); registerReceiver(scanReceiver, filter); } private void clickSearchButton() { AccessibilityNodeInfo root = getRootInActiveWindow(); if (root == null) { Log.w(TAG, "root == null, не могу найти окно"); return; } // Ищем кнопку поиска по contentDescription List<AccessibilityNodeInfo> buttons = root.findAccessibilityNodeInfosByViewId( "com.cleverence.android.MobileSMARTS.v3.Retail:id/search_button" ); if (buttons.isEmpty()) { // Альтернативный поиск: по тексту content-desc AccessibilityNodeInfo searchBtn = findNodeByDescription(root, SEARCH_BUTTON_DESC); if (searchBtn != null) { searchBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK); Log.d(TAG, "Кнопка поиска нажата (по description)"); // Ждём открытия поля ввода и вставляем штрихкод new Handler(Looper.getMainLooper()).postDelayed(() -> { insertBarcode(); }, 500); } else { Log.w(TAG, "Кнопка поиска не найдена ни по ID, ни по description"); } } else { buttons.get(0).performAction(AccessibilityNodeInfo.ACTION_CLICK); Log.d(TAG, "Кнопка поиска нажата (по ID)"); new Handler(Looper.getMainLooper()).postDelayed(() -> { insertBarcode(); }, 500); } root.recycle(); } private AccessibilityNodeInfo findNodeByDescription(AccessibilityNodeInfo root, String desc) { if (root == null) return null; if (desc.equals(root.getContentDescription())) { return root; } for (int i = 0; i < root.getChildCount(); i++) { AccessibilityNodeInfo child = root.getChild(i); AccessibilityNodeInfo found = findNodeByDescription(child, desc); if (found != null) return found; } return null; } private void insertBarcode() { if (pendingBarcode == null) { Log.w(TAG, "Нет ожидающего штрихкода"); return; } AccessibilityNodeInfo root = getRootInActiveWindow(); if (root == null) { Log.w(TAG, "root == null при вставке"); return; } // Ищем поле ввода List<AccessibilityNodeInfo> fields = root.findAccessibilityNodeInfosByViewId( "com.cleverence.android.MobileSMARTS.v3.Retail:" + SEARCH_FIELD_ID ); if (fields.isEmpty()) { // Ищем любое EditText AccessibilityNodeInfo editField = findEditableNode(root); if (editField != null) { setTextInField(editField, pendingBarcode); pendingBarcode = null; } } else { setTextInField(fields.get(0), pendingBarcode); pendingBarcode = null; } root.recycle(); } private AccessibilityNodeInfo findEditableNode(AccessibilityNodeInfo root) { if (root == null) return null; if (root.isEditable()) return root; for (int i = 0; i < root.getChildCount(); i++) { AccessibilityNodeInfo found = findEditableNode(root.getChild(i)); if (found != null) return found; } return null; } private void setTextInField(AccessibilityNodeInfo field, String text) { Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text); boolean success = field.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); Log.d(TAG, "Вставка текста '" + text + "': " + (success ? "успешно" : "ошибка")); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { } @Override public void onInterrupt() { Log.w(TAG, "Accessibility Service прерван"); } @Override public void onDestroy() { super.onDestroy(); try { unregisterReceiver(scanReceiver); } catch (Exception e) { } }}
5.3. Что работало, а что нет
Сервис успешно:
-
Обнаруживал кнопку «Поиск» по contentDescription;
-
Нажимал на неё через performAction(ACTION_CLICK);
-
Находил открывшееся поле ввода;
-
Вставлял штрихкод через ACTION_SET_TEXT.
Но на финальном шаге всё застопорилось. Чтобы запустить поиск, нужно было нажать Enter. И тут Android 12 показал зубы:
-
instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER) — требовал системных прав INJECT_EVENTS;
-
Runtime.getRuntime().exec(«input keyevent 66») — требовал прав root или системного UID;
-
Эмуляция через dispatchGesture для тапа по клавиатуре — не поддерживалась в Accessibility Service на Android 12;
-
Прямой вызов input tap через shell — требовал дополнительных разрешений.
Штрихкод в поле вставлялся, но поиск не запускался. Пользователь видел вставленный штрихкод и должен был вручную нажать Enter. Это было неудобно и сводило на нет весь смысл автоматизации.
5.4. Root: получаем полный контроль
Процесс рутирования описан в статье Как получить root на Urovo DT40 Pro (CT48): Android 12 (Проверено на практике).
При работе над рутированием стало ясно, что начинка DT 40 Pro (включая модуль сканера) – это CT48.
Для рутирования на ТСД был установлен Magisk и использован в дальнейшем.
5.5. Триумф костыля: Accessibility Service заработал
С root-доступом я быстро допилил ScanBridge: добавил выполнение input keyevent 66 через su. Весь процесс — сканирование, открытие поиска, вставка, Enter — заработал полностью:
private void pressEnter() { try { Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", "input keyevent 66"}); process.waitFor(); Log.d(TAG, "Enter отправлен"); } catch (Exception e) { Log.e(TAG, "Ошибка отправки Enter: " + e.getMessage()); }}
Технически решение работало. Штрихкод сканировался, поиск запускался. Но это был костыль: пользователь видел моргание интерфейса, были задержки, и главное — работали только базовые сценарии. Полноценная интеграция (с триггерами, фоновым сканированием, обработкой в разных окнах) была невозможна.
Часть 6. Подмена параметров: от сложного к простому
6.1. Идея: а что если нас обманывает только имя?
Пока искал пути решения задачи, параллельно читал статьи на сайте Cleverence. Там были интересные статьи «Интегрирован Urovo CT48 от 30 ноября 2023», «Интеграция ТСД Urovo CT48», где указаны основные параметры. И меня заинтересовал один параметр — DeviceID (код устройства, на котором проводилась интеграция, часть цифр заменена на ***) и его значение:
@UROVO-CT48-3-018********661
В настройках «Магазин 15» на моем ТСД DT 40 Pro значение этого параметра было другим:
@CODEX-DT40PRO-M-AXID0*************123
И пока писал костыли, меня не отпускала мысль: DT40 Pro и CT48 — не один ли это девайс с разными названиями? Ведь при рутировании всё указывало на это.
Если заставить «Магазин 15» думать, что наше устройство — CT48, то драйвер успешно подключится к сервису сканера.
План был таков:
-
С помощью root изменить свойства
ro.product.modelиro.product.manufacturerнаCT48иUROVOсоответственно; -
Проверить, изменится ли идентификатор устройства в «Магазине 15»;
-
Проверить, заработает ли сканер.
6.2. Первый подход: прямое редактирование build.prop
Самое очевидное — отредактировать /system/build.prop. Но на Android 12 системный раздел монтируется только для чтения. Даже с root’ом mount -o rw,remount /system не работал на этом устройстве из-за динамических разделов (super partition).
Пришлось искать альтернативу.
6.3. Magisk-модуль для подмены свойств
Magisk позволяет создавать модули, которые подменяют системные свойства без модификации системного раздела. Создал простой модуль:
Структура модуля codex_spoof:
codex_spoof/├── module.prop├── system.prop└── service.sh
module.prop:
propertiesid=codex_spoofname=Urovo Model Spoofversion=v1.0versionCode=1author=IT-RTdescription=Change device model for MobileSMARTS compatibility
Первая версия system.prop (максималистская):
propertiesro.product.manufacturer=UROVOro.product.model=CT48ro.product.brand=UROVOro.product.name=CT48ro.product.device=CT48ro.product.board=CT48ro.build.product=CT48
Упаковал в ZIP, установил через Magisk Manager, перезагрузил.
Результат: Device ID в «Магазине 15» изменился на @UROVO-CT48-M-AXID0***********123. Модель стала CT48, производитель UROVO, но оставалась странная приставка «M» и длинный идентификатор вместо ожидаемого серийного номера. Сканер по-прежнему не работал.
6.4. Диагностика: откуда приложение берёт модель?
Стало очевидно, что «Магазин 15» читает не только стандартные ro.product.*. Я запустил диагностику:
bash
# На рутированном устройстве с правами susugetprop | grep -iE "product|model|manufacturer|brand|device|name|ufs|hostname"
Вывод показал много интересного:
ro.product.manufacturer: UROVO # То, что мы подменилиro.product.model: CT48 # То, что мы подменилиro.product.brand: UROVO # То, что мы подменилиro.product.name: CT48 # То, что мы подменили# А ЭТИ ОСТАЛИСЬ СТАРЫМИ!persist.sys.product.manufacturer: codexpersist.sys.product.model: DT40 Propersist.sys.product.name: DT40 Propersist.sys.product.brand: UROVOro.ufs.model: DT40 Pronet.hostname: DT40 Propersist.sys.default.devicename.vendor: DT40 Pro
Ключевое открытие: «Магазин 15» читает не ro.product.*, а persist.sys.product.*! Эти свойства хранятся в разделе persist, переживают сброс к заводским настройкам и имеют более высокий приоритет для корпоративных приложений.
6.5. Добавляем persist-параметры в Magisk-модуль
Свойства persist.* нельзя просто прописать в system.prop — они загружаются раньше, чем Magisk применяет свои подмены. Для persist-свойств нужно использовать resetprop в service.sh:
service.sh:
#!/system/bin/sh# Применяем persist-свойства при каждой загрузкеresetprop persist.sys.product.manufacturer UROVOresetprop persist.sys.product.model CT48resetprop persist.sys.product.name CT48resetprop persist.sys.product.brand UROVO# Дополнительные свойства, которые тоже читает приложениеresetprop ro.ufs.model CT48resetprop net.hostname CT48resetprop persist.sys.default.devicename.vendor CT48
Обновил модуль, переустановил, перезагрузил.
Результат: Device ID изменился на @UROVO-CT48-9-***********321. Модель стала CT48 без приставки «M», появился нормальный серийный номер. Но главное — сканер заработал!
«Магазин 15» загрузил правильный драйвер и всё встало на свои места. Триггеры работали, сканирование шло в фоне, данные приходили через штатную интеграцию.
6.6. Метод научного тыка: ищем минимальный набор
Дальше я провёл серию экспериментов, последовательно убирая свойства из модуля и проверяя, продолжает ли работать сканер. И в итоге, минимально необходимый набор — всего два свойства:
propertiespersist.sys.product.manufacturer=UROVOpersist.sys.product.model=CT48
Всё. Остальные параметры не влияют на выбор драйвера. Приложение само подхватывает серийник из ro.serialno, цифру-идентификатор высчитывает из своих алгоритмов.
Часть 7. Финальное решение и его изящество (нужен Root)
7.1. Итоговый Magisk-модуль
После всех экспериментов финальная версия модуля оказалась поразительно простой:
Структура:
urovo_ct48_scanner_fix/├── module.prop├── system.prop└── service.sh
module.prop:
propertiesid=urovo_ct48_scanner_fixname=Urovo CT48 Scanner Fix for DT40 Proversion=v1.0versionCode=1author=IT-RTdescription=Fixes scanner integration in MobileSMARTS by setting correct device model
system.prop:
propertiespersist.sys.product.manufacturer=UROVOpersist.sys.product.model=CT48
service.sh:
#!/system/bin/shresetprop persist.sys.product.manufacturer UROVOresetprop persist.sys.product.model CT48
7.2. Установка и проверка
Установка элементарна:
-
Упаковать папку модуля в ZIP;
-
В Magisk Manager: Модули -> Установить из хранилища -> выбрать ZIP;
-
Перезагрузить устройство;
-
Запустить «Магазин 15» и убедиться, что сканер работает.
После установки в «Магазине 15» отображается:
-
Производитель: UROVO
-
Модель: CT48
-
Device ID: @UROVO-CT48-[цифра-идентификатор]-[серийный номер]
-
Сканер: активен, работает во всех режимах
Эпилог: выводы и уроки
DT40 Pro и CT48 построены на одной аппаратной платформе (MediaTek MT6765), используют один и тот же чип сканера и один и тот же системный сервис com.ubx.scan.service. Разница только в названии модели и производителя, прописанном в разделе persist.
«Магазин 15» при выборе драйвера руководствуется исключительно строковым совпадением модели и производителя устройства. Подменив модель и производителя на CT48 и UROVO, мы заставляем приложение загрузить правильный драйвер, который успешно подключается к системному сервису сканера. Никакой магии — просто восстановление логической цепочки, которую разорвал маркетинговый ребрендинг Urovo.
Но самый главный урок в том, что перед приобретением нужно предварительно и тщательно изучать документацию по софту и железу. А метод «сначала делать, а потом читать мануалы» будет приводить вот к таким приключениям.
ссылка на оригинал статьи https://habr.com/ru/articles/1057946/