Cómo usar pestañas personalizadas con Android 11

Android 11 introdujo cambios en la forma en que las apps pueden interactuar con otras que el usuario instaló en el dispositivo. Puedes obtener más información sobre esos cambios en la documentación de Android.

Cuando una app para Android que usa pestañas personalizadas se orienta al nivel de SDK 30 o versiones posteriores, es posible que se deban realizar algunos cambios. En este artículo, se describen los cambios que pueden ser necesarios para esas apps.

En el caso más simple, las pestañas personalizadas se pueden iniciar con una línea como la siguiente:

new CustomTabsIntent.Builder().build()
        .launchUrl(this, Uri.parse("https://www.example.com"));

Las aplicaciones que inician aplicaciones con este enfoque, o incluso agregan personalizaciones de la IU, como cambiar el color de la barra de herramientas o agregar un botón de acción, no necesitarán realizar ningún cambio en la aplicación.

Cómo preferir las apps nativas

Sin embargo, si seguiste las prácticas recomendadas, es posible que debas realizar algunos cambios.

La primera práctica recomendada relevante es que las aplicaciones deben preferir una app nativa para controlar el intent en lugar de una pestaña personalizada si se instaló una app que puede controlarlo.

En Android 11 y versiones posteriores

Android 11 presenta una nueva marca de intent, FLAG_ACTIVITY_REQUIRE_NON_BROWSER, que es la forma recomendada de intentar abrir una app nativa, ya que no requiere que la app declare ninguna consulta del administrador de paquetes.

static boolean launchNativeApi30(Context context, Uri uri) {
    Intent nativeAppIntent = new Intent(Intent.ACTION_VIEW, uri)
            .addCategory(Intent.CATEGORY_BROWSABLE)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER);
    try {
        context.startActivity(nativeAppIntent);
        return true;
    } catch (ActivityNotFoundException ex) {
        return false;
    }
}

La solución es intentar iniciar el intent y usar FLAG_ACTIVITY_REQUIRE_NON_BROWSER para pedirle a Android que evite los navegadores durante el inicio.

Si no se encuentra una app nativa que pueda controlar este intent, se arrojará una ActivityNotFoundException.

Antes de Android 11

Aunque la aplicación pueda orientarse a Android 11 o al nivel de API 30, las versiones anteriores de Android no comprenderán la marca FLAG_ACTIVITY_REQUIRE_NON_BROWSER, por lo que debemos recurrir a consultar el Administrador de paquetes en esos casos:

private static boolean launchNativeBeforeApi30(Context context, Uri uri) {
    PackageManager pm = context.getPackageManager();

    // Get all Apps that resolve a generic url
    Intent browserActivityIntent = new Intent()
            .setAction(Intent.ACTION_VIEW)
            .addCategory(Intent.CATEGORY_BROWSABLE)
            .setData(Uri.fromParts("http", "", null));
    Set<String> genericResolvedList = extractPackageNames(
            pm.queryIntentActivities(browserActivityIntent, 0));

    // Get all apps that resolve the specific Url
    Intent specializedActivityIntent = new Intent(Intent.ACTION_VIEW, uri)
            .addCategory(Intent.CATEGORY_BROWSABLE);
    Set<String> resolvedSpecializedList = extractPackageNames(
            pm.queryIntentActivities(specializedActivityIntent, 0));

    // Keep only the Urls that resolve the specific, but not the generic
    // urls.
    resolvedSpecializedList.removeAll(genericResolvedList);

    // If the list is empty, no native app handlers were found.
    if (resolvedSpecializedList.isEmpty()) {
        return false;
    }

    // We found native handlers. Launch the Intent.
    specializedActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(specializedActivityIntent);
    return true;
}

El enfoque que se usa aquí es consultar el Administrador de paquetes para obtener aplicaciones que admitan un intent http genérico. Es probable que esas aplicaciones sean navegadores.

Luego, consulta las aplicaciones que controlan los elementos de la URL específica que queremos iniciar. Esto mostrará los navegadores y las aplicaciones nativas configurados para controlar esa URL.

Ahora, quita todos los navegadores que se encuentran en la primera lista de la segunda, y solo nos quedarán las apps nativas.

Si la lista está vacía, sabemos que no hay controladores nativos y devolvemos un valor falso. De lo contrario, iniciamos el intent para el controlador nativo.

Revisión general

Debemos asegurarnos de usar el método correcto para cada ocasión:

static void launchUri(Context context, Uri uri) {
    boolean launched = Build.VERSION.SDK_INT >= 30 ?
            launchNativeApi30(context, uri) :
            launchNativeBeforeApi30(context, uri);

    if (!launched) {
        new CustomTabsIntent.Builder()
                .build()
                .launchUrl(context, uri);
    }
}

Build.VERSION.SDK_INT proporciona la información que necesitamos. Si es igual o superior a 30, Android conoce el FLAG_ACTIVITY_REQUIRE_NON_BROWSER y podemos intentar iniciar una app nativa con el nuevo enfoque. De lo contrario, intentaremos iniciar con el enfoque anterior.

Si falla el inicio de una app nativa, iniciamos una pestaña personalizada.

Esta práctica recomendada incluye texto de plantilla. Estamos trabajando para simplificar esto encapsulando la complejidad en una biblioteca. No te pierdas las actualizaciones de la biblioteca de compatibilidad android-browser-helper.

Cómo detectar navegadores que admiten Custom Tabs

Otro patrón común es usar PackageManager para detectar qué navegadores admiten Pestañas personalizadas en el dispositivo. Algunos casos de uso comunes para esto son configurar el paquete en el intent para evitar el diálogo de desambiguación de la app o elegir a qué navegador conectarse cuando te conectas al servicio de pestañas personalizadas.

Cuando se segmente para el nivel de API 30, los desarrolladores deberán agregar una sección de consultas a su manifiesto de Android y declarar un filtro de intents que coincida con los navegadores compatibles con Custom Tabs.

<queries>
    <intent>
        <action android:name=
            "android.support.customtabs.action.CustomTabsService" />
    </intent>
</queries>

Con el marcado implementado, el código existente que se usa para consultar navegadores que admiten pestañas personalizadas funcionará como se espera.

Preguntas frecuentes

P.: El código que busca proveedores de pestañas personalizadas busca aplicaciones que puedan controlar intents https://, pero el filtro de consulta solo declara una consulta android.support.customtabs.action.CustomTabsService. ¿No se debe declarar una consulta para los intents https://?

A: Cuando se declara un filtro de consulta, se filtran las respuestas a una consulta en PackageManager, no la consulta en sí. Dado que los navegadores que admiten pestañas personalizadas declaran el manejo de CustomTabsService, no se filtrarán. Se filtrarán los navegadores que no admitan las pestañas personalizadas.

Conclusión

Esos son todos los cambios necesarios para adaptar una integración existente de Custom Tabs para que funcione con Android 11. Para obtener más información sobre cómo integrar pestañas personalizadas en una app para Android, comienza con la guía de implementación y, luego, consulta las prácticas recomendadas para obtener información sobre cómo compilar una integración de primer nivel.

Comunícate con nosotros si tienes preguntas o comentarios.