PostMessage para TWA

Sayed El-Abady
Sayed El-Abady

A partir de Chrome 115, las Actividades web de confianza (TWA) pueden enviar mensajes con postMessage. En este documento, se explica la configuración necesaria para la comunicación entre tu app y la Web.

Al final de esta guía, podrás hacer lo siguiente:

  • Comprende cómo funciona la validación del cliente y del contenido web.
  • Saber cómo inicializar el canal de comunicación entre el cliente y el contenido web
  • Saber cómo enviar y recibir mensajes de contenido web

Para seguir esta guía, necesitarás lo siguiente:

  • Para agregar la biblioteca androidx.browser más reciente (v1.6.0-alpha02 como mínimo) a tu archivo build.gradle.
  • Chrome 115.0.5790.13 o versiones posteriores para TWA

El método window.postMessage() habilita de forma segura la comunicación entre orígenes de los objetos Window. Por ejemplo, entre una página y una ventana emergente que generó, o entre una página y un iframe incorporado en ella.

Por lo general, las secuencias de comandos de diferentes páginas pueden acceder entre sí solo si las páginas provienen del mismo origen, comparten el mismo protocolo, número de puerto y host (también conocido como política del mismo origen). El método window.postMessage() proporciona un mecanismo controlado para comunicarse de forma segura entre diferentes orígenes. Esto puede ser útil para implementar aplicaciones de chat, herramientas de colaboración y otras. Por ejemplo, una aplicación de chat podría usar postMessage para enviar mensajes entre usuarios que se encuentran en diferentes sitios web. El uso de postMessage en Actividades web de confianza (TWA) puede ser un poco complicado. En esta guía, se explica cómo usar postMessage en el cliente de TWA para enviar y recibir mensajes desde la página web.

Agrega la app a la validación web

La API de postMessage permite que dos orígenes válidos se comuniquen entre sí, una fuente y un origen de destino. Para que la aplicación para Android pueda enviar mensajes al origen de destino, debe declarar a qué origen de fuente es equivalente. Esto se puede hacer con los vínculos de recursos digitales (DAL) agregando el nombre del paquete de la app en tu archivo assetlinks.json con la relación use_as_origin, de modo que quede de la siguiente manera:

[{
  "relation": ["delegate_permission/common.use_as_origin"],
  "target" : { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [""] }
}]

Ten en cuenta que, en la configuración del origen asociado con el TWA, se debe proporcionar un origen para el campo MessageEvent.origin, pero se puede usar postMessage para comunicarse con otros sitios que no incluyen el vínculo de recursos digitales. Por ejemplo, si eres propietario de www.example.com, deberás demostrarlo a través de DAL, pero puedes comunicarte con cualquier otro sitio web, por ejemplo, www.wikipedia.org.

Agrega PostMessageService a tu manifiesto

Para recibir la comunicación de postMessage, debes configurar el servicio. Para ello, agrega PostMessageService a tu manifiesto de Android:

<service android:name="androidx.browser.customtabs.PostMessageService"
android:exported="true"/>

Obtén una instancia de CustomTabsSession

Después de agregar el servicio al manifiesto, usa la clase CustomTabsClient para vincularlo. Una vez conectado, puedes usar el cliente proporcionado para crear una sesión nueva de la siguiente manera. CustomTabsSession es la clase principal para controlar la API de postMessage. En el siguiente código, se muestra cómo, una vez que se conecta el servicio, se usa el cliente para crear una sesión nueva, que se usa para postMessage:

private CustomTabsClient mClient;
private CustomTabsSession mSession;

// We use this helper method to return the preferred package to use for
// Custom Tabs.
String packageName = CustomTabsClient.getPackageName(this, null);

// Binding the service to (packageName).
CustomTabsClient.bindCustomTabsService(this, packageName, new CustomTabsServiceConnection() {
 @Override
 public void onCustomTabsServiceConnected(@NonNull ComponentName name,
     @NonNull CustomTabsClient client) {
   mClient = client;

   // Note: validateRelationship requires warmup to have been called.
   client.warmup(0L);

   mSession = mClient.newSession(customTabsCallback);
 }

 @Override
 public void onServiceDisconnected(ComponentName componentName) {
   mClient = null;
 }
});

