Всем привет! Эта статья получилась совершенно случайно. Когда мы готовили Deep Dive Into Deep Link (habr, mobius в 2022 году), пришли к выводу: 

Не существуют удобного и гибкого инструмента для фильтрации сложных path на уровне ОС.

Меня никак не успокаивало то, что в Android нет ничего лучше, чем pathPattern с двумя бедными . и *. Я уже начал оправдывать Google. Придумал причину почему сделано именно так:

Ограничения в регулярных выражениях для pathPattern сделаны для того, чтобы не замедлять intent resolution.

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

И натыкаюсь на это… 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 доступна точка . для обозначение одного произвольного символа. 

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/…

scheme://host/abc

Да

scheme://host/1.2

Да

scheme://host/=_0

Да

scheme://host/=_*

Нет. no matches found

scheme://host/=_>

Нет. parse error near `\n'

scheme://host/=_)

Нет. parse error near `)'

К сожалению, даже несмотря на обещанные “any character”, их обработать не получится (примеры с *, >, )). К счастью, такие символы встречаются не часто ????

Звездочка * указывает, что символ, идущий сразу перед ней, может быть повторен 0 или сколько угодно раз.

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/.*

scheme://host/abc

Да

scheme://host/1

Да

scheme://host/__0__

Да

scheme://host/some/0…(65469 нулей)...0

Нет. error: closed

Да я уже вижу ваши голодные глаза, смотрящие на “сколько угодно раз” ???? Вопрос из категории “зачем я это знаю”: сколько символов сможет обработать *? Ответ 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 символов).

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/.+

scheme://host/1

Да

scheme://host/asv

Да

scheme://host/_+++

Да

Аналогично для /*

Нет

/e+

scheme://host/e

Да

scheme://host/ee

Да

scheme://host/eeeeee

Да

scheme://host/

Нет

scheme://host/ea

Нет

scheme://host/aaa

Нет

/e+a

scheme://host/ea

Да

scheme://host/eea

Да

scheme://host/eeeeea

Да

scheme://host/ec

Нет

scheme://host/eca

Нет

scheme://host/eeac

Нет

Квадратные скобки [...] указывают на множество символов, по которым будет выполняться соответствие. Внутри квадратных скобках может быть использован дефис - для обозначения промежутка между символом слева и справа. Дефис удобен для сокращения записи последовательности символов идущих друг за другом. Символ крышечки ^ используется для инвертирования множества.

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/[a1_]+

scheme://host/a

Да

scheme://host/11111

Да

scheme://host/aa_1__11

Да

scheme://host/c

Нет

scheme://host/a1d_

Нет

scheme://host/1____aca

Нет

/[a-zA-Z]+

scheme://host/valery

Да

scheme://host/Valery

Да

scheme://host/AbCdE

Да

scheme://host/valery1

Нет

scheme://host/va_petrov

Нет

scheme://host/

Нет

/[^0-9]

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/+-

Нет

Фигурные скобки {...} используются для обозначения количества повторений шаблона. Внутри скобок может быть указано:

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

  • Два числа через запятую. Количество повторений больше или равно числу слева и меньше или равно числу справа.

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

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/abc{3}

scheme://host/abccc

Да

scheme://host/abc

Нет

scheme://host/abcccc

Нет

scheme://host/abcabcabc

Нет

/[0-9]{2,4}

scheme://host/12

Да

scheme://host/987

Да

scheme://host/0246

Да

scheme://host/1

Нет

scheme://host/12345

Нет

/[a-z]{3,}

scheme://host/abc

Да

scheme://host/zxyulmn

Да

scheme://host/ab

Нет

scheme://host/p

Нет

/[a-z]{,3}

Такой паттерн не скомпилируется

Обратный слеш \ позволяет делать специальные символы обычными. Необходимо учитывать, что обратный слеш в XML используется в качестве escape character, поэтому при парсинге он “съедается”. Чтобы не потерять слеш необходимо писать \\. А чтобы экранировать слеш —  \\\\. В этих примерах будь внимательнее ????

pathAdvancedPattern

Пройдет фильтр

Не пройдет фильтр

/\+

scheme://host/

Да

scheme://host////

Да

scheme://host///////

Да

scheme://host/+

Нет

/\\+

scheme://host/+

Да

scheme://host/

Нет

scheme://host///

Нет

/\\\\

scheme://host/\

Да. При запуске из adb необходимо прописать \\\\

scheme://host/\\

Нет

Гораздо больше возможностей, не правда ли? Теперь эти шаблоны можно использовать для обработки ссылок, о которых мы раньше только мечтали ✨:

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/user/[0-9]+

/user/0

Да

/user/123

Да

/user/9876

Да

/user/

Нет

/user/abc

Нет

/user/12a

Нет

/login/[a-zA-Z0-9]+

scheme://host/name/valery007

Да

scheme://host/login/1^v

Нет

/phone/\+7[0-9]{10}

scheme://host/phone/+79876543210

Да

scheme://host/phone/+7987

Нет

scheme://host/phone/79876543210

Нет

scheme://host/phone/9876543210

Нет

/uuid/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}

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 и…

Не все так гладко

pathAdvancedPattern

Ссылка

Пройдет фильтр?

/[0-9]{2}

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:

pathSuffix

Ссылка

Пройдет фильтр?

end

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 более приятным. С наступающим! ☃️????

Комментарии (0)