2 января 2016 г.

Chrome Custom Tabs

    Уже достаточно давно Google представила Custom Tab’ы. Чтобы не таскать конструкцию “custom tab” обзову их по своей сути – это управляемые вами (разработчиками) вкладки от браузера Chrome, но внутри ваших приложений, по типу WebView. Вы как бы заказываете у Хрома его возможности. Так что обзову их “клиентские табы”.
    Статья – перевод вот этой официальной статьи: https://developer.chrome.com/multidevice/android/customtabs Опубликован оригинал был давно, ещё 30 августа 2015, но популярности клиентские табы пока не получили. Лично у меня пока только одно приложение их использует – Talon for Twitter. А между тем это реально удобная вещь, возможности который сильно превышают возможности системного WebView. К тому же эта фишка работает быстрее ВебВью и загружается быстрее Хрома.

Что такое клиентские табы Chrome?

    Когда пользователь тапает на URL, разработчики приложений сталкиваются с выбором – запустить браузер или же загрузить данные внутри приложения, например во WebView. Оба варианта – тот ещё выбор: запуск браузера уводит пользователя из приложения, да к тому же им нельзя управлять, а WebView не имеет нормального сквозного взаимодействия с браузером, да ещё добавляет головной боли при его сопровождении.
   Клиентские табы Chrome дают приложениям значительный контроль над вебом внутри них и, кроме того, делают взаимодействие между данными приложения и веб контентом более тесным, не требуя использования WebView. Клиентские табы Chrome позволяют приложению настроить, как Chrome должен выглядеть и что должен уметь делать. Приложение может изменять такие вещи как:
  • Цвет тулбара
  • Анимации входа и выхода
  • Добавлять собственные действия на тулбар Chrome и дополнительное меню (примечание: речь идёт об overflow menu. Это то самое, которое вываливается при нажатии на три точки, расположенные вертикально)
    Также клиентские табы позволяют разработчикам запускать Chrome заранее и заранее же подгружать в него нужный контент, чтобы, когда он понадобиться, отобразить его практически мгновенно.
CCT_Large 2Рис. 1. Демонстрация скорости загрузки страницы в клиентском табе, во внешнем Chrome и во WebView.
    Вы сами можете протестировать наш пример клиентских табов: GitHub.

Когда мне стоит использовать клиентские табы Chrome вместо WebView?

    WebView – это правильный выбор, если данные, которые нужно показать, находятся внутри ваших приложений. Если URL ведёт во вне, то мы рекомендуем использовать клиентские табы по этим причинам:
  • Простое внедрение. Нет необходимости писать код для управления запросами, запросами разрешений, управления куками.
  • Управление внешним видом:
    • Цвет тулбара
    • Кнопки действий
    • Собственные элементы меню
    • Собственные анимации входа/выхода
  • Осведомлённость о сёрфинге: браузер предоставляет приложению обратные вызовы о действиях, происходящих в нём.
  • Оптимизация производительности:
    • Предзагрузка браузера в фоне за счёт ресурсов приложения
    • Передача подходящего URL в браузер заранее, чтобы он смог предзагрузить данные, увеличив скорость загрузки страницы при обращении к ней
  • “Выживаемость” приложения: пока браузер находится на переднем плане, он не даёт системе убить приложение, т. к. считается, что это оно на переднем плане.
  • Общее (с Chrome) хранилище кук, а также модель управления разрешениями. Пользователю не нужно будет авторизовываться на сайтах, если он на них уже был ранее; не нужно предпринимать разрешения, если они уже запрашивались ранее.
  • Если пользователь включал в Chrome сжатие данных, то здесь оно тоже будет работать.
  • Синхронизация Автоподстановки между разными устройствами для удобства заполнения форм.
  • Простая настройка.
  • Быстрый возврат в приложение – одиночный тап.
  • Последняя версия браузера доступна в том числе на устройствах под Android 4.4 и ниже (в 5.0 WebView стал обновляемым), в отличие от WebView.

Когда это будет доступно?

    Клиентские табы Chrome предоставляет, начиная с версии 45. Т.е. это работает уже у всех, у кого установлен минимум Jellybean. Примечание: Последняя версия для ICS, напомню, была 42. Сейчас на ICS (API lvl15) приходится 2,9% устройств.

