В прошлом году мы провели онлайн-квест для мобильных разработчиков — Droid Mission. В течение месяца участники должны были решить как можно больше задач в трёх направлениях: fix it! (поиск ошибок и исследование кода), hack it! (реверс-инжиниринг) и dig it! (изучение особенностей Android). Всего в квесте было 23 задачи — они очень похожи на те, с которыми сталкиваются специалисты по Android в реальной работе. В посте мы покажем все условия и правильные решения.


Поиск ошибок и исследование кода


fix it! #1

Автор: Анастасия Лаушкина

Тихим пятничным вечером двое агентов остаются допоздна и ведут неразрешимый спор. Им поручили хранить большой объем секретных данных в БД Sqlite с целочисленным ключом key.

Агент А создает таблицу запросом вида: CREATE TABLE t(key INT PRIMARY KEY, secret_value_1, secret_value_2), а агент Б — запросом вида CREATE TABLE t(key INTEGER PRIMARY KEY ASC, secret_value_1, secret_value_2).

Агент А абсолютно уверен, что в его запросе нет ошибки, однако у агента Б есть аргумент против этого: он утверждает, что целостность данных при подходе агента А может быть нарушена.

Перечислите через запятую: колонку или колонки, которые отвечают за целостность у агента Б, но не отвечают у агента А; а также структуру данных, на русском языке, за счет которой запрос агента Б будет выполняться быстрее.

Примечания
Формат ответа: [имя колонки 1],[имя колонки N],[имя структуры] (без скобок, с маленькой буквы)

Пример ответа: x,y, массив

Решение

Речь идет о rowId и ее связи с primary key.

В случае «CREATE TABLE t(key INTEGER PRIMARY KEY ASC, secret_value_1, secret_value_2)» key — это alias к rowId. В случае «CREATE TABLE t(key INT PRIMARY KEY, secret_value_1, secret_value_2)» — нет. Причину можно найти в документации.

Поэтому при отсутствии связи key с rowId запрос вида «INSERT INTO t VALUES (»some", «y», «z»)" будет успешно выполнен, потенциально нарушив целостность данных. При наличии такой связи возникнет ошибка datatype mismatch.

К ответу принималась как пара key, rowId, так и key/rowId по отдельности (поскольку при наличии связи они обращаются к одной колонке).

Скорость достигается за счет хранения в виде дерева поиска.

fix it! #2

Автор: Анастасия Лаушкина
Бинарный файл

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

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

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

Примечания
Формат ответа: [строка 1],[строка 2] (без скобок)

Пример ответа: FrameLayout,ScrollView

Решение

1. Ищем файл с разметкой для виджета, видим в нем недопустимый элемент ConstraintLayout (см. developer.android.com/guide/topics/appwidgets#CreatingLayout).
2. Меняем его на допустимый и оптимальный — LinearLayout/ FrameLayout.

Несмотря на то, что использование FrameLayout ломало расположение элементов в виджете, к ответу он все равно принимался.

fix it! #3

Автор: Артём Витер
Бинарный файл

Тестировщики заметили аномальный рост потребления оперативной памяти. Есть подозрение, что где-то утечка.

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

Формат ответа: [Имя класса в котором вызывается метод]:[номер строки в этом файле]:[название метода] ?(без скобок)

Несколько ответов укажите в лексикографическом порядке, разделяя знаком #.

Пример:

Если вы считаете, что утечка памяти в файле ExampleFileName.java,

class ExampleFileName {
  public problemMethodName(Sting arg) {}
  public test(Sting arg) {
    problemMethodName(arg); // строка 8 в файле ExampleFileName.java
  }
}

Пример ответа: ExampleFileName:8:problemMethodName

Решение

Первый способ — анализ кода.

  1. Изучаем код приложения.
  2. Первая утечка очевидна. Это регистрация BroadCastReceiver в классе SecondActivity. Сообщение об этой проблеме можно увидеть в logcat, если запустить приложение.
  3. Если обратить внимание на метод startTimer(), можно заметить, что класс CountDownTimer добавляет в handler главного потока замыкание со ссылкой на textView. И перед разрушением activity у класса экземпляра CountDownTimer не вызывется метод cancel(). Это и есть вторая утечка памяти.

Второй способ — инструменты. Можно внедрить в приложение библиотеку leakcanary, получить heap dump через adb или android Profiler и проанализировать полученные отчеты. В анализе может помочь Eclipse Memory Analyzer (MAT). Чтобы можно было открыть heapdump в MAT, нужно сконвертировать его в понятный для MAT формат утилитой hprof-convd.

fix it! #4

Автор: Вали Ибрагимов
Бинарный файл

У Ивана сегодня первый день в онлайн-кинотеатре. Так как он крутой разработчик, его сразу же попросили исправить креш при просмотре фильма. Он решил заодно заняться рефакторингом. Код стал лаконичнее и чище, ошибка перестала воспроизводиться.

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

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

Решение

Для отправки позиции просмотра каждые пять секунд и похода в сеть используется ExecutorService. Создав ScheduledThreadPoolExecutor для периодической задачи, Иван подумал — почему бы не использовать его для похода в сеть? Ведь ScheduledThreadPoolExecutor является наследником ThreadPoolExecutor. Иван даже не подозревал, что ScheduledThreadPoolExecutor использует только один поток и при походе в сеть потребуется дождаться завершения периодической задачи.

scheduleFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
                    try {
                        timingsApi.sendTiming(filmId, player.getContentPosition()).get();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                },
                0, PERIOD_SECONDS, TimeUnit.SECONDS);

Решение задачи усложняло еще и то, что мы обращаемся к ExoPlayer не в UI-потоке — и в новой версии 2.9.* ExoPlayer начал писать в логи предупреждения об этом. Но сейчас это всего лишь предупреждение, а не допущенная Иваном ошибка.

fix it! #5

Автор: Александр Цыбин
Бинарный файл

Одна компания разрабатывала игру Симулятор Фермера.

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

Разработчику Васе поручили реализовать врагов. Во время реализации Вася столкнулся с проблемой производительности. Он решил исправить ее за счет многопоточности. Так как Вася не был знаком с многопоточностью, он воспользовался решениями из stackoverflow. Так он избавился от проблем производительности.

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

В качестве ответа введите номера проблемных строк кода — через запятую, в порядке возрастания и без пробелов. Вот так: 1,17,42

Решение

Boxing примитивных типов (например, int в Integer) — дорогие операции. В оптимизационных целях Java-машины иногда переиспользуют такие объекты. Строки — не исключение.

Cтроки ENEMY_LOCK и DECISIONS_LOCK используются в коде для синхронизации. Значит, если в проекте есть места с синхронизацией на такие же строки, то может произойти deadlock.

В общем случае нельзя использовать String/Integer и т. д. для синхронизаций.

Ответ: 7,8

fix it! #6

Автор: Иван Пухов
Бинарный файл

Команда обработки изображений выпустила новую нативную библиотеку, нужно ее опробовать и указать на проблемы, если они есть.

Решение

Это простая задача для разминки. В ресурсах лежит сама библиотека и заголовочный файл к ней. В заголовочнике — единственная функция, возвращающая jobject.

Не остается ничего больше, кроме как создать тестовый проект (установив NDK) и вызвать ее! Вызов функции на главном потоке выкинет исключение с просьбой не вызывать ее таким способом. Перенесем исполнение на другой тред любым способом — и все заработает.

Теперь стоит проверить возвращенный объект — действительно ли это битмап? Самый простой способ — сразу же отобразить его на ImageView. Мы увидим изображение с текстовой строкой, а задача требует ответа также в виде строки. Наверное, это не просто совпадение. :)

