Записки гиканутого

@channel_n5 Нравится 0
Это ваш канал? Подтвердите владение для дополнительных возможностей

Android: хакинг, кодинг и прочая жесть @ezobnin
Гео и язык канала
Россия, Русский
Категория
Технологии


Гео канала
Россия
Язык канала
Русский
Категория
Технологии
Добавлен в индекс
28.10.2017 23:53
реклама
Telegram-канал PristavBot
Информация о новшествах в сфере исполпроизводства
Админ канала? Добро пожаловать!
TAGIO - Самый желанный инструмент 2021 года стартовал!
Монетизация в Telegram 2021?
TAGIO.PRO это сделал еще в 2020! Присоединяйся!
261
подписчиков
~104
охват 1 публикации
~20
дневной охват
~27
постов / месяц
39.8%
ERR %
0.45
индекс цитирования
Репосты и упоминания канала
3 упоминаний канала
0 упоминаний публикаций
1 репостов
SysOps
Новые каналы
Groks
Android Tools
Каналы, которые цитирует @channel_n5
trgt
Android Tools
Последние публикации
Удалённые
С упоминаниями
Репосты
Using Flows for Form Validation in Android - короткая заметка о том, как реализовать валидацию форм с помощью Kotlin Flow. Интересна в первую очередь в качестве простой и наглядной демонстрации работы недавно появившегося StateFlow.

Допустим у нас есть форма с тремя полями: First Name, Password и User Id. Наша задача: сделать так, чтобы кнопка Submit активировалась только в том случае, если поле First Name содержит только символы латинского алфавита, поле Password содержит как минимум 8 символов, а поле User Id содержит хотя бы один символ подчеркивания.

Для хранения текущего значения поля будем использовать StateFlow:

private val _firstName = MutableStateFlow("")
private val _password = MutableStateFlow("")
private val _userID = MutableStateFlow("")

Дополнительно создадим три метода чтобы записывать значения в эти StateFlow:

fun setFirstName(name: String) {
_firstName.value = name
}

fun setPassword(password: String) {
_password.value = password
}

fun setUserId(id: String) {
_userID.value = id
}

Теперь объединим все три StateFlow в один Flow, который будет отдавать только значения true или false:

val isSubmitEnabled: Flow = combine(_firstName, _password, _userID) { firstName, password, userId ->
val regexString = "[a-zA-Z]+"
val isNameCorrect = firstName.matches(regexString.toRegex())
val isPasswordCorrect = password.length > 8
val isUserIdCorrect = userId.contains("_")
return@combine isNameCorrect and isPasswordCorrect and isUserIdCorrect
}

Этот код будет запускаться каждый раз, когда состояние любого из трех StateFlow изменится.

Теперь осталось только привязать три первых StateFlow к полям ввода:

private fun initListeners() {
editText_name.addTextChangedListener {
viewModel.setFirstName(it.toString())
}
editText_password.addTextChangedListener {
viewModel.setPassword(it.toString())
}
editText_user.addTextChangedListener {
viewModel.setUserId(it.toString())
}
}

А состояние кнопки Submit привязать к полученному в результате преобразования Flow:

private fun collectFlow() {
lifecycleScope.launch {
viewModel.isSubmitEnabled.collect { value ->
submit_button.isEnabled = value
}
}
}

Что делает весь этот код? При изменении любого из полей ввода будет автоматически изменено значение одного из трех StateFlow. Это, в свою очередь, повлечет за собой запуск функции combine, которая в итоге выпустит новое значение в поток isSubmitEnabled. На это действие среагирует код внутри функции collectFlow(). В итоге он изменит состояние кнопки.
Читать полностью
All you need to know about ArrayMap & SparseArray - статья об ArrayMap и SparseArray, двух фирменных, но не так хорошо известных коллекциях Android. Обе коллекции по сути аналоги HashMap из Java с тем исключением, что они созданы специально для минимизации потребления оперативной памяти.

В отличие HashMap, который для хранения каждого объекта создает новый объект и сохраняет его в массиве, ArrayMap не создает дополнительных объектов, но использует два массива: mHashes для последовательного хранения хешей ключей, и mArray - для хранения ключей и их значений (друг за другом). Начальный размер первого - 4, второго - 8.

При добавлении элемента ArrayMap сначала добавляет его хэш в первый массив, а затем добавляет ключ и значение во второй массив, где индекс ключа высчитывается как индекс хэша в массиве mHashes, умноженный на два, а индекс значения как индекс ключа плюс 1. В случае коллизии (когда два разных ключа имеют одинаковый хэш) ArrayMap производит линейный поиск ключа в mArray и, если он не найден, добавляет новый хэш в mHashes и новые ключ:значение в mArray. При достижении предельного размера массивов ArrayMap копирует их в новый массив, размер которого высчитывается так: oldSize+(oldSize>>1) (4 -> 8 -> 12 -> 18 -> 27 -> ...).

