Сегодня в выпуске: выполнение shell-кода с помощью JIT-компилятора, рассказ о том, куда смартфоны сливают данные, десять самых популярных вопросов о Kotlin, рассказ о WorkManager и Slices, представленных на Google I/O, и, конечно же, подборка свежих инструментов и библиотек.
Инструменты
Почитать
Выполнение shell-кода с помощью JIT-компилятора
— презентация, посвященная внедрению shell-кода в Android путем эксплуатации уязвимости в виртуальной машине Dalvik. Как мы все знаем, приложения для Android написаны на Java (или Kotlin) и скомпилированы в так называемый байт-код, исполняет который, в отличие от машинных инструкций, не процессор напрямую, а виртуальная машина.
В Android эта виртуальная машина изначально носила имя Dalvik, но была достаточно медлительной из-за того, что интерпретировала байт-код последовательно. С выходом версии Android 2.1 разработчики это поправили, внедрив в Dalvik так называемый JIT-компилятор (в версии Android 5.0 он был заменен на AOT-компилятор, но в 7.0 вернулся). Он транслирует в машинные инструкции целые куски байт-кода, так что виртуальной машине не приходится делать это последовательно, расходуя драгоценное время.
Оказалось, однако, что JIT-компилятор уязвим к подмене кода. То есть скомпилированные им фрагменты машинных инструкций можно подменить, заставив виртуальную машину выполнить совершенно другой код.
Виртуальная машина хранит ссылки на участки памяти со скомпилированными машинными инструкциями в структурах JitEntry, которые организованы в таблицу JitTable. Используя рефлексию и классы libcore.io.Posix (содержит методы mmap и munmap для манипуляции памятью), а также классы libcore.io.Memory и libcore.io.MemoryBlock, в память можно загрузить shell-код, а затем подменить адрес в JitEntry, чтобы вместо актуального машинного кода он ссылался на shell-код.
Механизм выполнения shell-кода отличается в разных версиях Android, но возможен и в 4.4.4, и в 7.1. А вот в Android P, скорее всего, начнутся проблемы, так как она запрещает вызов закрытых от сторонних приложений API. Также авторы исследования не упомянули, работает ли этот метод в отношении других приложений и системы в целом или только текущего процесса. Ведь Android запускает каждое приложение в собственной виртуальной машине, и память одной виртуальной машины не пересекается с другой.
К каким доменам чаще всего подключаются смартфоны
— очень короткая, но занятная статья о том, какие веб-сайты и сервисы следят за пользователями Android. Автор написал блокировщик рекламы для смартфонов Samsung, который перенаправляет DNS-запросы устройства на специальные DNS-серверы. Вместо IP-адресов рекламных и трекинговых сетей эти серверы возвращают 127.0.0.1, чем блокируют рекламу и трекеры.
Спустя несколько месяцев работы серверы накопили статистику заблокированных адресов. И первые три места с большим отрывом занимают следующие адреса:
Разработчику
10 самых популярных вопросов о Kotlin
— десять (на самом деле девять) наиболее часто задаваемых вопросов о Kotlin на Stack Overflow и ответы на них. Приводим очень краткую выжимку (лучше все-таки почитать оригинал).
1. Чем отличаются Array и IntArray?
Первый создает массив высокоуровневого типа Integer, второй — примитивного типа int. IntArray более высокопроизводительный и рекомендуется к использованию в любых ситуациях.
2. Чем отличается Iterable и Sequence?
Iterable — это стандартный интерфейс Java. Реализующие его классы (List и Set, например) обрабатывают всю коллекцию целиком, что может плохо сказаться на производительности. Например, следующий код выполнит две операции (filter и map) над всеми элементами списка, перед тем как взять первые пять элементов (take):
val people: List<Person> = getPeople()
val allowedEntrance = people
.filter { it.age >= 21 }
.map { it.name }
.take(5)
Sequence, с другой стороны, обрабатывает коллекции в ленивом режиме. Такой подход позволяет более эффективно обрабатывать коллекции в несколько проходов (как в этом примере) и выполнять процессинг только необходимого количества элементов:
val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
.filter { it.age >= 21 }
.map { it.name }
.take(5)
.toList()
В отличие от предыдущего, этот код будет обрабатывать каждый элемент списка отдельно, до тех пор пока не наберется пять элементов.
Что использовать? В случае небольших коллекций Iterable показывает лучшую производительность. Но если ты имеешь дело с очень большой коллекцией, тогда лучше задуматься о применении Sequence. Если же тебе нужна генерируемая на лету бесконечная коллекция, то Sequence (с его функцией generateSequence()) — твой единственный выход.
3. Проход по элементам коллекции
В Kotlin есть множество способов обойти все элементы коллекции в цикле. Однако самые производительные из них следующие:
for (arg in args) {
println(arg)
}
args.forEach { arg ->
println(arg)
}
В своей работе они используют Iterator, и это быстрее, чем последовательное получение каждого элемента из списка.
Вариант с индексами:
for ((index, arg) in args.withIndex()) {
println("$index: $arg")
}
args.forEachIndexed { index, arg ->
println("$index: $arg")
}
4. SAM-преобразования
Как и Java 8, Kotlin поддерживает SAM-преобразования (Single Abstract Method). Это значит, что вместо такого кода:
button.setListener(object: OnClickListener {
override fun onClick(button: Button) {
println("Clicked!")
}
})
можно написать такой:
button.setListener {
println("Clicked!")
}
и компилятор поймет, что к чему.
Но есть несколько нюансов.
В Kotlin нет поддержки статических полей и методов. Если тебе необходимо создать класс, содержащий только статические члены, просто объяви его как объект:
object Foo {
fun x() { ... }
}
Если же нужно сделать статическими только отдельные поля и методы, используй companion object:
class Foo {
companion object {
fun x() { ... }
}
fun y() { ... }
}
Если какой-то метод должен быть запущен только один раз независимо от количества созданных на основе класса объектов, используй статический инициализатор:
class X {
companion object {
init {
println("Static initialization!")
}
}
}
6. Умное приведение типов и null
Если ты попробуешь сделать так:
class Dog(var toy: Toy? = null) {
fun play() {
if (toy != null) {
toy.chew()
}
}
}
Kotlin сообщит тебе, что не может использовать умное приведение типов, потому что toy — это изменяемое свойство. Так происходит потому, что компилятор не может быть уверен, что между проверкой toy на null и вызовом метода chew() другой поток не сделает toy = null.
Чтобы это исправить, достаточно сделать так:
class Dog(var toy: Toy? = null) {
fun play() {
it?.chew()
}
}
Или так:
class Dog(var toy: Toy? = null) {
fun play() {
toy?.let {
it.chew()
}
}
}
В данном случае последний вариант избыточен, но он подойдет, если тебе необходимо не просто вызвать метод, а, например, записать возвращаемое им значение в другую переменную, которая не должна быть null.
7. Что конкретно делает оператор !!?
Оператор !!, заставляющий среду исполнения Kotlin выполнить код даже в том случае, если его левая часть равна null, на самом деле выполняет то же самое, что такая функция-расширение:
fun <T> T?.forceNonNull(): T {
if (this == null) {
throw KotlinNullPointerException("Oops, found null")
} else {
return this as T
}
}
Следующие две строки равнозначны:
airdate!!.getWeekday()
airdate.forceNonNull().getWeekday()
8. Что делать с аргументами при переопределении функции Java?
Программируя на Kotlin, ты всегда знаешь, может ли аргумент метода быть null. Но что делать, если ты переопределяешь метод, написанный на Java — языке, который не умеет сообщать, может ли аргумент быть null?
В этом случае тебе ничего не остается, кроме как всегда проверять аргументы на null. Это избыточно и не очень красиво выглядит в коде, но лучше перестраховаться.
9. Как использовать несколько функций-расширений с одинаковыми именами?
Одна из самых интересных возможностей Kotlin — это функции-расширения. Ты можешь в любое время добавить к любому доступному тебе классу любой дополнительный метод, независимо от того, имеешь ты доступ к коду класса или нет.
Но возникает одна проблема: если ты захочешь использовать две функции-расширения с одинаковыми именами, ты не сможешь обратиться к ним, используя полное имя пакета (например, com.example.code.indent()). В этом случае следует сделать так:
import com.example.code.indent as indent4
import com.example.square.indent as indent2
То есть просто импортировать функции под разными именами.
Список доступных для использования системных API
. Разработчики Android уже рассказывали об ограничениях, которые Android P будет накладывать на использование недокументированных/скрытых API. Вкратце: если ты попытаешься использовать рефлексию, чтобы получить доступ к скрытым от сторонних приложений API, то ОС выбросит исключение NoSuchFieldException или NoSuchMethodException.
Поначалу эта функция будет работать только в отношении очень редко используемых или совсем не используемых API. Однако в следующих версиях Google начнет ужесточать правила и расширять запрет на все большее количество API, предлагая взамен открытую для использования альтернативу.
Какие API доступны, а какие уже нельзя использовать? Вот так называемый . В нем более 11 тысяч методов и полей.
Что такое Slices в Android P и как их использовать?
— статья с рассказом о так называемых слайсах (Slice), новой функции Android, которая появилась в Android P, но вряд ли будет активирована к релизу (а может, и будет).
Слайсы — это часть новой подсистемы Actions on Google, которая позволяет разработчикам интегрировать свои приложения в Google Assistent. Работает это так: Google придумала ряд , таких как actions.intent.PLAY_GAME и actions.intent.GET_CRYPTOCURRENCY_PRICE, которые отправляются приложениям, когда пользователь делает определенный запрос в ассистенте. В данном случае это может быть что-то вроде «хочу поиграть в игру» и «цена биткойна».
Ассистент обрабатывает запрос, вычленяет из него семантическую часть, затем рассылает приложениям соответствующий интент. Приложение может ответить на этот интент с помощью SliceProvider’а, который позволяет запрограммировать карточку с информацией для Google Assistent. Эту карточку Google Assistent выведет на экран.
Как работать с WorkManager
— хорошая краткая статья о том, как использовать WorkManager, новую support-библиотеку Google для выполнения фоновой работы.
WorkManager был разработан как ответ на бардак в средствах фонового исполнения в разных версиях Android. До Android 5.0 нам предлагали использовать AlarmManager и сервисы для выполнения фоновых задач. Начиная с Android 5.0 появился JobScheduler, который толком не работал до версии Android 6.0, и вместо него приходилось использовать Firebase JobDispatcher, хотя сервисы продолжали нормально поддерживаться вплоть до версии Android 8, где Google ввела ограничение на их исполнение в несколько минут при уходе приложения в сон или получении push-уведомления.
WorkManager скрывает все эти нюансы и предлагает простой в использовании API для запуска фоновых задач. В зависимости от версии Android, на которой будет запущено приложение, он сам выберет лучший способ исполнения задачи, позволив тебе выбросить всю эту кашу из головы.
В простейшем случае создание и запуск задачи с помощью WorkManager выглядит так. Создаем задачу, наследуясь от класса Worker:
class YourWorker: Worker {
override fun WorkerResult doWork() {
// Делаем свои дела
return WorkerResult.SUCCESS
}
}
Затем создаем запрос на запуск задачи и ставим ее в очередь:
val work: OneTimeWorkRequest = OneTimeWorkRequest.Builder(YourWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
Так как мы не указали никаких условий выполнения задачи, она будет выполнена сразу.
Добавим условия:
val constraints: Constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
val work: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SomeWorker::class.java)
.setConstraints(constraints)
.build()
Чтобы запустить периодическую (повторяющуюся) задачу, используем PeriodicWorkRequest:
val recurringWork: PeriodicWorkRequest = PeriodicWorkRequest.Builder(YourWorker::class.java, 3, TimeUnit.HOURS).build()
WorkManager.getInstance().enqueue(recurringWork)
Задачи также можно объединять в цепочки:
WorkManager.getInstance()
.beginWith(firstWork)
.then(secondWork)
.then(thirdWork)
.enqueue()
Инструменты
Библиотеки
Инструменты
- — основанный на Frida инструмент для динамического анализа приложений под Android. Позволяет быстро сгенерировать скрипт и внедрить его в приложение;
- — модуль Node.js и утилита командной строки для подписи приложений iOS (файлов IPA);
- — скрипт для извлечения расшифрованных iOS-приложений с устройства;
- — огромное количество документов и книг, посвященных безопасности iOS.
Почитать
Выполнение shell-кода с помощью JIT-компилятора
— презентация, посвященная внедрению shell-кода в Android путем эксплуатации уязвимости в виртуальной машине Dalvik. Как мы все знаем, приложения для Android написаны на Java (или Kotlin) и скомпилированы в так называемый байт-код, исполняет который, в отличие от машинных инструкций, не процессор напрямую, а виртуальная машина.
В Android эта виртуальная машина изначально носила имя Dalvik, но была достаточно медлительной из-за того, что интерпретировала байт-код последовательно. С выходом версии Android 2.1 разработчики это поправили, внедрив в Dalvik так называемый JIT-компилятор (в версии Android 5.0 он был заменен на AOT-компилятор, но в 7.0 вернулся). Он транслирует в машинные инструкции целые куски байт-кода, так что виртуальной машине не приходится делать это последовательно, расходуя драгоценное время.
Оказалось, однако, что JIT-компилятор уязвим к подмене кода. То есть скомпилированные им фрагменты машинных инструкций можно подменить, заставив виртуальную машину выполнить совершенно другой код.
Виртуальная машина хранит ссылки на участки памяти со скомпилированными машинными инструкциями в структурах JitEntry, которые организованы в таблицу JitTable. Используя рефлексию и классы libcore.io.Posix (содержит методы mmap и munmap для манипуляции памятью), а также классы libcore.io.Memory и libcore.io.MemoryBlock, в память можно загрузить shell-код, а затем подменить адрес в JitEntry, чтобы вместо актуального машинного кода он ссылался на shell-код.
Механизм выполнения shell-кода отличается в разных версиях Android, но возможен и в 4.4.4, и в 7.1. А вот в Android P, скорее всего, начнутся проблемы, так как она запрещает вызов закрытых от сторонних приложений API. Также авторы исследования не упомянули, работает ли этот метод в отношении других приложений и системы в целом или только текущего процесса. Ведь Android запускает каждое приложение в собственной виртуальной машине, и память одной виртуальной машины не пересекается с другой.
К каким доменам чаще всего подключаются смартфоны
— очень короткая, но занятная статья о том, какие веб-сайты и сервисы следят за пользователями Android. Автор написал блокировщик рекламы для смартфонов Samsung, который перенаправляет DNS-запросы устройства на специальные DNS-серверы. Вместо IP-адресов рекламных и трекинговых сетей эти серверы возвращают 127.0.0.1, чем блокируют рекламу и трекеры.
Спустя несколько месяцев работы серверы накопили статистику заблокированных адресов. И первые три места с большим отрывом занимают следующие адреса:
- graph.facebook.com
- mobile.pipe.aria.microsoft.com
Разработчику
10 самых популярных вопросов о Kotlin
— десять (на самом деле девять) наиболее часто задаваемых вопросов о Kotlin на Stack Overflow и ответы на них. Приводим очень краткую выжимку (лучше все-таки почитать оригинал).
1. Чем отличаются Array и IntArray?
Первый создает массив высокоуровневого типа Integer, второй — примитивного типа int. IntArray более высокопроизводительный и рекомендуется к использованию в любых ситуациях.
2. Чем отличается Iterable и Sequence?
Iterable — это стандартный интерфейс Java. Реализующие его классы (List и Set, например) обрабатывают всю коллекцию целиком, что может плохо сказаться на производительности. Например, следующий код выполнит две операции (filter и map) над всеми элементами списка, перед тем как взять первые пять элементов (take):
val people: List<Person> = getPeople()
val allowedEntrance = people
.filter { it.age >= 21 }
.map { it.name }
.take(5)
Sequence, с другой стороны, обрабатывает коллекции в ленивом режиме. Такой подход позволяет более эффективно обрабатывать коллекции в несколько проходов (как в этом примере) и выполнять процессинг только необходимого количества элементов:
val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
.filter { it.age >= 21 }
.map { it.name }
.take(5)
.toList()
В отличие от предыдущего, этот код будет обрабатывать каждый элемент списка отдельно, до тех пор пока не наберется пять элементов.
Что использовать? В случае небольших коллекций Iterable показывает лучшую производительность. Но если ты имеешь дело с очень большой коллекцией, тогда лучше задуматься о применении Sequence. Если же тебе нужна генерируемая на лету бесконечная коллекция, то Sequence (с его функцией generateSequence()) — твой единственный выход.
3. Проход по элементам коллекции
В Kotlin есть множество способов обойти все элементы коллекции в цикле. Однако самые производительные из них следующие:
for (arg in args) {
println(arg)
}
args.forEach { arg ->
println(arg)
}
В своей работе они используют Iterator, и это быстрее, чем последовательное получение каждого элемента из списка.
Вариант с индексами:
for ((index, arg) in args.withIndex()) {
println("$index: $arg")
}
args.forEachIndexed { index, arg ->
println("$index: $arg")
}
4. SAM-преобразования
Как и Java 8, Kotlin поддерживает SAM-преобразования (Single Abstract Method). Это значит, что вместо такого кода:
button.setListener(object: OnClickListener {
override fun onClick(button: Button) {
println("Clicked!")
}
})
можно написать такой:
button.setListener {
println("Clicked!")
}
и компилятор поймет, что к чему.
Но есть несколько нюансов.
- Если ты используешь SAM-преобразование, ты не можешь обратиться к анонимному объекту, созданному в процессе (объекту OnClickListener, если говорить о примере выше).
- Ты можешь столкнуться с ошибкой компилятора, который заявит, что тип возвращаемого значения не совпадает. Такое происходит, если оригинальный метод требует вернуть значение определенного типа, а внутри лямбды ты вызываешь функцию, которая возвращает другой тип. В этом случае надо лишь добавить в конце лямбды значение нужного типа, например true или false, если необходимо вернуть Boolean.
В Kotlin нет поддержки статических полей и методов. Если тебе необходимо создать класс, содержащий только статические члены, просто объяви его как объект:
object Foo {
fun x() { ... }
}
Если же нужно сделать статическими только отдельные поля и методы, используй companion object:
class Foo {
companion object {
fun x() { ... }
}
fun y() { ... }
}
Если какой-то метод должен быть запущен только один раз независимо от количества созданных на основе класса объектов, используй статический инициализатор:
class X {
companion object {
init {
println("Static initialization!")
}
}
}
6. Умное приведение типов и null
Если ты попробуешь сделать так:
class Dog(var toy: Toy? = null) {
fun play() {
if (toy != null) {
toy.chew()
}
}
}
Kotlin сообщит тебе, что не может использовать умное приведение типов, потому что toy — это изменяемое свойство. Так происходит потому, что компилятор не может быть уверен, что между проверкой toy на null и вызовом метода chew() другой поток не сделает toy = null.
Чтобы это исправить, достаточно сделать так:
class Dog(var toy: Toy? = null) {
fun play() {
it?.chew()
}
}
Или так:
class Dog(var toy: Toy? = null) {
fun play() {
toy?.let {
it.chew()
}
}
}
В данном случае последний вариант избыточен, но он подойдет, если тебе необходимо не просто вызвать метод, а, например, записать возвращаемое им значение в другую переменную, которая не должна быть null.
7. Что конкретно делает оператор !!?
Оператор !!, заставляющий среду исполнения Kotlin выполнить код даже в том случае, если его левая часть равна null, на самом деле выполняет то же самое, что такая функция-расширение:
fun <T> T?.forceNonNull(): T {
if (this == null) {
throw KotlinNullPointerException("Oops, found null")
} else {
return this as T
}
}
Следующие две строки равнозначны:
airdate!!.getWeekday()
airdate.forceNonNull().getWeekday()
8. Что делать с аргументами при переопределении функции Java?
Программируя на Kotlin, ты всегда знаешь, может ли аргумент метода быть null. Но что делать, если ты переопределяешь метод, написанный на Java — языке, который не умеет сообщать, может ли аргумент быть null?
В этом случае тебе ничего не остается, кроме как всегда проверять аргументы на null. Это избыточно и не очень красиво выглядит в коде, но лучше перестраховаться.
9. Как использовать несколько функций-расширений с одинаковыми именами?
Одна из самых интересных возможностей Kotlin — это функции-расширения. Ты можешь в любое время добавить к любому доступному тебе классу любой дополнительный метод, независимо от того, имеешь ты доступ к коду класса или нет.
Но возникает одна проблема: если ты захочешь использовать две функции-расширения с одинаковыми именами, ты не сможешь обратиться к ним, используя полное имя пакета (например, com.example.code.indent()). В этом случае следует сделать так:
import com.example.code.indent as indent4
import com.example.square.indent as indent2
То есть просто импортировать функции под разными именами.
Список доступных для использования системных API
. Разработчики Android уже рассказывали об ограничениях, которые Android P будет накладывать на использование недокументированных/скрытых API. Вкратце: если ты попытаешься использовать рефлексию, чтобы получить доступ к скрытым от сторонних приложений API, то ОС выбросит исключение NoSuchFieldException или NoSuchMethodException.
Поначалу эта функция будет работать только в отношении очень редко используемых или совсем не используемых API. Однако в следующих версиях Google начнет ужесточать правила и расширять запрет на все большее количество API, предлагая взамен открытую для использования альтернативу.
Какие API доступны, а какие уже нельзя использовать? Вот так называемый . В нем более 11 тысяч методов и полей.
Что такое Slices в Android P и как их использовать?
— статья с рассказом о так называемых слайсах (Slice), новой функции Android, которая появилась в Android P, но вряд ли будет активирована к релизу (а может, и будет).
Слайсы — это часть новой подсистемы Actions on Google, которая позволяет разработчикам интегрировать свои приложения в Google Assistent. Работает это так: Google придумала ряд , таких как actions.intent.PLAY_GAME и actions.intent.GET_CRYPTOCURRENCY_PRICE, которые отправляются приложениям, когда пользователь делает определенный запрос в ассистенте. В данном случае это может быть что-то вроде «хочу поиграть в игру» и «цена биткойна».
Ассистент обрабатывает запрос, вычленяет из него семантическую часть, затем рассылает приложениям соответствующий интент. Приложение может ответить на этот интент с помощью SliceProvider’а, который позволяет запрограммировать карточку с информацией для Google Assistent. Эту карточку Google Assistent выведет на экран.
Как работать с WorkManager
— хорошая краткая статья о том, как использовать WorkManager, новую support-библиотеку Google для выполнения фоновой работы.
WorkManager был разработан как ответ на бардак в средствах фонового исполнения в разных версиях Android. До Android 5.0 нам предлагали использовать AlarmManager и сервисы для выполнения фоновых задач. Начиная с Android 5.0 появился JobScheduler, который толком не работал до версии Android 6.0, и вместо него приходилось использовать Firebase JobDispatcher, хотя сервисы продолжали нормально поддерживаться вплоть до версии Android 8, где Google ввела ограничение на их исполнение в несколько минут при уходе приложения в сон или получении push-уведомления.
WorkManager скрывает все эти нюансы и предлагает простой в использовании API для запуска фоновых задач. В зависимости от версии Android, на которой будет запущено приложение, он сам выберет лучший способ исполнения задачи, позволив тебе выбросить всю эту кашу из головы.
В простейшем случае создание и запуск задачи с помощью WorkManager выглядит так. Создаем задачу, наследуясь от класса Worker:
class YourWorker: Worker {
override fun WorkerResult doWork() {
// Делаем свои дела
return WorkerResult.SUCCESS
}
}
Затем создаем запрос на запуск задачи и ставим ее в очередь:
val work: OneTimeWorkRequest = OneTimeWorkRequest.Builder(YourWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
Так как мы не указали никаких условий выполнения задачи, она будет выполнена сразу.
Добавим условия:
val constraints: Constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build()
val work: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SomeWorker::class.java)
.setConstraints(constraints)
.build()
Чтобы запустить периодическую (повторяющуюся) задачу, используем PeriodicWorkRequest:
val recurringWork: PeriodicWorkRequest = PeriodicWorkRequest.Builder(YourWorker::class.java, 3, TimeUnit.HOURS).build()
WorkManager.getInstance().enqueue(recurringWork)
Задачи также можно объединять в цепочки:
WorkManager.getInstance()
.beginWith(firstWork)
.then(secondWork)
.then(thirdWork)
.enqueue()
Инструменты
- — утилита для манипуляции Android App Bundle, позволяет собирать, разбирать бандлы и генерировать APK для разных устройств;
- — инструмент для запуска нескольких Android-эмуляторов одновременно;
- — скрипт-обертка для ADB, позволяющий выполнить множество различных действий: включение/выключение Doze, мобильных данных, режима полета, разрешений, нажатие кнопок, снятие скриншотов и многие другие.
Библиотеки
- — коллекция ссылок на презентации, документацию, семплы кода и все, что было связано с Android на Google I/O 2018;
- — библиотека для шифрования настроек Android, созданных с помощью SharedPrefences;
- — плагин Android Studio для генерации data-классов Kotlin из JSON;
- — удобный фреймворк для тестирования приложений, написан на Kotlin с применением DSL;
- — приложение-пример, демонстрирующее использование Tensorflow Lite для реализации компьютерного зрения;
- — библиотека с реализацией редактора фотографий: инструменты рисования, фильтры, эмоджи-стикеры и другое;
- — два в одном: кнопка и прогрессбар;
- — диалог выбора файлов;
- — красивый индикатор загрузки;
- — нижняя панель навигации;
- — библиотека для автоматического сохранения состояния без необходимости реализовать методы onSaveInstanceState и onRestoreInstanceState.
Евгений Зобнин