fix it! #7

Автор: Дмитрий Зайцев
Бинарный файл

Есть приложение с кодами, где нужный код выделен рамкой. К сожалению, верстка была сломана и нужно ее восстановить, чтобы найти код.

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

Пример ответа: ScQG1jxbazmjbcARefbRMo

Решение

Сейчас Dagger де-факто стал стандартом в Android-разработке. Когда DI уже настроен, очень просто написать Inject перед конструктором и отдать всю оставшуюся работу на выполнение в Dagger. Это настолько легко, что впоследствии можно перестать задумываться о том, как зависимости предоставляются. Также в Android часто приходиться работать с классом Context и его потомками: Application и Activity. Application привязан к жизненному циклу приложения, а Activity — к жизненному циклу экрана. Поэтому в работе с этими классами важно соблюдать множество особенностей. Задача как раз и основана на использовании неправильного контекста, и все это замаскировано с помощью DI.

Для решения можно выполнить следующие шаги:

  1. Изучить файлы проекта и заметить, что ко всем TextView применяется стиль ItemStyle.
  2. Увидеть, что на самом деле стиль не применяется. Это должно натолкнуть на идею, что проблема связана с темой, а значит и с контекстом Activity.
  3. Разобраться в графе зависимостей и понять, что в адаптер предоставляется контекст Application.
  4. В ContestAdapter изменить тип контекста на Activity.

fix it! #8

Автор: Вали Ибрагимов
Бинарный файл

Ивану дали задачу отрефакторить приложение для получения списка задач через ContentProvider, используя DI. Он отрефакторил приложение, использовав Dagger, и при запуске приложение начало вылетать.

Помогите Ивану понять причину:

  1. Укажите номер строки с именем класса, которую нужно перенести в другой класс.
  2. Предоставьте ссылку на документацию с якорем на место, которое поможет понять, почему вылетает приложение.

Примечания
Формат ответа: [Имя класса]:[Номер строки]->[Имя класса]:[Номер строки],[Ссылка]

Пример ответа:

FirstCLass:12- >SecondClass:45,https://developer.android.com/reference/android/app/Activity#activity-lifecycle

Решение

Задача состояла из двух частей:

1. Разобраться и предоставить решение креша в ContentProvider при обращении в DI-компоненту. Правильным решением было перенести инициализацию DI-компоненты в ContentProvider.



Метод onCreate у ContentProvider вызывается раньше, чем у Application. В этом несложно убедиться, если изучить код старта приложения в классе ActivityThread и метода handleBindApplication.



