Введение

Раз, два, три, четыре! В прошлый раз мы выяснили вот что

Гипотеза 3. Разные оболочки ОС — ОПРОВЕРГНУТА. 

Оболочка Android ОС НЕ ВЛИЯЕТ на работу Deep Link

Гипотеза 4. Приложения — ПОДТВЕРЖДЕНА. 

Приложение, из которого открывается ссылка, ВЛИЯЕТ на работу Deep Link.

Как обычно не обошлось без тонкостей: оболочка может влиять на обработку, но через свои (специфичные для оболочки) приложения. У приложений есть встроенные браузеры, но даже они не повлияли на однозначный вывод – приложения влияют на работу deep link. 

Сегодня мы завершаем разбор наших семи проблем. Наконец-то! Набирайте кислород. Погружаемся!

Содержание

  1. Введение

  2. Проблема №5. Ссылки без scheme

  3. Обработка scheme приложением

  4. Проблема №6. Второй host

  5. Проблема №7. Перехват всех ссылок

  6. Резюме

Проблема №5. Ссылки без scheme

Ссылки вида host/path обрабатывались по-разному

Одна и та же ссылка, одно и то же устройство, но разные приложения (Messaging, Gmail):

Таблица конфигурации X-1
Таблица конфигурации X-1
Таблица конфигурации Y-1
Таблица конфигурации Y-1

Теперь эксперименты:

X-1 (Messaging). По клику на ссылку ПРЕДЛАГАЕТСЯ перейти в наше приложение.
X-1 (Messaging). По клику на ссылку ПРЕДЛАГАЕТСЯ перейти в наше приложение.
Y-1 (Gmail). По клику на ссылку НЕ ПРЕДЛАГАЕТСЯ перейти в наше приложение.
Y-1 (Gmail). По клику на ссылку НЕ ПРЕДЛАГАЕТСЯ перейти в наше приложение.

Мы уже знаем, что приложение влияет на обработку deep link. Но мы еще не пробовали переходить по ссылкам без host. Как видно выше переход по ссылке возможен, даже несмотря на отсутствие host. Предположим:

Гипотеза 5. Существует специальный механизм обработки ссылок без scheme.

Может появиться ощущение, что если прописать в манифесте схему null, то ссылки без схемы будут работать. Давайте попробуем

null scheme для intent-filter.
null scheme для intent-filter.

Есть еще один способ тестировать deep link, помимо ручного клика по ссылкам. Для этого нужно вызвать команду adb

adb shell am start -W -d null:developer.android.com 

Эта команда запустит intent с указанным Data URI (-d) и будет ожидать завершения запуска (-W).

Если настроить соответствующий intent-filter, то ОС сможет понять может ли ваше приложение обработать этот intent. Мы так и сделали. Теперь давайте посмотрим на скринкаст:

Запуск intent с null scheme в URI через adb.
Запуск intent с null scheme в URI через adb.

На заднем фоне видно, как наше приложение запустилось. Это значит, что

android:scheme="null" — это конкретное значение scheme, а не ее отсутствие.

Если null это не значит отсутствие scheme, то может вообще ее не указывать? Интересное предположение. Настраиваем intent-filter:

Манифест с настроенным intent-filter без атрибута android:scheme.
Манифест с настроенным intent-filter без атрибута android:scheme.

Студия подсказывает, что мы не указали обязательный атрибут scheme. Но не смотря на это скомпилировать получится.

Попробуем запустить intent через adb, но в этот раз без scheme:

adb shell am start -W -d developer.android.com 

В консоли видим:

Starting: Intent { dat=developer.android.com/about }
Error: Activity not started, unable to resolve Intent { dat=developer.android.com/about flg=0x10000000 }

Intent resolution не завершился успехом, даже не смотря настроенный intent-filter. Давайте кликнем на ссылку:

Deep link не работает, если не указывать android:scheme в intent-filter.
Deep link не работает, если не указывать android:scheme в intent-filter.

Обратимся к документации. Там написано, что, как минимум, нужно прописать хотя бы одну схему для интент-фильтра, иначе ни один другой атрибут не будет обработан (схожее сообщение мы видели в студии): 

“...at least one scheme attribute must be set for the filter, 

or none of the other URI attributes are meaningful.”

— А что если написать android:scheme=””

— Будет тоже самое, что и для только что разобранного примера.

После нескольких попыток у нас появилось предположение, что на обработку влияет http. Это оказалось действительно так. Посмотрите, что происходит на записи:

При открытии ссылки developer.android.com/about http автоматически подставляется к урлу.
При открытии ссылки developer.android.com/about http автоматически подставляется к урлу.

Давайте посмотрим на нечто более убедительно. Открываем logcat:

I/ActivityTaskManager: START u0 {
act=android.intent.action.VIEW 
dat=http://developer.android.com/about 
flg=0x14002000 cmp=com.android.chrome/
org.chromium.chrome.browser.ChromeTabbedActivity 
(has extras)
} from uid 10116

Здесь уже не остается никаких вопросов. http scheme была подставлена в URL без scheme. Причем это сделало именно приложение, а не мы или что-либо еще! Это в очередной раз доказывает, насколько сильно приложение, из которого открывается ссылка, влияет на работу deep link. Но как так получилось? Узнаем дальше.

Обработка scheme приложением

Кажется, что developer.android.com/about как-то сам сумел превратиться в ссылку. Так ли это на самом деле. Проверить просто: добавим TextView на экран и в android:text укажем три вида ссылок:

TextView c android:text не преобразует URL в кликабельную ссылку.
TextView c android:text не преобразует URL в кликабельную ссылку.

Никакой магии не случилось. Ссылки не стали кликабельными. Все то сколько на нее потом не нажимай, она не будет кликабельной, если не настроен специальный обработчик.

В Android есть много способов обработать URL (и scheme, в частности):

Способы обработки scheme и URL.
Способы обработки scheme и URL.

Рассмотрим каждый подробнее на примере. Дано:

TextView для отображения текста.
TextView для отображения текста.
LiveData, которая устанавливает текст в TextView.
LiveData, которая устанавливает текст в TextView.
ViewModel, которая устанавливает текст в LiveData.
ViewModel, которая устанавливает текст в LiveData.

android:autoLink

Начнём с самого лаконичного способа. Атрибут android:autoLink запускает поиск паттернов по содержимому TextView. Среди паттернов для поиска есть email, phone, map. Константа web, выполняет поиск URL'ов. То, что там нужно.

Описание autoLink. По умолчанию устанавливается none, который отключает появление кликабельных ссылок (поэтому в примере с android:text ничего не произошло).
Описание autoLink. По умолчанию устанавливается none, который отключает появление кликабельных ссылок (поэтому в примере с android:text ничего не произошло).

Добавляем autoLink c значением web в TextView:

Использование autoLink.
Использование autoLink.

Запускаем. Видим, что ссылка стала кликабельной. Более того, по клику запускается интент, где в data лежит URL с автоматически проставленной http схемой. Красота!

autoLink автоматически подставил http к ссылке без scheme и сделал ее кликабельной.
autoLink автоматически подставил http к ссылке без scheme и сделал ее кликабельной.

android.text.util.Linkify.addLinks

Эта статическая функция применяет регулярное выражение к содержимому TextView и выполняет поиск соответствий. Похоже на работу autoLink. В качестве аргумента addLinks можно передать defaultScheme, которая будет приставлена к ссылке, если та не начинается со схемы, указанной в другом параметре.

Вызываем addLinks и передаем нашу TextView, Pattern для WEB URL, дефолтную схему https, стандартный UrlMatchFilter, и null TransformFilter.

Пример использования функции Linkify.addLinks.
Пример использования функции Linkify.addLinks.

Запускаем. Результат как и в прошлом примере, но только в этот раз была подставлена https scheme.

android.text.util.Linkify.TransformFilter

Та же функция addLinks, но в этот раз используется параметр transformFilter, который позволяет изменять найденные ссылки.

В этот раз defaultScheme равны null, а в качестве transformFilter выступает наш HttpsTransformFilter, который добавляет префикс к URL.

Пример использования функции Linkify.addLinks с собственным transformFilter.
Пример использования функции Linkify.addLinks с собственным transformFilter.

Запускаем. Результат такой же, как и в прошлом примере.

android.text.Html

Если в приложении есть работа с HTML тегами, то этот способ может быть кстати. Воспользуемся статической функцией fromHtml и классом LinkMovementMethod. 

fromHtml из html-тега возвращает отображаемый текст и устанавливает ему ссылку из атрибута href. LinkMovementMethod представит переданный ему текст как кликабельную ссылку. 

Пример использования функции Html.fromHtml и LinkMovementMethod. На данный момент рекомендуется использовать не deprecated аналог с флагами.
Пример использования функции Html.fromHtml и LinkMovementMethod. На данный момент рекомендуется использовать не deprecated аналог с флагами.
Пример использования html-тегов в строках.
Пример использования html-тегов в строках.

Запускаем. Ссылка без scheme превратилась в кликабельную и указывает на https-ссылку.

android.text.style.URLSpan

Теперь посмотрим на механизм, который лежит в основе большинства разобранных способов обработки — spannable.

Интерфейс Spanned позволяет добавлять разметку к тексту. Одним из видов Span является URLSpan, который преобразует выбранный текст в кликабельный. По клику на URLSpan запускается Intent с ACTION_VIEW. Фильтр на этот aciton мы настраивали в нашем приложении.

