Blog Infos
Author
Published
Topics
Published
Topics

 

Generated by Midjourney: https://discord.com/channels/662267976984297473/997271873551351808/1056602339995303989

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:

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 pathPatternpathAdvancedPattern has a point . to designate one arbitrary character.

Examples of using . in pathPatternAdvancedExamples 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 pathPatternAdvancedExamples 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 Xscheme://host/some/0…(65469 zeros)…0scheme://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 pathPatternAdvancedExamples 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 pathPatternAdvancedExamples 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 pathPatternAdvancedExamples of using {…} and , in pathPatternAdvanced

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

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 pathPatternAdvancedExamples 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 pathPatternAdvancedAdvanced 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.0Examples 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_HOSTMATCH_CATEGORY_SCHEMENO_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 pathSuffixExamples 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 />pathAdvancedPatternpathSuffix.

Unlike pathPatternpathAdvancedPattern 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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE
Menu