2. Предоставить ссылку с описанием того, что onCreate у ContentProvider вызывается раньше, чем у Application. Google не упоминает об этом на странице документации ContentProvider. Но вот выдержка из описания метода onCreate Aplication:

Called when the application is starting, before any activity, service, or receiver objects (excluding content providers) have been created.

fix it! #9

Автор: Александр Цыбин
Бинарный файл

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

Примечания
Формат ответа:

<Имя файла без расширения>:<Номера строк через запятую>

<Имя файла без расширения>:<Номера строк через запятую>

Строки должны быть отсортированы в лексикографическом порядке. Номера строк должны быть отсортированы по возрастанию.

Пример ответа:

BarFoo:1

FooBar:12,13,14,99

Решение

В коде есть две проблемы:

1. Если судить по коду, предполагается, что вызов onViewDetachedFromWindow всегда будет происходить после onBindViewHolder, однако это не так. Метод-близнец для onViewDetachedFromWindow — это onViewAttachedToWindow. Как следствие, происходит вызов holder.view.reset() и центрирование пропадает, если скролл происходит медленно.

2. На Android до 7 версии addView(rootView, params) работает не совсем так, как ожидается.

При добавлении View происходит генерация LayoutParam, которая выглядит так:

Marshmallow — github.com/aosp-mirror/platform_frameworks_base/blob/marshmallow-release/core/java/android/widget/FrameLayout.java#L403
Nougat — github.com/aosp-mirror/platform_frameworks_base/blob/nougat-release/core/java/android/widget/FrameLayout.java#L384

У FrameLayout.LayoutParams есть два конструктора, один из которых принимает ViewGroup.MarginLayoutParams и копирует маргины в себя: github.com/aosp-mirror/platform_frameworks_base/blob/nougat-release/core/java/android/view/ViewGroup.java#L7314

Второй конструктор принимает ViewGroup.LayoutParams и игнорирует маргины: github.com/aosp-mirror/platform_frameworks_base/blob/nougat-release/core/java/android/view/ViewGroup.java#L7328

В итоге центрирование не работает на Android до 7 версии.

Ответ:
ItemView:35
MainActivity:53

Реверс-инжиниринг


hack it! #1

Автор: Валентин Барышев
Бинарный файл

Один разработчик поторопился и даже не протестировал финальную .apk перед отправкой в контест.

Оказалось, что он перепутал некоторые viewgroup в activity_main.xml. Кроме того, каждую букву ключа он разместил в отдельном textview. В итоге все view на layout разъехались и на экране происходит полнейший беспредел.

Попробуйте определить, какие viewgroup должны быть на месте текущих.

Ответом к задаче является код, который отобразится на экране после подстановки правильных viewgroup.

Решение

Нужно декомпилировать .apk любым доступным способом (например, онлайн-декомпиляторами) и воссоздать проект по коду.

Если проанализиовать main_layout, то по аттрибутам можно догадаться, что корневой viewgroup должен быть ConstraintLayout.

Ответ: tniartsnoc

hack it! #2

Автор: Александр Цыбин
Бинарный файл

У опытного пользователя упало приложение. Он не растерялся, открыл логи, нашел строку, которая вызвала ошибку: N72XbphDx5NnFl6CKMNl8w== и отправил ее в техподдержку.

Выяснилось, что он использует устаревшую версию приложения, которая была разработана еще до использования кодовых репозиториев, поэтому код расшифровки был утерян. Однако удалось найти .apk старой версии приложения.

Помогите расшифровать сообщение.

Решение

Нужно декомпилировать .apk любым доступным способом (например, онлайн-декомпиляторами) и воссоздать проект по коду.

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

Ответ:
TF: Good Job!

hack it! #3

Автор: Валентин Барышев
Бинарный файл

Как хорошо вы знаете lifecycle Android-приложения?

Знаете ли вы, что система в некоторых случаях может убить процесс приложения?

Смоделируйте такую ситуацию на приложении.

Вы увидите ответ после восстановления приложения — в виде кода.

Решение

Достаточно было установить .apk на телефон, запустить и добиться случая, когда система выгрузит приложение, но при этом оно останется в шторке.

Один из вариантов — установить приложение, запустить его и свернуть в шторку (или просто переключиться на другое приложение). Затем подключаемся по adb к компьютеру. В Android Studio выбираем вкладку Logcat, выбираем процесс этого приложения и нажимаем на красный значок stop. Затем открываем ранее свернутое приложение из шторки. На экране отобразится ответ: sss2384gxcxxX.

hack it! #4

Автор: Иван Пухов
Бинарный файл

Коллега-фронтендер выложил демо-версию нового модуля, но забыл прислать спецификации. Сейчас он спит в другом часовом поясе, а провести демо надо через несколько минут. Из разговора с коллегой вы помните его фразу «Да там легко, надо просто стабы написать, и все заработает».

Скачайте архив и постарайтесь справиться без коллеги.

Решение

Это несложная задача, но она предполагает наличие некоторого любопытства и настойчивости.

Скачиваем архив, выкладываем его содержимое на локальный тестовый сервер, создаем проект с WebView и запускаем. После небольшого таймаута видим сообщение «Calibrate orientation». И всё. Но сообщение намекает, что, может, стоит покрутить телефон в руках? После этого будет показано подозрительное сообщение о выброшенном исключении — c призывом проверить вывод консоли.