SparseArray представляет собой тот же ArrayMap, но предназначенный для работы с типами данных, где ключ - это int, а значение может быть либо объектом, либо простым типом данных: int, long, boolean (SparseIntArray, SparseLongArray, SparseBooleanArray). В итоге SparseArray нет необходимости хранить обертки над простыми типами данных.

Благодаря избавлению от необходимости хранить дополнительный объект для каждого элемента, ArrayMap оказывается примерно на 25% экономнее HashMap, а SparseArray почти в два раза экономнее. В то же время ArrayMap и SparseArray в целом в два раза медленнее HashMap.

Выводы:

- По возможности используй ArrayMap;
- Используй SparseArray если ключи имеют тип int;
- Если размер коллекции известен - указывай его в конструкторе.
Читать полностью
Decrease memory usage of your Android app in half - очередная статья о способах сократить использование оперативной памяти приложением.

1. Устрани утечки памяти. Это можно сделать используя инструмент LeakCanary, который будет показывать уведомление каждый раз, когда есть подозрение на утекшую активность, диалог или фрагмент.
2. Проанализируй использование памяти графическими элементами. Обычные Bitmap'ы, используемые в приложении, могут привести к ошибке OutOfMemoryError, когда приложение завершается из-за нехватки памяти. Чтобы этого избежать масштабируй изображения до меньшего размера, применяй кеширование и своевременно удаляй закешированную графику. Вот несколько советов как сделать это с помощью популярной библиотеки загрузки изображений Glide:

- По умолчанию библиотека использует формат ARGB_8888 для хранения изображений. Изменив его на RGB_565 можно вдвое сократить использование памяти не сильно потеряв в качестве (можно использовать только на low-end устройствах):

@GlideModule
class CustomGlideModuleV4 : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDefaultRequestOptions(
RequestOptions().format(DecodeFormat.PREFER_RGB_565)
)
}

}

GlideApp.with(view.context)
.load("$imgUrl$IMAGE_URL_SIZE_SPEC")
.into(view)

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

override fun onTrimMemory(level: Int) {
GlideApp.with(applicationContext).onTrimMemory(TRIM_MEMORY_MODERATE)
super.onTrimMemory(level)
}

- Чтобы уменьшить размер изображения можно использовать такой код:

Glide
.with(context)
.load(url)
.apply(new RequestOptions().override(600, 200))
.into(imageView);

3. Проверь код навигации приложения. При использовании паттерна "Одна активность", когда в приложении есть всего одна активность (Activity), а все экраны представляют собой фрагменты (Fragment), ты можешь заметить, что при перемещении на новый фрагмент старый фрагмент не уничтожается. Это стандартное поведение фрагментов, поэтому заботится об освобождении памяти должен ты сам. Для этого достаточно самостоятельно уничтожать все View в методе onDestroyView().

Другие советы:

- При использовании RecyclerView по возможности используй notifyItemChanged() вместо notifyDataSetChanged();
- Не создавай дополнительных объектов-оберток там, где этого можно избежать;
- Уменьши размер APK, это приведет к уменьшению памяти, занимаемого приложением;
- Не храни объекты "на всякий" случай;
- Запускай бенчмарки на релизных билдах;
- Избавься от избыточных анимаций.
Читать полностью
Mastering API Visibility in Kotlin - статья о том как сделать интерфейсы библиотек как можно более закрытыми сохранив гибкость, возможности тестирования и возможность взаимодействия с кодом на Java.

1. Internal - твой друг. Этот модификатор видимости чем-то похож на package private в Java, но покрывает не пакет, а целый модуль. Все классы, поля и методы, помеченные этим ключевым словом, будут видны только внутри текущего модуля.

2. Модификатор internal можно использовать совместно с аннотацией @VisibleForTesting чтобы тесты могли достучаться до нужных методов и полей:

@VisibleForTesting(otherwise = PRIVATE)
internal var state: State

3. В Java нет модификатора internal, поэтому в байткоде все, что помечено этим ключевым словом, станет public, но с одним важным отличием: к его имени прибавится название модуля. Например, метод createEntity со стороны Java будет выглядеть как createEntity$имяМодуля. Этого можно избежать с помощью аннотации @JvmName, позволяющей указать другое имя для использования из Java:

class Repository {
@JvmName("pleaseDoNotCallThisMethod")
internal fun createEntity() { ... }
}

Если же метод не должен быть виден вообще, можно использовать аннотацию @JvmSynthetic:

class Repository {
@JvmSynthetic
internal fun createEntity() { ... }
}