Ahora te preguntarás qué es esta instancia de customTabsCallback, ¿no? Lo crearemos en la siguiente sección.

Crea CustomTabsCallback

CustomTabsCallback es una clase de devolución de llamada para que CustomTabsClient reciba mensajes sobre eventos en sus pestañas personalizadas. Uno de estos eventos es onPostMessage, al que se llama cuando la app recibe un mensaje de la Web. Agrega la devolución de llamada al cliente para inicializar el canal postMessage y comenzar la comunicación, como se muestra en el siguiente código.

private final String TAG = "TWA/CCT-PostMessageDemo";

// The origin the TWA is equivalent to, where the Digital Asset Links file
// was created with the "use_as_origin" relationship.
private Uri SOURCE_ORIGIN = Uri.parse("https://source-origin.example.com");

// The origin the TWA will communicate with. In most cases, SOURCE_ORIGIN and
// TARGET_ORIGIN will be the same.
private Uri TARGET_ORIGIN = Uri.parse("https://target-origin.example.com");

// It stores the validation result so you can check on it before requesting
// postMessage channel, since without successful validation it is not possible
// to use postMessage.
boolean mValidated;

CustomTabsCallback customTabsCallback = new CustomTabsCallback() {

    // Listens for the validation result, you can use this for any kind of
    // logging purposes.
    @Override
    public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
        boolean result, @Nullable Bundle extras) {
        // If this fails:
        // - Have you called warmup?
        // - Have you set up Digital Asset Links correctly?
        // - Double check what browser you're using.
        Log.d(TAG, "Relationship result: " + result);
        mValidated = result;
    }

    // Listens for any navigation happens, it waits until the navigation finishes
    // then requests post message channel using
    // CustomTabsSession#requestPostMessageChannel(sourceUri, targetUri, extrasBundle)

    // The targetOrigin in requestPostMessageChannel means that you can be certain their messages are delivered only to the website you expect.
    @Override
    public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
        if (navigationEvent != NAVIGATION_FINISHED) {
            return;
        }

        if (!mValidated) {
            Log.d(TAG, "Not starting PostMessage as validation didn't succeed.");
        }

        // If this fails:
        // - Have you included PostMessageService in your AndroidManifest.xml ?
        boolean result = mSession.requestPostMessageChannel(SOURCE_ORIGIN, TARGET_ORIGIN, new Bundle());
        Log.d(TAG, "Requested Post Message Channel: " + result);
    }

    // This gets called when the channel we requested is ready for sending/receiving messages.
    @Override
    public void onMessageChannelReady(@Nullable Bundle extras) {
        Log.d(TAG, "Message channel ready.");

        int result = mSession.postMessage("First message", null);
        Log.d(TAG, "postMessage returned: " + result);
    }

    // Listens for upcoming messages from Web.
    @Override
    public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
        super.onPostMessage(message, extras);
        // Handle the received message.
    }
};

Cómo comunicarse desde la Web

Ahora podemos enviar y recibir mensajes desde nuestra app de host. ¿Cómo hacemos lo mismo desde la Web? La comunicación debe comenzar desde la app host y, luego, la página web debe obtener el puerto del primer mensaje. Este puerto se usa para la comunicación de vuelta. Tu archivo JavaScript se verá como el siguiente ejemplo:

window.addEventListener("message", function (event) {
  // We are receiveing messages from any origin, you can check of the origin by
  // using event.origin

  // get the port then use it for communication.
  var port = event.ports[0];
  if (typeof port === 'undefined') return;

  // Post message on this port.
  port.postMessage("Test")

  // Receive upcoming messages on this port.
  port.onmessage = function(event) {
    console.log("[PostMessage1] Got message" + event.data);
  };
});

Puedes encontrar un ejemplo completo aquí.

Foto de Joanna Kosinska en Unsplash