Открываем консоль и видим вызов несуществующего метода у объекта API. Добавляем к WebView объект для реализации JS API и снова пробуем протестировать приложение.

Видимо, стоит добавить возвращаемое значение. Подбираем, добавляем — работает. Но строки с ответом нигде нет. Крутим телефон снова — вызывается новый метод. Добавляем и его. Спустя несколько итераций получим вызов finalizeCalibrating и затем storeResult, в котором к нам прибудет наша строчка.

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

hack it! #5

Автор: Иван Пухов

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


Решение

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

adb logcat | grep 'SecretActivity'

1898 1898 E SecretActivity: Try broadcast ZggwxXsaQ9SapPKyHpnwnYmALHazVFWX

2. Пробуем кинуть broadcast с action из лога:

adb shell am broadcast -a ZggwxXsaQ9SapPKyHpnwnYmALHazVFWX

3. На экране появится тост с предложением посмотреть логи по тегу SecretReceiver.

adb logcat | grep 'SecretReceiver'

  1898 1898 E SecretReceiver: ISecretService.aidl
  1898 1898 E SecretReceiver: package android.app;
  1898 1898 E SecretReceiver: interface ISecretService {
  1898 1898 E SecretReceiver: String getSecret();
  1898 1898 E SecretReceiver: }
  1898 1898 E SecretReceiver: See global settings to bind SecretService


4. Смотрим глобальные настройки, чтобы найти action для соединения с SecretService.

adb shell settings list global

  ...
  bind_secret_service_action=g8mNyGQZR8aHLTXNWcjdwJJYZ85Ewx83
  ...


5. Пишем клиент для сервиса ISecretService по данному aidl-интерфейсу.

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = new Intent();
        intent.setAction("g8mNyGQZR8aHLTXNWcjdwJJYZ85Ewx83");

        PackageManager pm = getPackageManager();
        List<ResolveInfo> resolveInfoList = pm.queryIntentServices(intent, 0);

        if (resolveInfoList == null || resolveInfoList.size() != 1) {
            return;
        }

        ResolveInfo serviceInfo = resolveInfoList.get(0);
        ComponentName component = new ComponentName(serviceInfo.serviceInfo.packageName, serviceInfo.serviceInfo.name);
        Intent explicitIntent = new Intent(intent);
        explicitIntent.setComponent(component);

        bindService(explicitIntent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                ISecretService secretService = ISecretService.Stub.asInterface(service);
                try {
                    Log.d("MainActivity", secretService.getSecret());
                } catch (Exception e) {
                    Log.e("MainActivity", "RemoteException", e);
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, Context.BIND_AUTO_CREATE);
    }
}

6. Запускаем, в логах получаем ответ:

adb logcat | grep 'MainActivity'

  1898 1898 E MainActivity: VENHz=qWr7y!t3ZhP!8Skw!!kcTkt7V%

hack it! #6

Автор: Павел Воробкалов
Бинарный файл

Господин Таппер написал приложение для Андроида, которое отправляет сообщения.

Расшифруйте сообщение и двигайтесь дальше.

Формат ответа: строка в виде ссылки.

Решение

Устанавливаем и запускаем приложение на устройстве. Видим две кнопки: Send message и Send formula. При нажатии на них ничего видимого не происходит. Нужно разобраться, каким способом и куда приложение что-то отправляет.



Открываем .apk приложения с использованием какого-нибудь средства для реверс-инжениринга и смотрим главную Activity. Там можно увидеть методы onSendMessageClick и onSendFormulaClick. Они создают Intent с action «com.yandex.tupper.action.NEW_MESSAGE» и extra с ключом «com.yandex.tupper.message» и рассылают его приложениям из списка (с использованием общего метода). Список они получают при помощи вызова PackageManager.queryBroadcastReceivers.

Можно продолжить средствами реверс-инжениринга изучать message, выделив его из кода приложения. Но этот путь специально (хоть и незначительно) усложнен дополнительным шифрованием RSA.

Есть другой способ, который лучше соответствует программированию под Android. Нужно написать приложение, обрабатывающее этот broadcast Intent. Создаем свое приложение, в манифесте прописываем receiver:

        <receiver
            android:name=".TupperMessageReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.yandex.tupper.action.NEW_MESSAGE" />
            </intent-filter>
        </receiver>

В обработчике достаем из extra сообщение и запускаем собственный Activity, в котором будем показывать результат обработки:

public class TupperMessageReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
        if (message == null) {
            return;
        }

        Intent activityIntent = new Intent(context, MainActivity.class);
        activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        activityIntent.putExtra(MainActivity.EXTRA_MESSAGE, message);
        context.startActivity(activityIntent);
    }
}

Теперь займемся самой расшифровкой сообщения. Если поискать в Яндексе «сообщение Таппера», можно статью о формуле Таппера в Википедии. В ней есть пример закодированной формула в виде числа:

48584...
4858450636189713423582095962494202044581400587983244549483093085061934704708809928450644769865524364849997247024915119110411605739177407856919754326571855442057210445735883681829823754139634338225199452191651284348332905131193199953502413758765239264874613394906870130562295813219481113685339535565290850023875092856892694555974281546386510730049106723058933586052544096664351265349363643957125565695936815184334857605266940161251266951421550539554519153785457525756590740540157929001765967965480064427829131488548259914721248506352686630476300

Это число отсылается в качестве сообщения при нажатии на кнопку Send formula. Результат декодирования числа тоже есть в статье в Википедии. Его можно использовать для отладки своей реализации алгоритма декодирования.

Реализуем на Java алгоритм декодирования и для красоты преобразуем полученное бинарное число в монохромный Bitmap:

 public class TupperCodec {
    static final int TUPPER_R = 17;

    private TupperCodec() {}

    public static BigInteger decodeFromDecString(String decString) throws NumberFormatException {
        BigInteger data = new BigInteger(decString, 10);
        return data.divide(BigInteger.valueOf(TUPPER_R));
    }

    public static String getNumberAsBinImage(BigInteger number) {
        String binString = number.toString(2);
        StringBuffer result = new StringBuffer(binString.length());

        int i;
        for (i = 0; i < binString.length(); i++) {
            if ((i % TUPPER_R) == 0 && i > 0) {
                result.append('\n');
            }
            result.append(binString.charAt(binString.length() - 1 - i));
        }
        int numberOfRows = (binString.length() + TUPPER_R - 1) / TUPPER_R;
        for (; i < numberOfRows * TUPPER_R; i++) {
            result.append(~_~quot�quot~_~);
        }
        return result.toString();
    }

    public static Bitmap getBinImageAsBitmap(String binImage) {
        String[] lines = binImage.split("\n");
        int[] colors = new int[lines.length * TUPPER_R];
        for (int i = 0; i < lines.length; i++) {
            String line = lines[i];
            assert line.length() == TUPPER_R;
            for (int j = 0; j < line.length(); j++) {
                colors[i * TUPPER_R + j] = line.charAt(j) == '0' ? Color.WHITE : Color.BLACK;
            }
        }
        return Bitmap.createBitmap(colors, TUPPER_R, lines.length, Bitmap.Config.ARGB_8888);
    }
}

В Activity вызовем декодирование и отобразим результат в виде Bitmap в ImageView:

 public class MainActivity extends AppCompatActivity {

    static final String EXTRA_MESSAGE = "com.yandex.tupper.message";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = getIntent();
        if (intent != null && intent.hasExtra(MainActivity.EXTRA_MESSAGE)) {
            String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);

            try {
                BigInteger data = TupperCodec.decodeFromDecString(message);
                String binImage = TupperCodec.getNumberAsBinImage(data);
                Bitmap bitmap = TupperCodec.getBinImageAsBitmap(binImage);

                ImageView imageView = findViewById(R.id.message_image);
                imageView.setImageBitmap(bitmap);

                CharSequence toastText =
                        TextUtils.ellipsize(message, new TextPaint(), 400, TextUtils.TruncateAt.END);
                Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show();
            } catch (NumberFormatException ex) {
                Toast.makeText(this, getString(R.string.invalid_message), Toast.LENGTH_LONG).show();
            }
        }
    }
}


Скриншот декодированного сообщения

Ответ — clck.ru/FevR5.

hack it! #7

Автор: Игорь Еремеев
Бинарный файл

Нужно взломать алгоритм проверки ключа в приложении.

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

Формат ответа: при вводе правильного ключа получается строка-решение для прохождения этой задачи.

Решение

Откроем .apk как zip-архив и возьмем из него classes.dex — скомпилированный Java-код нашего приложения. Далее файл можно декомпилировать в читаемый вид с помощью, например, связки dex2jar и Android Studio. dex2jar восстановит Java-байткод, а в составе Android Studio в стандартной поставке есть хороший декомпилятор для class-файлов. Есть и другие аналогичные программы, можно выбрать любую.

Один из немногих классов, который сохранил имя после proguard (кроме android.support.* и androidx.*, но они нам неинтересны), — это com.yandex.contest.keygenme.MainActivity, Activity нашего приложения. Видно, что инлайнинг разделил алгоритм на две части: MainActivity.a#doInBackground и класс b.b.a.a.b. В классе b можно увидеть массивы исходных данных, которые формируют ключ.



Это массивы c и d. Если вам удалось угадать в алгоритме вычисление N-го шестнадцатеричного знака числа ? (формулу Бэйли-Борвайна-Плаффа), то вы могли найти решение самым эффективным способом. Ответом являются такие значения ключа, что ?hexed[c[i] + key[i]] = d[i], i = 0..11. Значения подобраны таким образом, чтобы ответ был единственно возможным. Ответ можно подобрать по публично доступным в интернете данным о первых 50 тысячах hex-символах числа ?. Вот он: 537306089144. В этом случае программа выдает ответ для контеста key_2056BE33E064.

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