4. Explicit API mode - твой второй друг. В Kotlin все объявления по умолчанию получают модификатор public. А это значит, что шанс забыть сделать метод internal или private высок. Специально для борьбы с этой проблемой в Kotlin 1.4 появился Explicit API mode, который заставляет добавлять модификатор видимости к любым объявлениям. Чтобы его включить достаточно добавить три строки в конфиг Gradle:

kotlin {
explicitApi()
}

5. Одно из неожиданных следствий использования internal - инлайновые функции не смогут использовать методы, помеченные этим ключевым словом. Так происходит потому, что код инлайновой функции полностью встраивается в вызывающий код, а он не имеет доступа к методам, помеченным как internal. Решить эту проблему можно с помощью аннотации @PublishedApi. Она сделает метод доступным для инлайновых функций, но оставит закрытым для всех остальных:

@PublishedApi
internal fun secretFunction() {
println("through the mountains")
}

public inline fun song() {
secretFunction()
}

fun clientCode() {
song() // ok
secretFunction() // нет доступа
}
Читать полностью
Server-Side Development with Kotlin: Frameworks and Libraries - статья разработчиков из JetBrains о серверной разработке на языке Kotlin и фреймворках, которые могут в этом помочь.

1. _Spring Framework_. Один из самых популярных фреймворков для бекенд-разработки. Spring изначально можно было использовать совместно с Kotlin, но начиная с пятой версии в фреймворке появился ряд расширений для более удобной разработки на этом языке. Даже примеры в документации Spring теперь приведены на двух языках. Генератор проектов start.spring.io теперь также поддерживает Kotlin.
2. _Ktor_. Фреймворк для создания асинхронных клиентских и серверных веб-приложений, разработанный в JetBrains. Базируется на корутинах и обладает высокой масштабируемостью.
3. _Exposed_. Реализация SQL ORM для Kotlin.
4. _Spek, Kotes, MockK, Kotlin Power Assert_. Библиотеки тестирования и мокинга специально для Kotlin.
5. _Klaxon, kotlinx.serialization_. Библиотеки для удобной работы с JSON.
6. _RSocket_. Протокол для создания микросервисов с поддержкой Kotlin.
7. _GraphQL-koltin_. Библиотека для удобной работы с GraphQL из Kotlin.

Многие другие фреймворки, включая Micronaut, Quarkus, Javalin, SparkJava, Vaadin, CUBA и Vert.x, также поддерживают Kotlin и имеют документацию и примеры кода на Kotlin. Большой список Kotlin-библиотек можно найти здесь.
Читать полностью
How to Make the Compiler Smarter - статья о том, как сделать компилятор Kotlin умнее используя контракты.

Компилятор Kotlin (и анализатор кода в среде разработки) отличается мощной системой выведения типов. Например, в следующем фрагменте кода компилятор способен самостоятельно понять, что переменная s не равна null при использовании в качестве аргумента функции print:

fun printLengthOfString() {
val s: String? = "Hey Medium!"
if (s != null) {
print("The length of '$s' is ${s.length}")
}
}

Но стоит немного изменить код и компилятор становится бессилен:

fun printLengthOfString() {
val s: String? = "Hey Medium!"
if (s.isNotNull()) {
print("The length of '$s' is ${s.length}")
}
}

fun String?.isNotNull(): Boolean {
return this != null
}

К счастью, начиная с Kotlin 1.3 в нашем распоряжении есть механизм, позволяющий подсказать компилятору как быть с теми или иными типами в разных ситуация. Мы можем как бы заключить с компилятором контракт, что если приведенная выше функция isNotNull возвращает true, то это значит, что строка не равна null. Компилятор будет это учитывать при выведении типов.

Контракты пишутся в самом начале функции примерно в таком виде:

fun String?.isNotNull(): Boolean {
contract {
returns(true) implies(this@isNotNull != null)
}
return this != null
}

В данном случае контракт как раз сообщает, что если функция возвращает true (resturns(true)), то строка не равна null (implies (this@isNotNull != null)). Компилятор будет это учитывать и приведенный выше пример уже не будет ошибочным.

Вторая часть контракта (функция implies) называется эффектом. Есть также другой эффект - CallsInPlace. Он нужен для того, чтобы сообщить компилятору, что указанная в аргументе лямбда была выполнена один или более раз.

Работает это так. Допустим у нас есть такой код:

var a: Int
{
a = 42
}
print(a)

Этот код не будет скомпилирован, так как компилятор не уверен, что код внутри лямбды (a = 42) был выполнен хотя бы раз. Теперь добавим в код контракт:

fun main() {
var a: Int
initialize {
a = 42
}
print(a)
}

fun initialize(myLambda: () -> Unit) {
contract {
callsInPlace(myLambda, InvocationKind.AT_LEAST_ONCE)
}
myLambda()
}

