Всем привет! Эта статья получилась совершенно случайно. Когда мы готовили Deep Dive Into Deep Link (habr, mobius в 2022 году), пришли к выводу:
Не существуют удобного и гибкого инструмента для фильтрации сложных path на уровне ОС.
Меня никак не успокаивало то, что в Android нет ничего лучше, чем pathPattern с двумя бедными .
и *
. Я уже начал оправдывать Google. Придумал причину почему сделано именно так:
Ограничения в регулярных выражениях для
pathPattern
сделаны для того, чтобы не замедлять intent resolution.
Даже после этого, казалось бы логичного объяснения, мне все равно не верилось, что такая очевидная функция недоступна и я решил в последний раз погуглить. Вот, что нашлось:
developer.android.com: Create Deep Links to App Content. Нет сложных сценариев.
Medium: How to manage a complex DeepLinks scheme on your Android App. Говорит, что нет полноценной поддержки регулярок.
Stack Overflow: How to use PathPattern in order to create DeepLink Apps Android? Тоже ничего примечательного.
И натыкаюсь на это… Stack Overflow: How to use PATTERN_ADVANCED_GLOB in android manifest intent-filter? В этом треде указывают на то, что у PatternMatcher есть атрибут PATTERN_ADVANCED_GLOB
. Автор спрашивает, может ли он им воспользоваться в <data/>
? Ему отвечают, что так нельзя. PATTERN_ADVANCED_GLOB
был добавлен в API 26 и Google ничего не анонсировал никакие связанные с ними фичи. Дальше самое смешное. Автор отвечает:
I see - but well they could create a android:pathPatternAdvanced and leave the rest untouched, surely that wouldn't break anything. But this is getting off-topic. Thank you!
С тех пор прошло 3 года))) Я уже было отчаялся и решил в последний раз взглянуть в документацию… И знаешь что? Там появился атрибут pathAdvancedPattern
! Кажется, Дед Мороз подбросил его туда перед самым Новым годом! ????????Клянусь, я не видел этот атрибут когда готовил Deep Dive Into Deep Link. Его там не было! Более того, в сети еще нет ни одной статьи о pathAdvancedPattern! Дарю ее тебе ???? С наступающим!
Возможности pathAdvancedPattern
В отличие от pathPattern, у которого доступны специальные символы: .
, *
, pathAdvancedPattern может обрабатывать: .
, *
, [...]
, ^
, +
, {...}
. Давайте разберем каждый паттерн на примерах.
Как это будет выглядеть в коде
<activity
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="scheme"
android:host="host"
android:pathAdvancedPattern="Сюда будем вставлять примеры"/>
</intent-filter>
</activity>
Все примеры ниже я буду запускать на эмуляторе Pixel 2, arm64, API 31 при помощи команды adb shell am start -d <reference>.Все примеры ниже я буду запускать на эмуляторе Pixel 2, arm64, API 31 при помощи команды adb shell am start -d <reference>.Все примеры ниже я буду запускать на эмуляторе Pixel 2, arm64, API 31 при помощи команды adb shell am start -d <reference>.Все примеры ниже я буду запускать на эмуляторе Pixel 2, arm64, API 31 при помощи команды adb shell am start -d <reference>.
Все примеры ниже я буду запускать на эмуляторе Pixel 2, arm64, API 31 при помощи команды adb shell am start -d <reference>
.
Как и pathPattern
, в pathAdvancedPattern
доступна точка .
для обозначение одного произвольного символа.
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/abc |
Да |
scheme://host/1.2 |
Да |
|
scheme://host/=_0 |
Да |
|
scheme://host/=_* |
Нет. |
|
scheme://host/=_> |
Нет. |
|
scheme://host/=_) |
Нет. |
К сожалению, даже несмотря на обещанные “any character”, их обработать не получится (примеры с *
, >
, )
). К счастью, такие символы встречаются не часто ????
Звездочка *
указывает, что символ, идущий сразу перед ней, может быть повторен 0 или сколько угодно раз.
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/abc |
Да |
scheme://host/1 |
Да |
|
scheme://host/__0__ |
Да |
|
scheme://host/some/0…(65469 нулей)...0 |
Нет. |
Да я уже вижу ваши голодные глаза, смотрящие на “сколько угодно раз” ???? Вопрос из категории “зачем я это знаю”: сколько символов сможет обработать *? Ответ 65471. Чтобы это узнать я запустил: adb shell am start -d scheme://host/some/0…(65469 нулей)...0
— и увидел в терминале error: closed
.
Плюс +
указывает, что символ, идущий сразу перед ним, может быть повторен 1 или сколько угодно (65471) раз.
Интересное замечание
Если в host добавить X символов, то количество нулей нужно будет сократить на X: scheme://host/some/0…(65469 нулей)...0, scheme://host/some123/0…(65466 нулей)...0. Подозреваю, что ограничение на длину распространяется не на сам path, а на URL в целом (65490 символов).
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/1 |
Да |
scheme://host/asv |
Да |
|
scheme://host/_+++ |
Да |
|
Аналогично для |
Нет |
|
|
scheme://host/e |
Да |
scheme://host/ee |
Да |
|
scheme://host/eeeeee |
Да |
|
scheme://host/ |
Нет |
|
scheme://host/ea |
Нет |
|
scheme://host/aaa |
Нет |
|
|
scheme://host/ea |
Да |
scheme://host/eea |
Да |
|
scheme://host/eeeeea |
Да |
|
scheme://host/ec |
Нет |
|
scheme://host/eca |
Нет |
|
scheme://host/eeac |
Нет |
Квадратные скобки [...]
указывают на множество символов, по которым будет выполняться соответствие. Внутри квадратных скобках может быть использован дефис -
для обозначения промежутка между символом слева и справа. Дефис удобен для сокращения записи последовательности символов идущих друг за другом. Символ крышечки ^
используется для инвертирования множества.
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/a |
Да |
scheme://host/11111 |
Да |
|
scheme://host/aa_1__11 |
Да |
|
scheme://host/c |
Нет |
|
scheme://host/a1d_ |
Нет |
|
scheme://host/1____aca |
Нет |
|
|
scheme://host/valery |
Да |
scheme://host/Valery |
Да |
|
scheme://host/AbCdE |
Да |
|
scheme://host/valery1 |
Нет |
|
scheme://host/va_petrov |
Нет |
|
scheme://host/ |
Нет |
|
|
scheme://host/_ |
Да |
scheme://host/a |
Да |
|
scheme://host/P |
Да |
|
scheme://host/0 |
Нет |
|
scheme://host/1 |
Нет |
|
scheme://host/ab |
Нет |
|
|
scheme://host/+ |
Да |
scheme://host/+++ |
Да |
|
scheme://host/a |
Нет |
|
scheme://host/+- |
Нет |
Фигурные скобки {...}
используются для обозначения количества повторений шаблона. Внутри скобок может быть указано:
Одно число. Шаблон должен повториться ровно указанное число. Ни больше, ни меньше.
Два числа через запятую. Количество повторений больше или равно числу слева и меньше или равно числу справа.
Одно число и запятая. Как и в предыдущем случае, но без ограничений справа.
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/abccc |
Да |
scheme://host/abc |
Нет |
|
scheme://host/abcccc |
Нет |
|
scheme://host/abcabcabc |
Нет |
|
|
scheme://host/12 |
Да |
scheme://host/987 |
Да |
|
scheme://host/0246 |
Да |
|
scheme://host/1 |
Нет |
|
scheme://host/12345 |
Нет |
|
|
scheme://host/abc |
Да |
scheme://host/zxyulmn |
Да |
|
scheme://host/ab |
Нет |
|
scheme://host/p |
Нет |
|
|
Такой паттерн не скомпилируется |
Обратный слеш \
позволяет делать специальные символы обычными. Необходимо учитывать, что обратный слеш в XML используется в качестве escape character, поэтому при парсинге он “съедается”. Чтобы не потерять слеш необходимо писать \\
. А чтобы экранировать слеш — \\\\
. В этих примерах будь внимательнее ????
|
Пройдет фильтр |
Не пройдет фильтр |
|
scheme://host/ |
Да |
scheme://host//// |
Да |
|
scheme://host/////// |
Да |
|
scheme://host/+ |
Нет |
|
|
scheme://host/+ |
Да |
scheme://host/ |
Нет |
|
scheme://host/// |
Нет |
|
|
scheme://host/\ |
Да. При запуске из adb необходимо прописать \\\\ |
scheme://host/\\ |
Нет |
Гораздо больше возможностей, не правда ли? Теперь эти шаблоны можно использовать для обработки ссылок, о которых мы раньше только мечтали ✨:
|
Ссылка |
Пройдет фильтр? |
|
/user/0 |
Да |
/user/123 |
Да |
|
/user/9876 |
Да |
|
/user/ |
Нет |
|
/user/abc |
Нет |
|
/user/12a |
Нет |
|
|
scheme://host/name/valery007 |
Да |
scheme://host/login/1^v |
Нет |
|
|
scheme://host/phone/+79876543210 |
Да |
scheme://host/phone/+7987 |
Нет |
|
scheme://host/phone/79876543210 |
Нет |
|
scheme://host/phone/9876543210 |
Нет |
|
|
scheme://host/uuid/123e4567-e89b-12d3-a456-426614174000 |
Да |
scheme://host/uuid/invalid-uuid |
Нет |
Все примеры выше я тестировал на API 31. Заявлено, что pathAdvancedPattern
работает с API 26. И вот я настраиваю android:pathAdvancedPattern="/[0-9]{2}"
запускаю на Android 8.0 и…
Не все так гладко
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/12 |
Да |
scheme://host/1 |
Да |
|
scheme://host/123 |
Да |
|
scheme://host/abc123 |
Да |
|
Не нашел (кроме примера с максимальной длиной и специальными символами) |
Нет |
Он начинает обрабатывать всё! Все заявленные возможности как будто бы просто игнорируются! Я решил повторить тесты для Android 7.0, 9.0, 10.0, 11.0, 12.0, 13.0.
Напоминаю, тестовое устройство: эмулятор Pixel 2, arm64. Для Android 7.0 (API 24), 8.0 (API 26), 9.0 (API 28), 10.0 (API 29), 11.0 (API 30) значение pathAdvancedPattern
игнорируется. Приложение ведет себя так, как если бы вместо, например /[0-9]{2}
, было бы написано /.*
. Заявленные в pathAdvancedPattern
шаблоны работают только начиная с API 31: Android 12.0 (API 31), 13.0 (API 33).
Попытки разобраться
На самом деле, все что описано выше я узнал не сразу. Мне пришлось несколько дней разбираться с тем, почему pathAdvancedPattern
не работает на API 26. В один день я почти отчаялся и чуть не решил забросить статью, но запустил на API 31 и это спасло положение. В кратце опишу, что я узнал в те несколько дней.
Intent resolution состоит из множества этапов. Я решил начать с PatternMatcher
. Нашел коммит в котором появился PATTERN_ADVANCED_GLOB
. Создал тестовое приложение и написал там код:
val patternMatcher = PatternMatcher("/user/[0-9]{3}", PATTERN_ADVANCED_GLOB)
...
append("/user/1 - ${patternMatcher.match("/user/1")}\n")
append("/user/123 - ${patternMatcher.match("/user/123")}\n")
append("/user/1234 - ${patternMatcher.match("/user/1234")}\n")
append("/user/12a - ${patternMatcher.match("/user/12a")}\n")
Запустил на API 26 и увидел:
PatternMatcher:
/user/1 - false
/user/123 - true
/user/1234 - false
/user/12a - false
Отработал как надо. Идем дальше. Посмотрим на IntentFilter. Пишу код:
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_VIEW)
addCategory(Intent.CATEGORY_DEFAULT)
addDataScheme("myscheme")
addDataAuthority("myhost", null)
addDataPath("/user/[0-9]{3}", PATTERN_ADVANCED_GLOB)
}
...
val intent1 = Intent(Intent.ACTION_VIEW, Uri.parse("myscheme://myhost/user/1"))
val intent2 = Intent(Intent.ACTION_VIEW, Uri.parse("myscheme://myhost/user/123"))
val intent3 = Intent(Intent.ACTION_VIEW, Uri.parse("myscheme://myhost/user/1234"))
val intent4 = Intent(Intent.ACTION_VIEW, Uri.parse("myscheme://myhost/user/12a"))
...
append("myscheme://myhost/user/1 - ${intentFilter.match(contentResolver, intent1, true, "EXP-1!")}\n")
append("myscheme://myhost/user/123 - ${intentFilter.match(contentResolver, intent2, true, "EXP-2!")}\n")
append("myscheme://myhost/user/1234 - ${intentFilter.match(contentResolver, intent3, true, "EXP-3!")}\n")
append("myscheme://myhost/user/12a - ${intentFilter.match(contentResolver, intent4, true, "EXP-4!")}\n")
Запускаю на API 26 и вижу:
IntentFilter:
myscheme://myhost/user/1 - -2
myscheme://myhost/user/123 - 5275648
myscheme://myhost/user/1234 - -2
myscheme://myhost/user/12a - -2
Что за магические числа? intentFilter.match
возвращает число, которое означает по каким категориям прошло соответствие. Например, MATCH_CATEGORY_HOST
, MATCH_CATEGORY_SCHEME
, NO_MATCH_DATA
и другие или их сумму (подробнее здесь).
NO_MATCH_DATA = -2
. Это значит, что intent не прошел фильтр из разницы в data URI.MATCH_CATEGORY_PATH + MATCH_ADJUSTMENT_NORMAL = 5275648
. Это значит, что data URI intent соответствует фильтру. То, что и ожидалось от рабочегоPATTERN_ADVANCED_GLOB
.
Идем на уровень выше: PackageManager
. Будем вызывает метод queryIntentActivities для получения всех активностей, которые могут быть запущены по указанному intent. Дописываю код:
var queryIntentActivities1: List<ResolveInfo> = packageManager.queryIntentActivities(intent1, MATCH_DEFAULT_ONLY)
var queryIntentActivities2: List<ResolveInfo> = packageManager.queryIntentActivities(intent2, MATCH_DEFAULT_ONLY)
var queryIntentActivities3: List<ResolveInfo> = packageManager.queryIntentActivities(intent3, MATCH_DEFAULT_ONLY)
var queryIntentActivities4: List<ResolveInfo> = packageManager.queryIntentActivities(intent4, MATCH_DEFAULT_ONLY)
...
append("queryIntentActivities - myscheme://myhost/user/1 = \n$queryIntentActivities1\n")
append("queryIntentActivities - myscheme://myhost/user/123 = \n$queryIntentActivities2\n")
append("queryIntentActivities - myscheme://myhost/user/1234 = \n$queryIntentActivities3\n")
append("queryIntentActivities - myscheme://myhost/user/12a = \n$queryIntentActivities4\n")
Настраиваю intent-filter:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="myscheme"
android:host="myhost"
android:pathAdvancedPattern="/user/[0-9]{3}"/>
</intent-filter>
Запускаю:
queryIntentActivities - myscheme://myhost/user/1 =
[ResolveInfo{79cc25c ru.valeryvpetrov.dev.pathadvancedpattern/.MainActivity m=0x308000}]
queryIntentActivities - myscheme://myhost/user/123 =
[ResolveInfo{1ef965 ru.valeryvpetrov.dev.pathadvancedpattern/.MainActivity m=0x308000}]
queryIntentActivities - myscheme://myhost/user/1234 =
[ResolveInfo{64b263a ru.valeryvpetrov.dev.pathadvancedpattern/.MainActivity m=0x308000}]
queryIntentActivities - myscheme://myhost/user/12a =
[ResolveInfo{e8122eb ru.valeryvpetrov.dev.pathadvancedpattern/.MainActivity m=0x308000}]
У ResolverInfo
есть атрибут match. Он показывает то, как ОС оценила соответствие intent фильтру. В логе отображается шестнадцатеричное число. 0x30800
соответствует 3178496
в десятичной системе счисления. MATCH_ADJUSTMENT_NORMAL+MATCH_CATEGORY_HOST = 3178496
. Это значит, что ОС оценила совпадение только в категории host. В категории path совпадений нет.
Погружаться дальше я не стал. Если знаете подробности, то добро пожаловать в комментарии.
pathSuffix
Помните про атрибут pathPrefix
? В API 31 появился его аналог для матчинга суффиксов. Его можно использовать для URL, в которых известно окончание path:
|
Ссылка |
Пройдет фильтр? |
|
scheme://host/some_end |
Да |
scheme://host/end |
Да |
|
scheme://host/123end |
Да |
|
scheme://host/somebody |
Нет |
|
scheme://host/endd |
Нет |
При запуске на API ниже 31 поведение схожее с pathAdvancedPattern
: обрабатывается любой path.
Выводы
С API 31 (Android 12.0) у разработчиков появилась возможность в <data />
использовать два новых атрибута: pathAdvancedPattern
, pathSuffix
.
В отличие от pathPattern
у pathAdvancedPattern
появились новые шаблоны для описания regex-like выражений:
[...]
для обозначения множества символов. Внутри квадратных скобок можно использовать - для обозначения последовательности символов и ^ для инвертирования множества.+
для обозначения шаблона, который может повторяться 1 и более раз.{...}
для обозначения количества точного количества повторений или промежутка значений.
Надеюсь, что скоро появятся поддержка новых специальных символов регулярных выражений. Например:
Количество вхождений:
?
Специальные символы:
\d
,\D
,\w
,\W
Группировка символов:
(...)
,|
Новый атрибут pathSuffix
похож на pathPrefix
, но работает только для окончания path.
С Android 12.0 при помощи pathAdvancedPattern
и pathSuffix
можно обработать ссылки, которые раньше приходилось обрабатывать только на уровне приложения. pathAdvancedPattern
и pathSuffix
помогут сократить количество неправильных переходов в приложение и сократить количество кода для обработки ссылок.
Заявлено, что pathAdvancedPattern
работает с API 26, но эксперименты на эмуляторе Pixel показали, что до API 31 атрибут игнорируется и intent-filter принимает любой path. Возможно, поведение будет отличаться при тестировании на реальных устройствах и устройствах с оболочками от других вендров. На моем Samsung, One UI 4.1, Android 12.0 поведение не отличалось.
На этом все. Если тебе известно что-то большее, то жду в комментариях. Надеюсь, эти атрибуты сделают твой Android более приятным. С наступающим! ☃️????