Видно, что каждая итерация обрабатывается независимо от других, а успех итерации определяется по условию if (var12[0] == var8) {… }. Значение var7 в начале итерации — это i-я цифра пользовательского ввода. Нетрудно изменить код так, чтобы значение var7 для каждой итерации пробегало от 0 до 9 и выводилось в консоль только при выполнении условия в конце цикла. Это и будет ключ. Можно было переписать код, переупаковать с ним .apk (либо запустить этот фрагмент на десктопной JVM) и получить ответ.

hack it! #8

Автор: Александр Васин
Бинарный файл

Прогуливаясь по радиорынку небольшого города N китайской провинции Гуандун, ваш друг увидел магнитолу для своего старого автомобиля. Ее характеристики внушают уважение: операционная система Android версии 4.4, экран с разрешением 1200 ? 600, четыре ядра у процессора и даже встроенный FM-радиоприемник. Приехав из командировки домой, друг установил магнитолу в автомобиль и понял, что FM-радио в ней не работает. Он смог скопировать приложение FM-радио себе на флешку и решил обратиться к вам, как к программисту с многолетним опытом.

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

Ответом к задаче будет являться последовательность методов, необходимых для выполнения следующих действий:

1. Изменить частоту радио на 106.00 кГц
2. Автопоиск (seek) следующей радиостанции вверх
3. Метод, возвращающий текущую частоту

Скачайте приложение, которое прислал ваш друг.

Примечания
Формат ответа: Вызовы методов необходимо перечислить через точку с запятой вместе с их аргументами. В конце ответа точка с запятой не ставится.

Пример ответа: Например, если метод a(106.00) изменяет текущую частоту на 106.00 кГц, метод b(true) ищет радиостанцию вверх, а метод c() возвращает частоту, то ответом будет строка a(106.00);b(true);c()

Решение

Наша задача: найти методы, благодаря которым FM-радио управляется приложением. Весь код в .apk лежит в dex-файлах. Нам нужно декомпилировать приложение обратно в Java. Это легко, на наш взгляд, самый удобный декомпилятор — jadx.

Получив декомплированную .apk, мы можем посмотреть ее ресурсы. В манифесте объявлена только одна MainActivity. Посмотрим ее код: fragment_holder заменяется на RadioFullFragment и ничего более. Начнем путешествовать по классам. В исходном коде фрагмента есть RadioFullPresenter, содержащий ChangeRadioFrequencyUsecase, из которого мы попадаем в интерфейс RadioRepo. Найдем его реализацию.

Поищем по всем файлам «implements RadioRepo». Реализация одна: RadioRepoImpl. Становится понятно, что нам нужен некий класс b. ServiceConnection — явное подключение к сервису. Заметим, что поле b присваивается только в lambda$new$0$RadioRepoImpl. Поискав, попадаем в $$Lambda$RadioRepoImpl$FYTR9gAgZEGvrLjkbkpAIup7OKw, которая передается в метод serviceConnection у IRadioService radioService. Это интерфейс, его реализует класс RadioService.

Открываем его: в onServiceConnected есть строчка «b radioManager = Stub.asInterface(service)». Осталось понять, к какому сервису мы подключаемся. Для этого поищем «bindService». Находим bindService с интентом «com.some_company.help_service». Сервис должен быть зарегистирован в манифесте, возвращаемся туда и попадаем в com.some_company.carradio.FmUtils. Видим подсказки, говорящие, что мы на верном пути. Осмотревшись, понимаем: j возвращает частоту, причем как целое значение: 8880 равносильно 88.00 МГц. Поэтому и для смены частоты нужно передать 8800. Целое число принимает только метод a. Логично, что он меняет частоту.

Осталось найти автопоиск. Кандидатов два: k и z. Возвращаемся обратно по всей цепочке и замечаем в RadioRepoImpl, что оба метода enableRadio() и disableRadio() вызывают метод k. Рядом есть метод z(boolean isForwardDirection), который очень похож на «Автопоиск (seek) следующей радиостанции вверх».

Собираем всё вместе и получаем ответ: a(10600);z(true);j().

hack it! #9

Автор: Эдуард Мацуков
Бинарный файл

Нужно извлечь из flatbuffers массив точек и наложить на карту. Желательно сделать маппинг в формат GPX или подобный. Отобразить координаты на карте. Исходные данные даны в бинарном виде, скачайте файл.

Схема FlatBuffers данных в задаче:

namespace ru.yandex.android.task;
struct GeoPoint { latitude:double; longitude:double; }
table Points { items:[GeoPoint]; }
root_type Points;

Формат ответа: слово, которое нужно преобразовать алгоритмом sha256. Это и будет ответом к задаче.

Решение

Сначала нужно понять, что такое FlatBuffers.

1. Находим компилятор fbs-схем и пытаемся скормить ему предоставленную в условии задачи схему.
2. На выходе получаем Java-код. Создаем тестовый JVM-проект и пробуем скомпилировать код, добавляем в зависимости Gradle библиотеку рантайма flatbuffers.
3. Пробуем скомпилировать пустой проект со сгенерированным кодом fbs-схем.
4. Если использовать самую последнюю версию fbs-компилятора, то проект не скомпилируется из-за ошибки в сгенерированном коде — там указан несуществующий метод. Чтобы исправить сгенерированный код, идем читать документацию по типам данных и структурам во FlatBuffers.
5. Из нужных разделов документации легко понять, как поправить код. Теперь structs называются таблицами, поэтому нужно поменять каждый несуществующий метод создания и работы со struct на table. После этого код сможет скомпилироваться.
6. Можно пойти еще чуть дальше и сразу переписать сгенерированный код на Kotlin. Так вы лучше поймете суть происходящих преобразований и механизм работы с FlatBuffers.