Теперь код успешно компилируется, потому что мы зверели компилятор, что лямбда initialize будет выполнена хотя бы один раз.
Читать полностью
Учитывая все сказанное выше, можно утверждать, что Flow стал полноценной, более производительной и приближенной к реальным задачам заменой каналов. Однако есть ситуация, в которой каналы могут имеют преимущества - когда сообщение должно быть обработано ровно один раз (не больше и не меньше).

class SingleShotEventBus {
private val _events = Channel()
val events = _events.receiveAsFlow()

suspend fun postEvent(event: Event) {
// корутина будет приостановлена при переполнении
_events.send(event)
}
}

Этот пример схож с приведенным ранее BroadcastEventBus. Он также отдает сообщения во внешний мир с помощью Flow. Но есть важное отличие: в первом случае при отсутствии подписчиков сообщение будет выброшено, во втором - производитель будет приостановлен до тех пор, пока не появится потребитель.
Читать полностью
Shared flows, broadcast channels - статья Романа Елизарова (текущего главы разработки Kotlin) о развитии средств коммуникации между корутинами и текущем положении дел в этой области.

Статья начинается с рассказа о каналах (Channel), средстве коммуникации потоков исполнения (в данном случае корутин), позаимствованном из модели параллельного программирования CSP и языка Go. Каналы позволяют удобно и легко обмениваться данными и синхронизировать состояние нескольких корутин без опасности столкнуться с проблемой одновременного изменения состояния объекта.

В дополнение к классическим каналам в библиотеке kotlinx-coroutines также появился так называемой BroadcastChannel. Это специальный тип канала, позволяющий получать отправленные в канал сообщения сразу нескольким корутинам (подписчикам). По сути это была реализация паттерна event bus.

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

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

Эту проблему решил новый механизм передачи данных - Flow. Он позволяет создать так называемый холодный поток данных, который будет порожден только при подключении потребителя и создаст проблемы с производительностью только в том случае, если источник и потребитель будут работать в разных потоках. Основная идея Flow в том, чтобы позволить программисту _описать_ алгоритм генерации потока данных и запускать этот поток только тогда, когда в нем возникнет необходимость. К примеру, следующий код не создает никаких данных до тех пор пока на объекте coldFlow не будет вызван метод collect:

val coldFlow = flow {
while (isActive) {
emit(nextEvent)
}
}

При этом каждый потребитель получит собственную копию данных. И в этом же его проблема. Flow превосходно справляется с задачей организации конвейеров обработки данных, но его нельзя использовать в ситуациях, когда поток данных должен существовать сам по себе: например, для организации обработки системных событий в том самом паттерне event bus.

Решить эту задачу призван SharedFlow. Это своего рода производительный аналог BroadcastChannel, он существует независимо от наличия потребителей (которые теперь называются подписчиками) и отдает всем подписчикам один и те же данные (в противовес классическому Flow, который создает индивидуальный поток данных для каждого потребителя).

Создать event bus с помощью SharedFlow крайне просто:

class BroadcastEventBus {
private val _events = MutableSharedFlow()
val events = _events.asSharedFlow()

suspend fun postEvent(event: Event) {
// корутина будет приостановлена до получения сообщения подписчиками
_events.emit(event)
}
}

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

Если же задача требует чтобы производитель не приостанавливался ни при каких обстоятельствах, а потребители получали только последнее сообщение, то для этого есть StateFlow. Это основанная на Flow реализация паттерна Состояние:

class StateModel {
private val _state = MutableStateFlow(initial)
val state = _state.asStateFlow()

fun update(newValue: Value) {
// производитель не приостаналивается
_state.value = newValue
}
}

Как говорит Роман, val x: StateFlow можно представить как асинхронную версию var x: T с функцией подписки на обновления.
Читать полностью
Decrypting images hidden with Calculator+ - разбор способа реверса и последующей расшифровки файлов приложения Calculator+ - Photo Vault & Video Vault hide photos. Пример крайне простой и поэтому хорошо подходит для демонстрации основ реверса приложений для Android.

Итак, есть приложение Calculator+, которое имеет неожиданную функцию секретного хранилища зашифрованных фотографий и видеороликов. Задача: вытащить эти фотографии не используя само приложение.

1. Извлекаем приложение из смартфона:

$ adb shell pm list packages|grep calc
$ adb shell pm path eztools.calculator.photo.vault
$ adb pull /data/app/eztools.calculator.photo.vault-OP_MBoGMZN-LZ5wr50dNWA==/base.apk

2. Используем JADX-GUI для декомпиляции приложения обратно в исходный код Java.

3. С помощью встроенной функции поиска ищем все строки, имеющие отношение к шифрованию: "encrypt", "decrypt", "AES" и так далее. Автору удалось найти следующий фрагмент кода:

FileInputStream fileInputStream = new FileInputStream(this.f6363e.f6361a);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
EncryptUtils.m10791a("12345678", fileInputStream, byteArrayOutputStream);
byteArrayOutputStream.flush();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
this.f6362d = byteArrayInputStream;
aVar.mo2601d(byteArrayInputStream);

Метод EncryptUtils.m10791a() оказался искомым методом шифрования.

Метод шифрует файл алгоритмом DES и ключом, переданным ему в качестве аргумента. А в аргументе всегда передается строка "12345678".

4. Расшифровать файлы не составит труда, но сначала необходимо найти, где они хранятся. Для этого надо проследить за методами, вызывающими метод шифрования. В конечном итоге нашелся такой код:

public static final File m18904s(Context context) {
C3655i.m20371c(context, "context");
File externalFilesDir = context.getExternalFilesDir("photo_encrypt");
if (externalFilesDir != null) {
return externalFilesDir;
}
C3655i.m20376h();
throw null;
}

Это код создания объекта класса File во внешнем приватном каталоге приложения (context.getExternalFilesDir): /sdcard/Android/data/eztools.calculator.photo.vault/files.

5. Теперь файлы можно извлечь и расшифровать. Автор написал для этого скрипт на Python, который я не буду здесь приводить.
Читать полностью
IntelliJ IDEA / Android Studio Tricks: Surround With - статья об очень полезной, но далеко не всем известной функции IDEA под названием Surround With.

В IDEA (и, как следствие, Android Studio) есть функция Live Templates (живые шаблоны), которая представляет собой нечто вроде сокращений для длинных строк кода и конструкций. Типичный пример - шаблон ifn - if null. Ты просто пишешь ifn, затем нажимаешь и среда разработки сама вставляет в код конструкцию if (x == null) { }, автоматически выделяет x и ставит на него курсор. Функция настолько умная, что даже попытается предсказать имя переменной x на основании того, какие пременные текущей области видимости могут иметь значение null.

У шаблонов есть родственная функция под названием Surround With. Она работает примерно также, но по отношению к уже имеющимуся выражению. Например, если навести курсор на любую строку ввода и нажать среда разработки покажет на экране меню с выбором шаблонов. Среди них есть шаблон, который обрамит выражение в блок try/catch, в if/else и так далее.

И конечно же IDEA позволяет создавать собственные шаблоны. Открой настройки, далее Editor -> Live Templates, далее кнопка "+". Полезные примеры шаблонов:

by lazy { $SELECTION$ }

CoroutineScope(Dispatchers.Main).launch {
$SELECTION$
}

withContext(Dispatchers.IO) {
$SELECTION$
}
Читать полностью
HOWTO: Running Java code directly on Android (without creating an APK) - небольшая заметка о том как запустить исполняемый код на Android без создания пакета APK.

Допустим у нас есть такой код на Java (не обязательно вдаваться в подробности его реализации):

package com.example;

import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

public class HelloWorld {
public static void main(String[] args) throws ParseException {
Option version = new Option("v", "Print version");
Option help = new Option("h", "Print help");
Options options = new Options();
options.addOption(help);
options.addOption(version);
for (Option opt : new DefaultParser().parse(options, args).getOptions()) {
if (opt.equals(version)) {
String os = System.getProperty("os.arch");
System.out.println("Hello World (" + os + ") v0.1");
}
if (opt.equals(help)) {
new HelpFormatter().printHelp("Hello World", options);
}
}
}
}

Мы хотим запустить его прямо на устройстве не создавая файл APK. Для этого его необходимо скомпилировать:

$ javac -source 1.7 -target 1.7 -d bin -cp lib/commons-cli-1.3.1.jar src/com/example/HelloWorld.java

В данном случае bin - это каталог, в котором компилятор разместит полученный байткод Java, lib/commons-cli-1.3.1.jar - библиотека-зависимость кода, а src/com/example/HelloWorld.java - путь к исходному файлу.

Теперь полученный байткод Java необходимо преобразовать в байткод DEX, пригодный для исполнения на Android-смартфоне:

$ ./android-sdk-linux/build-tools/23.0.2/dx --output=helloworld.jar --dex ./bin lib/commons-cli-1.3.1.jar

В твоем случае путь к команде dx может быть другим (обычно каталог android-sdk-linux располагается в каталоге Android Studio или рядом с ним).

Далее необходимо создать shell-скрипт, который будет запускать наш код:

base=/data/local/tmp/helloworld
export CLASSPATH=$base/helloworld.jar
export ANDROID_DATA=$base
mkdir -p $base/dalvik-cache
exec app_process $base com.example.HelloWorld "$@"

Здесь app_process - это команда Android, которая запускает новую виртуальную машину. Точно таким же образом происходит запуск стандартных приложений для Android.

Теперь скрипт вместе с байткодом можно перекинуть на устройство:

$ adb shell mkdir -p /data/local/tmp/helloworld
$ adb push helloworld.jar /data/local/tmp/helloworld
$ adb push helloworld.sh /data/local/tmp/helloworld
$ adb shell chmod 777 /data/local/tmp/helloworld/helloworld.sh

И запустить:

$ adb shell /data/local/tmp/helloworld/helloworld.sh -v
Читать полностью
Announcing a painless Kotlin/Multiplatform NoSQL embedded database - статья-анонс новой NoSQL базы данных для мультиплатформенных проектов на Kotlin.

Kodein-DB специально создана для не-серверных проектов, в которых важно удобство использования и гибкость, а не возможность создания сложных схем хранения данных. Это крайне быстрая база данных, в некоторых операциях в десятки раз опережающая SQLite по скорости. Она очень проста в использовании и не требует никаких подготовительных шагов по созданию схем. Базу данных можно открыть и сразу использовать для сохранения объектов:

// Все модели должны быть сериализуемы и реализовать интерфейс Metadata
@Serializable
data class User(
// Каждый экземпляр должен использовать уникальный id
override val id: String,
val firstName: String,
val lastName: String
) : Metadata

fun store(db: DB, user: User) { db.put(user) }

fun load(db: DB, id: String): User = db.get(db.newKey(id)).model

fun test(db: DB) {
val id = UUID.randomUUID()
val user = User(id, "John", "Doe")
store(db, user)
val otherUser = load(db, id)
assertEquals(user, otherUser)
}

Kodein-DB кеширует все данных в оперативной памяти и умеет сообщать о произошедших операциях, поэтому ее можно использовать как единый источник истины (single source of truth), не дублируя состояние БД в оперативной памяти самостоятельно:

fun listenForChange(db: DB) {
db.on().register {
didPut {
addMessageToUI(it)
}

didDelete {
reloadList(
db.find()
.byIndex("time")
.useModels(reverse = true) { it.toList() }
)
}
}
}

В настоящий момент проект находится в состоянии беты, но уже вполне пригоден для повседневного использования.
Читать полностью
Adapt your app for the latest privacy best practices - статья о том, как написать правильное, с точки зрения приватности пользователя, приложение.

1. _Разрешение на "общение" с другими приложениями._ Для приложений, собранных для Android 11 (targetSdkVersion 30) действуют ограничения на просмотр информации и коммуникацию с другими приложениями. Теперь приложение должно прямо указывать в манифесте с какими приложениями оно может взаимодействовать и какие интенты может использовать. Официальная документация рассматривает множество различных примеров, покрывающих почти все возможные случаи. Плюс ко всему, если твое приложение использует content provider чтобы расшаривать информацию другим приложениям, ты должен указывать флаг Intent.FLAG_GRANT_READ_URI_PERMISSION в любом интенте, расшаривающем данные провайдера:

val shareIntent = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
data = // Uri, расшариваемый с другим приложением
}

2. _Постепенный запрос разрешений._ Исследования показывают, что пользователи склонны предоставлять приложениям определенные полномочия когда им требуется определенная функциональность приложения, которая не будет работать без этого разрешения. Только 32% пользователей предоставляют разрешение потому, что доверяют разработчику приложения. Второй момент, который необходимо помнить: если приложение собрано для Android 11 оно больше не может запрашивать разрешение на определение местоположения в фоне (background location) сразу, сначала оно должно запросить разрешение foreground location.

3. _Корректный доступ к камере и микрофону._ Android разрешает использовать доступ к камере и микрофону только если приложение видимо на экране. Это же относится и к сервисам: foreground service имеет иконку в панели состояния и поэтому он видим и может использовать камеру и микрофон. Android 11 вводит еще одно требование: сервис должен прямо указать какие типы сенсоров он будет использовать:

android:foregroundServiceType = "microphone | location | camera"

4. _Идентификаторы устройства._ Android 10 запрещает чтение многих хардварных идентификаторов устройства, включая серийный номер SIM-карты, IMEI и многие другие. Android 11 также закрыла возможность чтения ICC ID. В качестве альтернативы можно использовать метод getSubscriptionId(), который возвращает идентификатор, уникальный для каждой SIM-карты.
Читать полностью
Sophisticated new Android malware marks the latest evolution of mobile ransomware - статья исследователей из Microsoft о новом типе Ransomware, найденном на просторах интернета.

Малварь называется AndroidOS/MalLocker.B и в целом уже известна и достаточно хорошо изучена. Интерес исследователей вызывала новая разновидность этого вымагателя, которая научилась блокировать устройство пользователя показывая сообщение о выкупе без использования экранных оверлеев (SYSTEM_ALERT_WINDOW), возможности которых Google серьезно ограничила в последних версиях Android.