Инструкция по внедрению

    Полноценный пример доступен по линку https://github.com/GoogleChrome/custom-tabs-client. Он содержит классы для управления UI, подключения к фоновому сервису и защиту от прибивания и для приложения, и для активити клиентского таба. Кроме того в нём есть AIDL файлы, необходимые для подключения к сервису, т.к. те, что содержатся в Cromium репозитории не могут быть использованы в Android Studio напрямую.
    Если вы будете следовать инструкциям с этой страницы, то у вас всё получится.
  • Настройка UI и взаимодействие с клиентскими табами.
  • Ускорение загрузки страницы и защита приложения от прибивания.
    Первое делается добавлением дополнительных сведений в интент ACTION_VIEW, отправляемый в Chrome. Второе – подключением к сервису, предоставляемому Хромом.
Замечание. Всё это работает, начиная с Chrome 45;
Включение клиентских табов Chrome
// Using a VIEW intent for compatibility with any other browsers on device.
// Caller should not be setting FLAG_ACTIVITY_NEW_TASK or 
// FLAG_ACTIVITY_NEW_DOCUMENT. 
String url = ¨https://paul.kinlan.me/¨;
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 
//  Must have. Extra used to match the session. Its value is an IBinder passed
//  whilst creating a news session. See newSession() below. Even if the service is not 
//  used and there is no valid session id to be provided, this extra has to be present 
//  with a null value to launch a custom tab.

private static final String EXTRA_CUSTOM_TABS_SESSION = "android.support.customtabs.extra.SESSION";
Bundle extras = new Bundle;
extras.putBinder(EXTRA_CUSTOM_TABS_SESSION, 
   sessionICustomTabsCallback.asBinder() /* Set to null for no session */);
intent.putExtras(extras);
Что случится, если у пользователя не установлено подходящих версий Chrome?
    Мы используем интент ACTION_VIEW, что означает, что по умолчанию страница будет открыта в системном браузере или в браузере, выбранным пользователем основным. Если у пользователя браузером по умолчанию выбран Chrome, он автоматически отловит EXTRAS и отобразит модифицированный UI. Кроме того возможно, что сторонние браузеры смогут реагировать аналогично и тоже предоставлять модифицированный IU.
Тем не менее, как я могу проверить, поддерживает ли Chrome клиентские табы?
    Все версии Chrome, поддерживающие клиентские табы, предоставляют сервис (см. ниже “Подключение к сервису”). Для проверки поддержки клиентских табов в Chrome, попробуйте связаться с сервисом. Если удалось, то клиентские табы можно использовать.
Настройка цвета адресной строки
    Самое важное (и самое простое для внедрения) в клиентских табах – это возможность задать свой цвет адресной строки, чтобы он был таким же, как и в вашем основном приложении.
// Extra that changes the background color for the omnibox. colorInt is an int
// that specifies a Color.

private static final String EXTRA_CUSTOM_TABS_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
intent.putExtra(EXTRA_CUSTOM_TABS_TOOLBAR_COLOR, colorInt);
Настройка действия собственной кнопки
action
    Как разработчик, вы имеете полный контроль над кнопками, которые даёте пользователям внутри клиентского таба.
    Самое популярное действие – это “Поделиться” или подобное взаимодействие, выполняемое пользователями.
    Кнопки действий представляются как пакет с иконкой кнопки и pendingIntent’а, который будет вызван Хромом, если пользователь нажмёт на кнопку. Здесь иконка имеет 24dp в высоту и 24-48 в ширину.

// Key that specifies the Bitmap to be used as the image source for the
// action button.

private static final String KEY_CUSTOM_TABS_ICON = "android.support.customtabs.customaction.ICON";
// Key that specifies the PendingIntent to launch when the action button
// or menu item was tapped. Chrome will be calling PendingIntent#send() on
// taps after adding the url as data. The client app can call
// Intent#getDataString() to get the url.
public static final String KEY_CUSTOM_TABS_PENDING_INTENT = "android.support.customtabs.customaction.PENDING_INTENT";