Оставляем в закладках документацию по fbs и начинаем писать прикладной код, который будет разбирать бинарный файл из условия задачи. Для этого нужно создать FlatBufferBuilder и скормить ему весь бинарный input. Затем попробуем преобразовать input в наши структуры:

val pointsList = Points.getRootAsPoints(buffer)
    val restoredPoints = (0 until pointsList.itemsLength())
            .mapNotNull(pointsList::items)

Если c описанием структур все хорошо, то преобразование пройдет успешно и мы получим список геоточек (lat, long). Этот список можно наложить на карту as-is либо преобразовать в GPX-трек.

Для преобразования в GPX-трек (или любой схожий формат) находим спецификацию формата. В случае с GPX это простой XML. Пишем код, который направит каждый наш геопоинт в XML-ноду с нужными атрибутами. Список XML-нод направляем в GPX-track-ноду. Полученный XML-файл загружаем в MAP-сервис и запускаем в нем построение трека на основе этого файла. Видим на карте слово «yandroid».

Наконец, необходимо выполнить преобразование над полученным словом. Для этого можно просто запустить CLI-команду:

echo "yandroid" | shasum -a 256

Результат и будет ответом к задаче.

Система Android


dig it! #1

Автор: Артём Витер
Бинарный файл

QA-инженер прислал отчет об ошибке.

Шаги воспроизведения:

— Запустить приложение (откроется экран ’Screen 1’)
— Нажать кнопку GO NEXT (откроется экран ’Screen 2’)
— Нажать кнопку GO BACK
— Как только появится кнопка GO NEXT, нажать ее
— Подождать 5–10 секунд

Ожидаемый результат: откроется экран ’Screen 2’.

Фактический результат: приложение зависнет, появится сообщение «Приложение не отвечает».

У вас есть только .apk приложения.

Примечания
Формат ответа

Cтрока в формате stack trace, которая приводит к зависанию приложения.

Пример

Если stack trace порожденной последовательности вызовов выглядит так…

at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:326)
at android.os.Looper.loop(Looper.java:160)
at com.android.server.SystemServer.run(SystemServer.java:454)
at com.android.server.SystemServer.main(SystemServer.java:294)
at java.lang.re?ect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:838)

… и вы считаете, что проблема в классе RuntimeInit.java, в строке 493, то пример ответа:

com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)

Решение

Пробуем воспроизвести баг, убеждаемся, что действительно появляется сообщение «Приложение не отвечает».

