Разработка
Как я исследовал приложение Zomato, чтобы создать свою собственную систему уведомлений
С момента разработки мне было очень удобно отслеживать статус заказа, не заходя в приложение по несколько раз. Я надеюсь, что статья предоставила достойное представление о процессе реверс-инжиниринга и реализации сетевых вызовов.
Являясь ежедневным пользователем приложения Zomato для Android и инженером по разработке приложений, я полюбил дизайн приложения и то удобство, которое оно предлагает для заказа еды из любого места. Однако мне всегда было неудобно постоянно открывать приложение, чтобы проверить статус доставки еды. Хотя в приложении есть отличный экран отслеживания заказов, ему не хватает целостности и легкости доступа, которые предлагает iOS-вариант приложения Zomato благодаря использованию в iOS Live Activity. Вдохновившись этим, я решил взять дело в свои руки и провести реверс-инжиниринг приложения Zomato для Android, чтобы создать собственное решение для отслеживания заказов. В этой публикации я расскажу о том, как нашел необходимые конечные точки API, разработал системную архитектуру приложения и реализовал уведомление, которое предоставляет информацию о заказе практически в режиме реального времени. И все это без необходимости постоянно открывать приложение Zomato.
Часть 1: Понимание трафика приложения
Чтобы понять, как приложение может отображать информацию, нам нужно выяснить, как приложение получает необходимую информацию. Отличной отправной точкой будет проверка сетевого трафика между приложением и сервером, поскольку она дает точное представление о том, какая информация отправляется в приложение. Это даст нам информацию о конечных точках API, которые мы можем впоследствии попытаться вызвать самостоятельно, чтобы получить необходимую информацию о заказе.
Для этого я воспользовался следующими инструментами:
- Apktool
- Charles Proxy
- Эмулятор Android
Процесс настройки приложений для отладки достаточно прост, поэтому я не буду его описывать, так как это не относится к данной публикации.
Часть 2: Анализ трафика приложений
Настроив среду, мы можем проанализировать трафик и просмотреть содержимое запросов. Чтобы выяснить запрос на получение истории заказов, мы переходим на страницу истории заказов приложения и одновременно просматриваем активность в Charles. Похоже, что есть определенная конечная точка, которая вызывается каждый раз, когда мы попадаем на эту страницу. Это :
https://api.zomato.com/gw/order/history/online_order
Давайте проверим ответ:
... { "status": "success", "has_more": true, "postback_params": "{\"last_created_timestamp\":{\"seconds\":1714292519},\"last_order_id\":5747518212}", "results": [{ "layout_config": { "snippet_type": "filter_info_card", "layout_type": "carousel", "section_count": 1 } }, { "layout_config": { "snippet_type": "order_history_snippet_type_2", "layout_type": "carousel", "section_count": 1 }, "order_history_snippet_type_2": { "click_action": { "type": "deeplink", "deeplink": { "url": "zomato://delivery/5777845053" } }, "top_container": { "title": { "text": "\u003cmedium-400|{grey-900|Leon's Burgers \u0026 Wings}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "subtitle1": { "text": "\u003cmedium-100|{grey-600|Whitefield, Bangalore}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "subtitle2": { "text": "\u003cmedium-100|{grey-600|54 mins}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "image": { "url": "https://b.zmtcdn.com/data/pictures/6/19337846/eefd6011022ffa01ec1c0c9becfaded1_featured_v2.jpg?fit=around%7C108%3A108\u0026crop=108%3A108%3B%2A%2C%2A" }, "tag": { "title": { "text": "On the way", "color": { "tint": "500", "type": "blue" } }, "bg_color": { "tint": "100", "type": "blue" }, "border_color": { "tint": "400", "type": "blue" }, "image": { "animation": { "url": "https://b.zmtcdn.com/data/file_assets/e2585eef505467d5ada82b1c0169fc161632987479.json", "duration_per_step": 0, "animate": false, "repeat": false } } }, "right_button": { "type": "text", "text": "View menu", "suffix_icon": { "code": "e875", "color": { "tint": "500", "type": "red" } }, "click_action": { "type": "deeplink", "deeplink": { "url": "zomato://order/19337846" } }, "tracking_data": [{ "table_name": "jevent", "payload": "{\"var1\":\"5777845053\",\"var2\":\"19337846\",\"var3\":\"On the way\",\"var4\":\"1\",\"var5\":\"your_orders\",\"var6\":\"view_menu\"}", "event_names": { "tap": "{\"ename\":\"order_history_snippet_tapped\"}" } }], "should_use_decoration": false, "should_use_squircle": false, "should_round_corner": false }, "click_action": { "type": "deeplink", "deeplink": { "url": "zomato://order/19337846" } }, "tracking_data": [{ "table_name": "jevent", "payload": "{\"var1\":\"5777845053\",\"var2\":\"19337846\",\"var3\":\"On the way\",\"var4\":\"1\",\"var5\":\"your_orders\",\"var6\":\"res_card\"}", "event_names": { "tap": "{\"ename\":\"order_history_snippet_tapped\"}" } }] }, "items": [{ "title": { "text": "\u003csemibold-300|{grey-600|1 x}\u003e \u003csemibold-300|{grey-900|Peri Peri Chicken Wrap}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "image": { "url": "https://b.zmtcdn.com/data/o2_assets/3e0b4b89a7a1d815a1adaf5ad216505f1657182448.png" } }, { "title": { "text": "\u003csemibold-300|{grey-600|1 x}\u003e \u003csemibold-300|{grey-900|Chicken Doner Salad}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "image": { "url": "https://b.zmtcdn.com/data/o2_assets/3e0b4b89a7a1d815a1adaf5ad216505f1657182448.png" } }, { "title": { "text": "\u003csemibold-300|{grey-600|1 x}\u003e \u003csemibold-300|{grey-900|Hummus With Chicken and Pitta}\u003e", "is_markdown": 1, "markdown_version": 2, "number_of_lines": 1 }, "image": { "url": "https://b.zmtcdn.com/data/o2_assets/3e0b4b89a7a1d815a1adaf5ad216505f1657182448.png" } }], "bottom_container": { "title": { "text": "30 Apr 2024 at 9:11PM" }, "subtitle1": { "text": "\u003csemibold-200|{grey-900|₹649.55}\u003e", "is_markdown": 1, "markdown_version": 2, "suffix_icon": { "code": "e822", "color": { "tint": "500", "type": "grey" } } }, "buttons": [{ "type": "solid", "text": "Track order", "click_action": { "type": "deeplink", "deeplink": { "url": "zomato://delivery/5777845053" } }, "tracking_data": [{ "table_name": "jevent", "payload": "{\"var1\":\"5777845053\",\"var2\":\"19337846\",\"var3\":\"On the way\",\"var4\":\"1\",\"var5\":\"your_orders\",\"var6\":\"track_order\"}", "event_names": { "tap": "{\"ename\":\"order_history_snippet_tapped\"}" } }], "should_use_decoration": false, "should_use_squircle": false, "should_round_corner": false }] }, "tracking_data": [{ "table_name": "jevent", "payload": "{\"var1\":\"5777845053\",\"var2\":\"19337846\",\"var3\":\"On the way\",\"var4\":\"1\",\"var5\":\"your_orders\"}", "event_names": { "impression": "{\"ename\":\"order_history_snippet_impression\"}" } }, { "table_name": "jevent", "payload": "{\"var1\":\"5777845053\",\"var2\":\"19337846\",\"var3\":\"On the way\",\"var4\":\"1\",\"var5\":\"your_orders\",\"var6\":\"order_dish_card\"}", "event_names": { "tap": "{\"ename\":\"order_history_snippet_tapped\"}" } }] } } ....
Это сокращенный ответ для примера, полный код ответа приведен здесь.
Если мы изучим эту структуру JSON, то увидим, что существует список результатов, в котором есть объект с ключами layout_config
и order_history_snippet_type_2
. Объект order_history_snippet_type_2
содержит полезную для нас информацию. Объект click_action
содержит строку с глубокой ссылкой, которую мы можем использовать для извлечения идентификатора заказа. Идентификатор заказа присутствует и в других местах, но мне показалось, что в этом месте все просто. Есть также top_container
и bottom_container
, которые содержат всю остальную информацию, которая нам нужна. Название ресторана можно извлечь из текстового элемента внутри объекта title
в top_container
. Идентификатор заказа можно просто извлечь из deeplink
, заменив начальные теги и сохранив номера. Статус заказа можно получить из текстового элемента внутри объекта title
в top_container
. Время доставки заказа можно получить из текстового элемента внутри объекта title
в bottom_container
.
Аналогично, если мы размещаем заказ и анализируем активность трафика на странице Сведений о заказе в приложении, то повторно вызывается следующая конечная точка API:
https://api.zomato.com/v2/order/crystal_v2
Ответ для этой конечной точки содержит много конфиденциальной информации, поэтому я не буду публиковать пример отклика. Однако, для справки, следующие теги в ответе предоставляют нам такую информацию:
response -> order_details -> res_name = Restaurant Name response -> order_details -> tab_id = Order ID response -> header_data -> pill_data -> left_data -> title -> text = Estimated Time (String) [ex: Arriving in 5 mins] response -> header_data -> subtitle2 -> text = Order Status in text [ex: On the way, Will be picked up soon etc] response -> header_data -> pill_data -> right_data -> title -> text = Order Estimate Status [ex: On time, Delayed]
Часть 3. Разработка уведомления
Поняв часть, связанную с получением данных, я захотел создать систему уведомлений, которая вдохновлялась бы индикатором активности на iOS. Вот как Zomato использует службу Activity Indication в своем приложении для iOS:
Я создал аналогичный макет для уведомления Android. Вот как это выглядело в дизайне Figma, который я создал:
Как и индикатор активности в iOS, в уведомлении, созданном моим приложением, также будет отображаться название ресторана, текстовое представление текущего статуса заказа, оценка времени доставки и фактическая оценка времени передачи заказа.
Часть 3. Обработка потока информации и управление уведомлением
Чтобы иметь уведомление, которое можно было бы регулярно обновлять, нам нужно использовать Foreground Service, а также некоторые функции для многократного получения сведений о заказе. Давайте назовем эту службу OrderTrackService. Основная цель этого сервиса — принять идентификатор заказа и вызвать crystal_v2 API для получения информации о статусе заказа, а затем отобразить ее в уведомлении. Эта задача будет повторяться с интервалом в 30 секунд, и этот процесс будет продолжаться до тех пор, пока статус заказа не станет “Доставлен”.
Чтобы показать Уведомление, мы создадим кастомный макет, в котором будет отображаться информация о заказе. Уведомление будет отображаться с определенным идентификатором. В Android, если мы повторно отправим уведомление с тем же идентификатором, оно автоматически обновит существующее уведомление, которое отображалось на панели уведомлений. Таким образом, мы используем эту логику для получения информации о заказе и просто воссоздаем уведомление и отображаем его, используя существующий идентификатор.
Чтобы управлять сервисом и запускать его, нам также понадобится Activity, основная цель которой — получить историю заказов и отобразить ее в виде списка. С помощью этого списка мы можем выбрать, какие идентификаторы заказов будут переданы в OrderTrackService для обработки уведомления.
Часть 4: Собираем все вместе
Теперь, когда дизайн и система понятны, пришло время воплотить идею в жизнь. Я начал с реализации MainActivity с помощью Jetpack Compose. MainActivity состоит из трех кнопок, первая кнопка предназначена для запроса разрешения POST_NOTIFICATION для приложения, вторая кнопка служит для запуска Foreground службы, а последняя кнопка используется для просмотра истории заказов. При нажатии на кнопку “Получить заказы” приложение вызывает online_order API и фильтрует результаты, в которых статус заказа не “Доставлен”. Отфильтрованные заказы — это те заказы, которые активны в данный момент.
Для фактического вызова конечных точек API я внедрил библиотеку Retrofit и создал подходящие интерфейсы с требуемыми заголовками для двух конечных точек API. Чтобы обеспечить качество кода, все здесь было реализовано с использованием шаблона MVVM, а также Hilt для внедрения зависимостей, чтобы упростить работу с экземпляром Retrofit.
Класс OrderTrackService является расширением класса Service
. Как уже говорилось, здесь мы будем обрабатывать логику уведомлений. Связь между MainActivity и OrderTrackService устанавливается с помощью Broadcast Intent. Соответствующая кнопка для каждого заказа в списке, отображаемом в MainActivity, отправляет широковещательное намерение с идентификатором заказа как часть пакета данных для этого намерения. После получения этого широковещательного запроса Foreground служба начинает действовать, вызывая crystal_v2 API для получения сведений о заказе для этого указанного идентификатора. Затем она создает уведомление, в котором отображается информация. Логика получения и отправки сообщений создана для работы в Kotlin Flow, так что она может работать асинхронно, с поддержкой независимой обработки нескольких уведомлений, если требуется, поскольку для каждой полученной рассылки с действительным идентификатором заказа мы запускаем повторяющийся поток. С помощью Flow мы также можем приостановить/повторить действие по желанию, таким образом, это также позволяет нам повторять ту же задачу с задержками (в нашем случае 30 секунд).
Финал
Мое приложение не только экономит время, но и предоставляет гораздо лучший способ быть в курсе статуса моего заказа. С момента разработки мне было очень удобно отслеживать статус заказа, не заходя в приложение по несколько раз. Я надеюсь, что статья предоставила достойное представление о процессе реверс-инжиниринга и реализации сетевых вызовов.
Я разместил полный исходный код для этого проекта в моем репозитории на GitHub здесь.
Не стесняйтесь исследовать, экспериментировать и даже вносить свой вклад в проект, если у вас есть идеи по дальнейшему совершенствованию.