// Optional. Use a bundle for parameters if an the action button is specified.
public static final String EXTRA_CUSTOM_TABS_ACTION_BUTTON_BUNDLE = 
"android.support.customtabs.extra.ACTION_BUNDLE_BUTTON";
Bundle actionButtonBundle = new Bundle();
actionButtonBundle.putParcelable(KEY_CUSTOM_TABS_ICON, icon);
actionButtonBundle.putParcelable(KEY_CUSTOM_TABS_PENDING_INTENT, pendingIntent);
intent.putExtra(EXTRA_CUSTOM_TABS_ACTION_BUTTON_BUNDLE, actionButtonBundle);
Настраиваем меню
menu
    Меню в Chrome содержит полный набор действий, который пользователь может выполнить в браузере. Однако, далеко не все из них нужны в контексте вашего приложения.
    Клиентские табы Chrome всё время имеют три колонки с кнопками “Вперёд”, “Информация о странице” и “Обновить” в верхней части меню и пункты “Найти на странице” и “Открыть в Chrome” в нижней части меню.
    Как разработчик, вы можете добавить до трёх своих пунктов меню между верхними кнопками и двумя нижними пунктами.
    Меню представляется как массив из совокупностей (сейчас без иконки) текста элемента меню и pendingIntent, который Chrome вызовет по вашему требованию, если пользователь тапнет на этот элемент меню.

// Key for the title string for a given custom menu item
public static final String KEY_CUSTOM_TABS_MENU_TITLE = "android.support.customtabs.customaction.MENU_ITEM_TITLE";

// Optional. Use an ArrayList for specifying menu related params. There 
// should be a separate Bundle for each custom menu item.
public static final String EXTRA_CUSTOM_TABS_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
ArrayList menuItemBundleList = new ArrayList<>();

// For each menu item do:
Bundle menuItem = new Bundle();
menuItem.putString(KEY_CUSTOM_TABS_MENU_TITLE, menuItemTitle);
menuItem.putParcelable(KEY_CUSTOM_TABS_PENDING_INTENT, pendingIntent);
menuItemBundleList.add(menuItem);

intent.putParcelableArrayList(EXTRA_CUSTOM_TABS_MENU_ITEMS, menuItemBundleList);
Настройка собственной анимации входа и выхода
    Многие приложения под Android используют собственную анимацию перехода между разными активити. Клиентские табы Chrome не являются исключением. Вы можете задавать свои анимации входа и выхода (т.е. когда пользователь нажимает “Назад”) для сохранения общего стиля анимации во всём приложении.
// Optional. Bundle constructed out of
ActivityOptions that Chrome will be running when
// it finishes CustomTabActivity. If you start the Custom Tab with 
// a customized animation, you can specify a matching animation when Custom Tab 
// returns to your app.

public static final String EXTRA_CUSTOM_TABS_EXIT_ANIMATION_BUNDLE =
"android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
Bundle finishBundle = ActivityOptions.makeCustomAnimation(context, R.anim.clientEnterAnimResId, R.anim.CustomTabExitAnimResId).toBundle; 
intent.putExtra(EXTRA_CUSTOM_TABS_EXIT_ANIMATION_BUNDLE, finishBundle);
Bundle startBundle = ActivityOptions.makeCustomAnimation(context, R.anim.CustomTabEnterAnimResId, R.anim.clientExitAnimResId).toBundle; 
startActivity(intent, startBundle);
Предварительная подготовка Chrome для ускорения загрузки страниц
    Обычно, когда вызывается startActivity с заданным ACTION_VIEW Intent, происходит инициализация Chrome и затем обработка URL. Это занимает драгоценное время и негативно сказывается на плавности и общем восприятии поведения. Мы считаем, что пользователи хотят мгновенных результатов по своим запросам, потому сделали в Chrome сервис, к которому можно подключиться приложением и заставить Хром инициализировать движок и нативные компоненты. Также мы экспериментируем с тем, какие возможности предоставлять через сервис и, к примеру, разработчики могут передать Хрому набор веб страниц, которые пользователь может посетить. Хром же выполнит следующие действия:
  • Заранее разрезолвит DNS основного домена
  • Заранее разрезолвит DNS наиболее подходящих ресурсов домена
  • Заранее  установит подключение к целевому ресурсу, включая HTTPS/TSL
    Процесс предварительной подготовки Chrome включает в себя:
  • Подключение к сервису
  • Прикрепление колбэка навигации с событием finishSetup, которое будет означать, что страница уже загрузилась
  • Вызов warmup из сервиса для запуска Chrome в фоне
  • Создание newSession. Эта сессия будет использоваться для всех запросов в API
  • Сообщение Хрому, какие страницы пользователь, возможно, пожелает посетить, средствами mayLaunchUrl
  • Запуск VIEW Intent с ID сессии