Вместо оверлея зловред использует так называемое полноэкранное уведомление, используемое легитимным софтом для показа экрана звонка. Кроме текста (и других стандартных атрибутов) такое уведомление также содержит ссылку на активность (экран приложения), который и будет показан когда уведомление появится в системе.

Однако одиночный показ сообщения о выкупе был бесполезен, так как пользователь смог бы нажать кнопку "домой" или "назад" и просто закрыть его. Поэтому зловред использует еще один прием: перезапуск активности в методе onUserLeaveHint().

Метод onUserLeaveHint() - это коллбек, который система вызывает перед тем, как активность исчезнет с экрана. Поэтому перезапуск активности в этом методе приводит к тому, что пользователь просто не может покинуть экран с сообщением о выкупе.

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

Еще одна интересная особенность зловреда - способ шифрования кода, а точнее способ скрытия методов шифрования кода. В основном DEX-файле шифровальщика есть класс, содержащий методы для шифрования и расшифровки остальных частей приложения. На вход эти методы получают строку (которая на первый взгляд кажется ключом шифрования), а на выходе почему-то возвращают объект класса Intent (такие объекты в Android используются для отправки сообщений другим приложениям).

На самом деле входная строка - это просто мусор, а сам интент содержит поле action, в котором как раз и содержится адрес расшифрованных данных.
Читать полностью
10 Useful Kotlin String Extensions - статья с десятью примерами функций-расширений для работы со строками. Из того, что можно вынести в свою мини-библиотеку:

val String.containsLatinLetter: Boolean
get() = matches(Regex(".*[A-Za-z].*"))

val String.containsDigit: Boolean
get() = matches(Regex(".*[0-9].*"))

val String.isAlphanumeric: Boolean
get() = matches(Regex("[A-Za-z0-9]*"))

val String.hasLettersAndDigits: Boolean
get() = containsLatinLetter && containsDigit

val String.isIntegerNumber: Boolean
get() = toIntOrNull() != null

val String.toDecimalNumber: Boolean
get() = toDoubleOrNull() != null

Использовать так:

val cl = "Contains letters".containsLatinLetter // true
val cnl = "12345".containsLatinLetter // false
val cd = "Contains digits 123".containsDigit // true
val istr = "123".isIntegerNumber // true
val dstr = "12.9".toDecimalNumber // true

Заслуживают внимания также функции для валидации email-адресов и телефонных номеров:

fun String.isEmailValid(): Boolean {
val expression = "^[\\w.-]+@([\\w\\-]+\\.)+[A-Z]{2,8}$"
val pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(this)
return matcher.matches()
}

fun String.formatPhoneNumber(context: Context, region: String): String? {
val phoneNumberKit = PhoneNumberUtil.createInstance(context)
val number = phoneNumberKit.parse(this, region)
if (!phoneNumberKit.isValidNumber(number))
return null

return phoneNumberKit.format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL)
}
Читать полностью
Lockscreen and authentication improvements in Android 11 - статья разработчиков из команды безопасности Andorid об изменении в работе механизмов аутентификации по отпечаткам пальцев и лица.

До Android 11 система аутентификации Android работала по следующим правилам:

1. Пароль или PIN-код - считается наиболее надежным методом аутентификации и поэтому дает полный контроль над устройством без всяких ограничений;
2. Отпечаток пальца или снимок лица - менее надежный, система запрашивает пароль после каждой перезагрузки телефона, а также через каждые 72 часа;
3. Smart Lock - наименее надежный метод, поэтому на него накладываются те же ограничения, что и на биометрический метод, плюс он не позволяет получить доступ к аутентификационным ключам Keymaster (например, тем, что используются для платежей), а принудительный запрос пароля происходит не через 72 часа, а уже через 4.

В Android 11 появилось понятие надежности способа биометрической аутентификации. Теперь система учитывает насколько надежный датчик отпечатков пальцев или сканер лица установлен в устройство и может изменить поведение системы. Например, ненадежный способ аутентификации нельзя будет использовать для аутентификации в сторонних приложениях и для разблокировки доступа к KeyStore. Также для такого способа аутентификации таймаут перед следующим запросом пароля будет снижен с 72-х до 24 часов.

Всего есть три класса надежности датчиков (способов) биометрической аутентификации:

- Класс 3 - Надежный, запрос пароля через 72 часа, доступ к KeyStore и возможность использования в сторонних приложениях;
- Класс 2 - Слабый, запрос пароля через 24 часа, доступ к KeyStore, невозможно использовать в сторонних приложениях;
- Класс 1 - Удобный, запрос пароля через 24 часа, нет доступа к KeyStore, невозможно использовать в сторонних приложениях.

Их надежность определяется на основе процента ложных срабатываний, безопасности способа обработки биометрических данных и некоторых других параметров.
Читать полностью
Don’t Reinvent the Wheel, Delegate It! - статья об использовании встроенных в язык Kotlin средств делегирования реализаций классов и свойств.