Во ViewModel мы создали SpannableString, передали ссылку без схемы, добавили URLSpan, указали необходимую ссылку. Так как SpannableString не является наследником String, то меняем тип данных на CharSequence. Обратите внимание на LinkMovementMethod. Он триггерит запуск интента по ссылке из URLSpan.

Пример использования URLSpan.
Пример использования URLSpan.

Запускаем. Работает как часы!

Мы разобрали три наиболее распространенных способов обработки ссылок. Есть и другие (например, Better-Link-Movement-Method), но они, как правило, используют разобранные нами API. 

Все еще помните, о какой гипотезе шла речь? :) Мы проделали большую работу и смело можем заключить:

Гипотеза 5 — ПОДТВЕРЖДЕНА. 

Существует специальный механизм обработки ссылок без scheme.

Проблема №6. Второй host

Мы забыли о том, что у нас есть второй домен.

У нашего проекта два хоста: один из них основной, а другой редиректит на основной. Нам заранее неизвестно, по какому из них пользователь перейдет в наше приложение. При этом рабочими должны быть оба варианта.

Сначала мы думали, что можно прописать для каждого хоста свой интент-фильтр:

Отдельные intent-filter для каждого host.
Отдельные intent-filter для каждого host.

Потом вспомнили, что по итогам все data-теги из одного фильтра, смерджатся и будут созданы всевозможные комбинации из их атрибутов. Не стоит забывать об этой особенности, потому что она может привести к неожиданным последствиям :)

Мы решили не поддерживать два отдельных intent-filter и переписали предыдущий код. Получилось сильно короче и проще.

Два host в одном intent-filter.
Два host в одном intent-filter.

Проблема №7. Перехват всех ссылок

Приложение начало перехватывать все ссылки с нашим доменом 

(даже те, для которых у нас не было сценариев обработки)

Есть два ресурса: privacy-policy и terms-of-use. Подразумевается, что пользователь будет обращаться к ним через браузер (на эти ссылки мы явно не настраивали deep link). В нашем приложении нет экранов для отображения этого контента. Но как вы можете видеть на скринкасте, при переходе по ссылкам система предлагает открыть наше приложение. У нас этого контента нет, поэтому пользователь видит 404. Знатоки, внимание: вопрос: “Почему так случилось?”.

Приложение перехватывает ссылку на ресурс, который не обрабатывается мобильным приложением.
Приложение перехватывает ссылку на ресурс, который не обрабатывается мобильным приложением.

Изначально мы хотели добавить диплинки на ссылки без path. То есть ссылки, у которых есть только схема и хост (например, http://developer.android.com). Для этого мы настроили наш intent-filter так:

Intent-filter без android:path* атрибутов.
Intent-filter без android:path* атрибутов.

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

Но помимо этого появилась обработка ссылок с path на несуществующий контент, что вело к появлению 404 ошибок. Дело в том, что 

Отсутствие явно указанного path подразумевает использование любого значения.

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

Резюме

Фух! Много нового в этот раз:

  • Гипотеза 5 — Подтверждена. Существует специальный механизм обработки ссылок без scheme.

  • Для тестирования deep link иногда удобно пользоваться командой: adb shell am start -W -d “Data URI”.

  • android:scheme="null" — это конкретное значение scheme, а не ее отсутствие.

  • android:scheme="" или отсутствие этого атрибута приведет к тому, что ни один другой атрибут не будет обработан и deep link работать не будет.

  • android:autoLink — короткий и простой способ сделать URL кликабельной ссылкой.

  • Linkify.addLinks функция, которая применяет регулярное выражение к содержимому TextView и выполняет поиск соответствий и имеет параметры для обработки.

  • android.text.Html.fromHtml преобразует html-теги в Spanned, URLSpan в частности.

  • android.text.style.URLSpan специальный вид span, который преобразует текст в кликабельный и запускает intent с ACTION_VIEW и Data URL по клику.

  • android.text.method.LinkMovementMethod обрабатывает нажатие по тексту-ссылке.

  • Data-теги мерджатся между собой. По итогу получаются всевозможные комбинации из значений. Например: http, https, host1, host2 преобразуются в http://host1, http://host2, https://host1, https://host2.

  • Аккуратнее с ссылками без path: его отсутствие в <data /> понимается ОС как возможность передавать любое значение. Ваше приложение может начать обрабатывать то, что не должно.

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

  • Почему path паттерны такие слабые

  • Как происходит диспетчеризация URL?

  • Почему Jetpack Navigation Component не так крут, как кажется?

  • Как мы кастомизировали обработка deep link.

  • И почему очень важно помнить про обратную совместимость deep link. 

Скоро увидимся!

Валера Петров

Android-разработчик. TG: @valeryvpetrov

Ангелина Евсикова

Android-разработчица Технократии. TG: @Angelina_dev


Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.

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