Получаем файл дампом потоков приложения из /data/anr/traces.txt или /data/anr/anr_* (https://developer.android.com/topic/performance/vitals/anr#pull_a_traces_file). Для этого можно использовать следующие adb-команды:

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

(Чтобы выполнить команду adb root на эмуляторе, нужно запустить образ без Google API.)

Находим stack trace главного потока приложения. Ответ — это первая строка из stack trace, которая относится к пакету приложения:

com.droid.mission.anrapk.MainActivity$2.onClick(MainActivity.java:43)



dig it! #2

Автор: Алексей Иванов

Создаем новый проект в Android Studio, добавляем в качестве зависимостей большое количество тяжеловесных библиотек, чтобы проект компилировался под MultiDex. По-умолчанию, в процессе сборки apk-файла все классы упаковываются в .dex-файл под лимит (65536). Результирующее число классов/методов можно узнать открыв собранную apk в самой Andorid Studio и выделив нужный .dex-файл.

Задача — оптимизировать упаковку классов dex-компилятором таким образом, чтобы в первом dex-файле (главный файл, из которого загружается приложение) всегда оставалось максимально возможное свободное место. Ограничения по количеству dex-файлов нет.

Условия: multidex проект, android gradle plugin версии 3.0.0 – 3.1.4

Настройте сборку соответствующим образом.

Какая манипуляция со сборкой необходима для достижения поставленного результата?

Решение

Создаем или находим проект с огромным количеством дополнительных библиотек, который не умещается в один dex-файл. Изучаем цепочку сборки проекта при помощи Gradle — нужно понять, какие шаги проходит проект прежде, чем получится .apk.

Нас интересует таск рекомпиляции Java-байткода в dex-байткод для виртуальной машины ART/Dalvik: transformClassesWithDexBuilderForDebug. Ищем в интернете устройство dex-компилятора, его механику работы. Будет полезно заглянуть в исходный код Android Gradle plugin (AGP) и изучить механизм работы Gradle с компилятором. По сути, компилятор — внешний инструмент, Gradle готовит для него исходные файлы и запускает его с определенными флагами и настройками. Выясняем, за что отвечают разные флаги и настройки отвечают. Часть информации можно найти в исходном коде AGP, другую часть — прямо в DSL-модели Gradle при выставлении параметров в файле build.gradle.

В версиях плагина 3.1.* мы наткнемся на параметр minimal-main-dex. Он и служит ответом, это можно понять из его описания.

dig it! #3

Автор: Алексей Иванов
Вы — разработчик вредоносной прошивки для Андроид-устройств и хотите заблокировать любые действия пользователя на экране смартфона своим баннером.

Какое окно системы вы замените своим баннером?

Формат ответа: число — тип этого системного окна.

Решение

Для начала нужно понять, что же за типы окон от нас требуются. За окна в Android отвечает одноименный WindowManager. Внутри у него два метода и два задекларированных эксепшена. Ничего интересного, кроме WindowManager.LayoutParams. В нём много констант, а так же перечислены необходимые нам типы окон, начиная с TYPE_APPLICATION.

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

Вспомним, что в Android SDK, который доступен приложениям, попадают далеко не все составляющие классов в фреймворке. Посмотрим, что находится в исходном WindowManager в AOSP. Здесь типов окон стало больше, можно перебрать их все, а можно предположить, какое из них будет отрисовано выше всех и заблокирует любые действия пользователя.

Посмотрим, где в системе использются эти типы окон, на примере TYPE_BOOT_PROGRESS. Можно заметить в том числе метод getWindowLayerFromTypeLw в WindowManagerPolicy. Его Javadoc гласит:

Returns the layer assignment for the window type. Allows you to control how different kinds of windows are ordered on-screen

Кажется, мы нашли то, что хотели. Смотрим на большой switch-case внутри и видим, что максимальный layer — у TYPE_POINTER, тип 2018. Это и есть ответ.

dig it! #4

Автор: Алексей Иванов

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

Вы решили интегрировать эти правила в свою прошивку. Они могут определить, вредоносное ли ПО, по названию компонента, который запускается в данный момент.

Как в системе Android называется директория, в которой вам следует разместить правила фильтрования интентов для приложений?

Решение

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

Для примера рассмотрим метод startActivity интерфейса Context. Одной из имплементаций интерфейса Context является Activity — посмотрим, как в ней реализован метод startActivity. Через пару вложенных вызовов добираемся до метода startActivityForResult. В его реализации можно заметить, что для запуска Activity используется mInstrumentation.execStartActivity.

В чем заключается реализация этого метода? в конце вызывается startActivity у ActivityManagerService, который получается через ServiceManager.getService(Context.ACTIVITY_SERVICE).

Смотрим реализацию этого метода в ActivityManagerService. В имлементации startActivityAsUser, в которую уходит startActivity, создается ActivityStarter и на нем вызывается execute. Смотрим, что у него внутри.

Cпустя несколько вложенных вызовов доходим до довольно массивного метода startActivity. Если внимательно посмотреть на его имплементацию, то можно заметить флаг abort, при выставлении которого в true возвращается код ошибки START_ABORTED и не происходит старта Activity.

Если посмотреть, как определяется этот флаг, то внимание сразу привлекает вызов mService.mIntentFirewall.checkStartActivity. Смотрим, что это за метод. В описании сказано:

This is called from ActivityManager to check if a start activity intent should be allowed.

Осталось только найти, откуда IntentFirewall считывает правила. В самом начале файла можно увидеть константу RULES_DIR. Ответ: ifw.

dig it! #5

Автор: Илья Малахов

В Андроиде существует системная ошибка. Предположим, у пользователя в контактах записан номер организации (например, банка) — 8800ХХХХХХХ. При этом, существует номер, у которого первая цифра является кодом страны (+8800ХХХХХХХ). Очевидно, что номера 8800ХХХХХХХ и +8800ХХХХХХХ разные, но при входящем/исходящем звонке информация отображается о том, который сохранен в контакты.

В Андроиде есть специальный механизм для сопоставления вызовов с номерами из контактов. В нём есть проблема. По умолчанию считается, что номера +8800ХХХХХХХ и 8800ХХХХХХХ одинаковые.

Необходимо исследовать проблему и найти способ исправления стандартными средствами платформы, не модифицируя исходный код (java/c/c++). Ответом является строка, в которой сделано изменение.

Скачать исходники можно на этой странице: source.android.com
Посмотреть код — здесь: androidxref.com

Решение

При анализе кода в Dialer видно, что информация о номере не хранится непосредственно в приложении, а запрашивается по URI с authorities = "content://com.android.contacts".

Ищем ContentProvider. Для этого просто грепаем по исходникам эту URI и находим ContactsPoviider. Смотрим код: находим, где собирается собирается SQL-запрос для поиска контакта по номеру телефона. Там есть интересная строчка:

sb.append("PHONE_NUMBERS_EQUAL(" + Tables.DATA + "." + Phone.NUMBER + ", ");

PHONE_NUMBERS_EQUAL — это кастомный SQL-метод. Ищем реализацию sqlite в дереве исходного кода.

Если изучить метод sqlite, станет понятно, что мы можем воспользоваться точным сравнением телефонных номеров. Возвращаемся в ContactsProvider и видим, что достаточно изменить системный ресурс config_use_strict_phone_number_comparation, чтобы вызывался метод явной компарации. Ответ:

<bool name="config_use_strict_phone_number_comparation">true</bool>