About the possibilities in handling complex paths in deep links using new attributes: pathAdvancedPattern, pathSuffix available from Android 12.0, and nuances of its use.
Generated by Midjourney: discord.com
Hi all! This article came about quite by accident. When we were preparing Deep Dive Into Deep Link (habr, mobius in 2022), came to the conclusion:
There is no convenient and flexible tool for filtering complex paths at the OS level.
I was not reassured by the fact that in Android there is nothing better than a pathPattern with two poor . and
*. I’ve already started making excuses for Google. I came up with a reason why it was done this way:
Restrictions in regular expressions for pathPattern made in order not to slow down intent resolution.
Even after this seemingly logical explanation, I still could not believe that such an obvious function was not available and I decided to google one last time. Here’s what was found:
- developer.android.com: Create Deep Links to App Content. There are no complex scenarios.
- Medium: How to manage a complex DeepLinks scheme on your Android App. He says that there is no full support for regular expressions.
- Stack Overflow: How to use PathPattern in order to create DeepLink Apps Android? Nothing remarkable either.
And stumble upon this… Stack Overflow: How to use PATTERN_ADVANCED_GLOB in android manifest intent-filter? This thread points out that PatternMatcher has an attribute PATTERN_ADVANCED_GLOB. The author asks if he can use it in <data/>? He is told that this is not possible. PATTERN_ADVANCED_GLOB was added in API 26 and Google hasn’t announced anything related to them. Next is the funniest. The author answers:
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!
Three years have passed since then… I was already desperate and decided to take a last look at documentation… And you know what? There is an attribute pathAdvancedPattern! It seems that Santa Claus threw it there just before the New Year! 🎅🎄 I swear I didn’t see this attribute when I was preparing Deep Dive Into Deep Link. It wasn’t there! Moreover, there are no articles on the web about pathAdvancedPattern! I give it to you 🎁 Happy New Year!
Features of pathAdvancedPattern
Unlike pathPattern, which has special characters available: .,
* , pathAdvancedPattern can handle:
.
, *,
[…],
^,
+,
{…}. Let’s look at each pattern with examples.
How it will look in code:
<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="We will insert examples here"/> </intent-filter> </activity>
All examples below I will run on the Pixel 2, arm64, API 31 emulator using the command:
adb shell am start -d <reference>
Feature of pathAdvancedPattern: dot .
Like pathPattern, pathAdvancedPattern has a point . to designate one arbitrary character.
Examples of using . in pathPatternAdvanced
Unfortunately, despite the promises “any character”, they cannot be processed (examples with *,
>,
)). Fortunately, such characters are not often seen 🙂
Feature of pathAdvancedPattern: asterisk *
Asterisk * specifies that the character immediately before can be repeated zero or any number of times.
Examples of using * in pathPatternAdvanced
Yes, I already see your hungry eyes looking at “any number of times” 👹 A question from the “why do I know this” category: how many characters can * handle? The answer is 65471. To find out, I ran:
adb shell am start -d scheme://host/some/0…(65469 zeros)…0
and saw in the terminal error: closed.
Feature of pathAdvancedPattern: plus +
Plus + indicates that the character immediately before it can be repeated 1 or any number (65471) times.
Interesting remark:
If X characters are added to host, then the number of zeros will need to be reduced by X: scheme://host/some/0…(65469 zeros)…0, scheme://host/some123/0…(65466 zeros)…0. I suspect that the length limit does not apply to the path itself, but to the URL as a whole (65490 characters).
Examples of using + in pathPatternAdvanced
Feature of pathAdvancedPattern: square brackets […]
Square brackets […] indicate a set of characters to match. Inside square brackets, a hyphen
– can be used to indicate the range between the character on the left and right. A hyphen is useful for shortening a sequence of consecutive characters. The cap symbol
^ is used to invert a set.
Examples of using […], -, ^ in pathPatternAdvanced
Feature of pathAdvancedPattern: curly brackets {…}
Curly braces {…} are used to indicate the number of repetitions of a pattern. Inside parentheses can be:
- One number. The pattern must repeat exactly the specified number. No more, no less.
- Two numbers separated by comma. In this case the number of repetitions is greater than or equal to the number on the left and less than or equal to the number on the right.
- One number and a comma. As in the previous case, but without restrictions on the right.
Examples of using {…} and , in pathPatternAdvanced
Job Offers
Feature of pathAdvancedPattern: backslash \
The backslash \ allows you to make special characters normal. Keep in mind that the backslash in XML is used as an escape character, so it is “eaten” during parsing. In order not to lose the slash, you must write
\\. And to escape the slash —
\\\\. Be careful in examples below 🔎
Examples of using \ in pathPatternAdvanced
Real examples of pathAdvancedPattern use
Much more possibilities, isn’t it? Now these patterns can be used to process links that we only dreamed of before ✨:
Advanced examples of using pathPatternAdvanced
I tested all the examples above on API 31. It is stated that pathAdvancedPattern works with API 26. So I set up android:pathAdvancedPattern=”/[0–9]{2}” and ran on Android 8.0 and…
Not everything is so smooth
Examples of unexpected behaviour then using pathPatternAdvanced on Android 8.0
It starts to process everything! All the declared possibilities seem to be simply ignored! I decided to repeat the tests for Android 7.0, 9.0, 10.0, 11.0, 12.0, 13.0.
I remind you, the test device: Pixel 2 emulator, arm64. For Android 7.0 (API 24), 8.0 (API 26), 9.0 (API 28), 10.0 (API 29), 11.0 (API 30) the value of pathAdvancedPattern is ignored. The application behaves as if instead of /[0–9]{2}, for example, /.* would be written. The patterns declared in pathAdvancedPattern only work starting from API 31: Android 12.0 (API 31), 13.0 (API 33).
Attempts to figure it out
In fact, everything that is described above, I did not immediately learn. I had several days to figure out why pathAdvancedPattern does not work on API 26. One day I almost despaired and almost decided to abandon the article, but I launched it on API 31 and it saved the day. I will briefly describe what I learned in those few days.
Intent resolution consists of many steps. I decided to start with PatternMatcher. I found a commit which introduced PATTERN_ADVANCED_GLOB. I created a test application and wrote the code there:
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")
Launched on API 26 and saw:
PatternMatcher: /user/1 - false /user/123 - true /user/1234 - false /user/12a - false
Worked as it should.
Move on. Let’s look at the IntentFilter. I write code:
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")
I run on API 26 and see:
IntentFilter: myscheme://myhost/user/1 - -2 myscheme://myhost/user/123 - 5275648 myscheme://myhost/user/1234 - -2 myscheme://myhost/user/12a - -2
What are the magic numbers? intentFilter.match returns a number indicating which categories matched. For example, MATCH_CATEGORY_HOST, MATCH_CATEGORY_SCHEME, NO_MATCH_DATA and others or their sum (more here).
- NO_MATCH_DATA = -2. This means that the intent did not pass the filter from the data URI difference.
- MATCH_CATEGORY_PATH + MATCH_ADJUSTMENT_NORMAL = 5275648. This means that the data URI intent matches the filter. Exactly as expected from a working PATTERN_ADVANCED_GLOB.
Go up higher: PackageManager. Let’s call the method queryIntentActivities to get all activities that can be started by the intent specified. I add the code:
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")
I set up the 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>
and run:
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 has an attribute match. It shows how the OS evaluated the intent to match the filter. The log displays a hexadecimal number. 0x30800 corresponds to 3178496 in decimal. MATCH_ADJUSTMENT_NORMAL+MATCH_CATEGORY_HOST = 3178496. This means that the OS only evaluated the match in the host category. There are no matches in the path category.
I didn’t dive further. If you know the details, then welcome to the comments.
Features of pathSuffix
Remember the pathPrefix attribute? API 31 introduced its counterpart for suffix matching. It can be used for URLs where the path ending is known:
Examples of using pathSuffix
When running on APIs below 31, the behaviour is similar to pathAdvancedPattern: any path is processed.
Conclusions
With API 31 (Android 12.0), developers have the opportunity to use two new attributes in <data />: pathAdvancedPattern, pathSuffix.
Unlike pathPattern, pathAdvancedPattern has new patterns for describing regex-like expressions:
[…] to represent multiple characters. Inside square brackets, you can use dash
– to denote a sequence of characters and cap
^ to invert a set.
+ to indicate a pattern that can be repeated one or more times.
{…} to indicate the exact number or range of repetitions.
I hope there will be support for new special regex characters soon. For example: ?
, \d
, \D
, \w
, \W
, (…)
, |
, etc.
The new pathSuffix attribute is similar to pathPrefix but only works on path endings.
The pathAdvancedPattern is claimed to work with API 26, but experiments on the Pixel emulator have shown that prior to API 31, the attribute is ignored and intent-filter accepts any path. Perhaps the behaviour will be different when testing on real devices and devices with shells from other vendors. I’ve also tested pathAdvancedPattern on my Samsung, One UI 4.1, Android 12.0 and the behaviour was not different.
Starting from Android 12.0, using pathAdvancedPattern and pathSuffix, you can handle links that previously had to be handled only at the application level. pathAdvancedPattern and pathSuffix will help reduce the number of incorrect navigations to the application and reduce the amount of code for handling links.
That’s all. If you know something more, then I’m waiting in the comments. Hopefully these attributes will make your Android more enjoyable. Happy New Year! ☃️🎄
This article was originally published on proandroiddev.com on December 28, 2022