Один из ключевых постулатов современного ООП-программирования гласит: "Предпочитайте делегирование наследованию". Это значит, что вместо наследования от какого-либо класса лучше включить инстанс этого класса в другой класс и вызывать его методы при вызове одноименных методов этого класса:

class Class1() {
fun method1() { ... }
fun method1() { ... }
}

class Class2(firstClass: Class1) {
private val class1 = firstClass

fun method1() { firstClass.method1() }
fun method2() { firstClass.method2() }
}

Зачем это нужно? Для того, чтобы избежать проблем когда методы класса-родителя вызывают друг друга. Если method1() вызывает method2(), то переопределив второй метод мы сломаем работу первого. Делегирование решает эту проблему так как Class1 и Class2 остаются не связанными друг с другом.

С другой стороны делегирование усложняет код и поэтому в Kotlin есть специальное ключевое слово by, сильно упрощающее жизнь разработчику. Благодаря ему реализовать второй класс можно с помощью всего одной строки:

class Class2(firstClass: Class1) : Class1 by firstClass

Это действительно все. Компилятор Kotlin автоматически преобразует эту строку в аналог реализации Class2 из первого примера.

Kotlin также позволяет делегировать реализацию свойств. В следующей записи используется стандартный делегат lazy, инициализирующий переменную при первом обращении к ней:

val orm by lazy { KotlinORM("main.db") }

Еще более интересно работает делегат map, позволяющий магическим образом взять значения из хэшмепа.

class User(val map: Map) {
val name: String by map
val age: Int by map
}

val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))

println(user.name) // "John Doe"
println(user.age) // 25

Кроме lazy и map стандартная библиотека Kotlin включает в себя еще три стандартных делегата:

* nutNull - аналог ключевого слова lateinit с более широкими возможностями;
* observable - позволяет выполнить код в момент чтения или записи переменной;
* vetoable - похож на observable, но срабатывает перед записью нового значение и может запретить изменение.

Ну и конечно же любой разработчик может создать собственный делегат. Это всего-лишь класс c реализацией операторов getValue() и setValue():

class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
Читать полностью
Building resilient Android applications - статья разработчиков из Microsoft о том, как использовать библиотеку resilience4j для создания устойчивых к обрыву сетевого соединения приложений.

Resilience4j позволяет описать алгоритм восстановления соединения в функциональном стиле на языке Java или Kotlin. Например:

private val connectRetry = Retry.of("connect", RetryConfig {
maxAttempts(3)
waitDuration(Duration.ofSeconds(3))
retryOnResult { it == ConnectionResult.UNREACHABLE }
})

private val sendMessageRetry = Retry.of("sendMessage", RetryConfig {
maxAttempts(3)
intervalFunction { retryNumber -> 2.toDouble().pow(retryNumber).roundToLong() }
retryOnException { it is FailedToSendException }
})

Далее этот алгоритм можно использовать для подключения:

suspend fun connectAndSendMessage(message: ByteArray) {
val result = if (!connection.isConnected) {
connectRetry.executeSuspendFunction {
connection.connectToDevice()
}
} else ConnectionResult.CONNECTED

if (result == ConnectionResult.CONNECTED) {
sendMessageRetry.executeSuspendFunction {
connection.sendMessage(message)
}
} else throw FailedToSendException()
}
Читать полностью
Список модулей APEX (системных компонентов Android, обновляемых через Google Play) в Android 10/11
Prefer Storing Data with Jetpack DataStore - статья разработчиков Android Jetpack о новой библиотеке для замены SharedPreferences.

DataStore состоит из двух компонентов: Proto DataStore для хранения объектов (на базе ProtocolBuffers), и Preferences DataStore для хранения пар ключ:значение. В отличие от SharedPreferences, новая библиотека транзакционная (то есть может гарантировать целостность данных) и имеет асинхронный API, функции которого можно вызывать из основного потока приложения. Поддерживается и миграция с SharedPreferences.

Работать с API достаточно просто. Для начала необходимо добавить в build.gradle нужные зависимости:

// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
// Proto DataStore
implementation "androidx.datastore:datastore-core:1.0.0-alpha01"

Затем создать объект DataStore (далее примеры для Preferences DataStore):

val dataStore: DataStore = context.createDataStore(name = "settings")

Далее можно читать данные:

val MY_COUNTER = preferencesKey("my_counter")
val myCounterFlow: Flow = dataStore.data
.map { currentPreferences ->
currentPreferences[MY_COUNTER] ?: 0
}

И писать:

suspend fun incrementCounter() {
dataStore.edit { settings ->
val currentCounterValue = settings[MY_COUNTER] ?: 0
settings[MY_COUNTER] = currentCounterValue + 1
}
}
Читать полностью