Cómo usar pestañas personalizadas con Android 11

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

Cuando una app para Android que usa pestañas personalizadas tiene como objetivo el nivel de SDK 30 o uno superior, es posible que se necesiten algunos cambios. En este artículo, se describen los cambios que podrían ser necesarios para esas apps.

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

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

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

Prefiero 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 instala una app que puede manejarlo.

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 al 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 puede controlar este intent, se arrojará una ActivityNotFoundException.

Antes de Android 11

Aunque la aplicación puede 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 al Administrador de paquetes en busca de aplicaciones que admitan un intent http genérico. Esas aplicaciones son probablemente navegadores.

Luego, busca aplicaciones que manejen los itinerarios 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 encuentren en la primera lista de la segunda, y solo nos quedaremos con las aplicaciones nativas.

Si la lista está vacía, sabemos que no hay controladores nativos y se muestra el 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 mayor que 30, Android conoce el FLAG_ACTIVITY_REQUIRE_NON_BROWSER y podemos intentar lanzar una app nativa con el enfoque nuevo. De lo contrario, intentamos realizar el lanzamiento con el enfoque anterior.

Si falla el lanzamiento de una aplicación nativa, se inicia una pestaña personalizada.

Hay algo de código estándar en esta práctica recomendada. Estamos trabajando para simplificar este proceso mediante el encapsulamiento de la complejidad en una biblioteca. No te pierdas las actualizaciones de la biblioteca de compatibilidad de android-browser-helper.

Cómo detectar navegadores compatibles con pestañas personalizadas

Otro patrón común es usar PackageManager para detectar qué navegadores admiten pestañas personalizadas en el dispositivo. Algunos casos de uso comunes 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 los desarrolladores tengan como objetivo el nivel de API 30, 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 pestañas personalizadas.

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

Una vez que hayas implementado el lenguaje de marcado, el código existente que se usa para buscar navegadores que admiten pestañas personalizadas funcionará como se espera.

Preguntas frecuentes

P.: El código que busca pestañas personalizadas presta consultas a aplicaciones que pueden controlar intents https://, pero el filtro solo declara una consulta android.support.customtabs.action.CustomTabsService. ¿No se debería declarar una consulta para intents https://?

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

Conclusión

Esos son todos los cambios necesarios para adaptar una integración existente de pestañas personalizadas de modo que funcione con Android 11. Si quieres obtener más información para 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 compilar una integración de primer nivel.

Comunícate con nosotros si tienes preguntas o comentarios.