Подключение к сервису Chrome
    Если вы не очень хорошо знакомы с тем, как подключаться к сервисам в Android, то для вас есть готовый интерфейс, созданный с AIDL, который автоматически создаст класс прокси для сервиса. AIDL, который определяет сервис, можно найти в нашем примере на GitHab.
// Package name for the Chrome channel the client wants to connect to. This
// depends on the channel name.
// Stable = com.android.chrome
// Beta = com.chrome.beta
// Dev = com.chrome.dev
public static final String CUSTOM_TAB_PACKAGE_NAME = "com.chrome.dev";  // Change when in stable

// Action to add to the service intent. This action can be used as a way 
// generically pick apps that handle custom tabs for both activity and service 
// side implementations.
public static final String ACTION_CUSTOM_TABS_CONNECTION =
       "android.support.customtabs.action.CustomTabsService";
Intent serviceIntent = new Intent(ACTION_CUSTOM_TABS_CONNECTION);

serviceIntent.setPackage(CUSTOM_TAB_PACKAGE_NAME);
context.bindService(serviceIntent, mServiceConnection,
                    Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY);
Инициализация Chrome
    boolean warmup(long flags)    Инициализирует процесс браузера и загружает нативные библиотеки. Инициализация происходит асинхронно и значение возвращается, как только запрос может быть принят. Возвращает True в случае успеха. Множественные успешные вызовы также будут возвращать True.
Создание новой сессии таба
    boolean newSession(ICustomTabsCallback callback)
    Сессия используется для последующих вызовов ссылок из mayLaunchUrl для установки связи между VIEW интентом и созданным табом. Используемый здесь интерфейс обратного вызова связан с созданной сессией и должен передаваться во все последующие вызовы mayLaunchUrl. Любые изменения для созданной сессии (смотри ниже “Обратный вызов в клиентских табах”) также передаются через этот интерфейс. Возвращает, была ли сессия создана успешно. Множественные вызовы с одинаковым ICustomTabsCallback или нулевым значением возвращают False.
Сообщение Хрому URL, который пользователь, возможно, пожелает открыть
    boolean mayLaunchUrl(ICustomTabsCallback sessionCallback, Uri url, Bundle extras,ListotherLikelyBundles)
    Сообщает браузеру URL, которые потенциально могут быть открыты в будущем. Настоятельно рекомендуем в первую очередь делать вызов метода warmup(). В первую очередь нужно сообщить наиболее вероятный URL. Дополнительно можно передать список вероятных URL. Они будут обработаны с меньшим приоритетом и их нужно отсортировать по уменьшению важности. Дополнительные URL могут быть проигнорированы. Все предыдущие вызовы этого метода будут понижены в приоритете. Возвращает успешность завершения операции.
Обратный вызов подключения в клиентских табах
    void onNavigationEvent(int navigationEvent, Bundle extras)
    Будет вызван, если произойдёт какое-либо событие в клентском табе. “NavigationEvent int” – это одно из шести значений, которые определены для состояний страницы в табе.
/**
* Sent when the tab has started loading a page.
*/
public static final int NAVIGATION_STARTED = 1;

/**
* Sent when the tab has finished loading a page.
*/
public static final int NAVIGATION_FINISHED = 2;

/**
* Sent when the tab couldn't finish loading due to a failure.
*/
public static final int NAVIGATION_FAILED = 3;

/**
* Sent when loading was aborted by a user action before it finishes like clicking on a link
* or refreshing the page.
*/
public static final int NAVIGATION_ABORTED = 4;

/**
* Sent when the tab becomes visible.
*/
public static final int TAB_SHOWN = 5;

/**
* Sent when the tab becomes hidden.
*/
public static final int TAB_HIDDEN = 6;
Использование библиотеки поддержки с клиентской стороны
Смотри пример приложения.

FAQ:

  • Есть чё для iOS?
    • Нет. Вот этот проект позволит вам интегрировать Chrome в ваше приложение, но совсем другим путём
  • Когда это добро будет доступно на стабильном канале Хрома?
    • Уже давно
  • Где можно задавать вопросы?