TWA 用の PostMessage

Sayed El-Abady
Sayed El-Abady

Chrome 115 以降、Trusted Web Activity(TWA) は postMessage を使用してメッセージを送信できるようになりました。このドキュメントでは、アプリとウェブとの間で通信するために必要な設定について説明します。

このガイドを読み終えると、次のことができるようになります。

  • クライアントとウェブ コンテンツの検証の仕組みを理解する。
  • クライアントとウェブコンテンツ間の通信チャネルを初期化する方法を学びます。
  • ウェブコンテンツにメッセージを送信したり、ウェブコンテンツからメッセージを受信したりする方法について学びます。

このガイドに沿って操作するには、次のものが必要です。

  • 最新の androidx.browser(v1.6.0-alpha02 以降)ライブラリを build.gradle ファイルに追加します。
  • TWA の場合は Chrome バージョン 115.0.5790.13 以降。

window.postMessage() メソッドを使用すると、Window オブジェクト間のクロスオリジン通信を安全に有効にできます。たとえば、ページとそのページが生成したポップアップの間、またはページとそのページに埋め込まれた iframe の間などです。

通常、異なるページのスクリプトが相互にアクセスできるのは、ページが同じオリジンから発信され、同じプロトコル、ポート番号、ホストを共有している場合のみです(同じオリジン ポリシーとも呼ばれます)。window.postMessage() メソッドは、異なるオリジン間で安全に通信するための制御されたメカニズムを提供します。これは、チャット アプリケーションやコラボレーション ツールの実装などに役立ちます。たとえば、チャット アプリケーションで postMessage を使用して、異なるウェブサイトのユーザー間でメッセージを送信できます。Trusted Web Activity(TWA)postMessage を使用するのは少し難しい場合があります。このガイドでは、TWA クライアントで postMessage を使用してウェブページにメッセージを送信し、ウェブページからメッセージを受信する方法について説明します。

ウェブ検証にアプリを追加する

postMessage API を使用すると、2 つの有効なオリジン(ソースとターゲット)が相互に通信できます。Android アプリがターゲット送信元にメッセージを送信できるようにするには、同等の送信元を宣言する必要があります。これは、Digital Asset Links(DAL)を使用して、アプリのパッケージ名assetlinks.json ファイルにリレーション use_as_origin として追加することで行えます。これにより、次のようなようになります。

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

TWA に関連付けられたオリジンで設定する場合は、MessageEvent.origin フィールドにオリジンを指定する必要がありますが、postMessage を使用して、デジタル アセット リンクを含まない他のサイトと通信できます。たとえば、www.example.com を所有している場合は、DAL を通じてそのことを証明する必要がありますが、www.wikipedia.org などの他のウェブサイトと通信することは可能です。

PostMessageService をマニフェストに追加する

postMessage 通信を受信するには、サービスを設定する必要があります。設定するには、Android マニフェストに PostMessageService を追加します。

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

CustomTabsSession インスタンスを取得する

サービスをマニフェストに追加したら、CustomTabsClient クラスを使用してサービスをバインドします。接続したら、提供されたクライアントを使用して、次のように新しいセッションを作成できます。CustomTabsSession は、postMessage API を処理するためのコアクラスです。次のコードは、サービスが接続されたら、クライアントを使用して新しいセッションを作成し、このセッションを使用して 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;
 }
});

この customTabsCallback インスタンスとは何でしょうか?次のセクションで作成します。

CustomTabsCallback を作成する

CustomTabsCallback は、CustomTabsClient がカスタムタブのイベントに関するメッセージを受け取るためのコールバック クラスです。これらのイベントの 1 つが onPostMessage で、アプリがウェブからメッセージを受信すると呼び出されます。次のコードに示すように、クライアントにコールバックを追加して postMessage チャネルを初期化し、通信を開始します。

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.
    }
};

ウェブからの通信

ホストアプリからメッセージを送受信できるようになりましたが、ウェブから同じことをするにはどうすればよいですか?通信はホストアプリから開始し、ウェブページは最初のメッセージからポートを取得する必要があります。このポートは、返信の通信に使用されます。JavaScript ファイルは次のようになります。

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);
  };
});

完全なサンプルはこちらで確認できます。

写真提供: Joanna KosinskaUnsplash