chromium/content/public/android/java/src/org/chromium/content/browser/AppWebMessagePort.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Pair;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content_public.browser.MessagePayload;
import org.chromium.content_public.browser.MessagePort;

/**
 * Represents the MessageChannel MessagePort object. Inspired from
 * http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#message-channels
 *
 * State management:
 *
 * A message port can be in transferred state while a transfer is pending or complete. An
 * application cannot use a transferred port to post messages. If a transferred port
 * receives messages, they will be queued. This state is not visible to embedder app.
 *
 * A message port should be closed by the app when it is not needed any more. This will free
 * any resources used by it. A closed port cannot receive/send messages and cannot be transferred.
 * close() can be called multiple times. A transferred port cannot be closed by the application,
 * since the ownership is also transferred during the transfer. Closing a transferred port will
 * throw an exception.
 *
 * All methods are called on the UI thread, except for MessageHandler.handleMessage, which is
 * used to dispatch messages on a potentially separate thread.
 *
 * Restrictions:
 * The HTML5 message protocol is very flexible in transferring ports. However, this
 * sometimes leads to surprising behavior. For example, in current version of chrome (m41)
 * the code below
 *  1.  var c1 = new MessageChannel();
 *  2.  var c2 = new MessageChannel();
 *  3.  c1.port2.onmessage = function(e) { console.log("1"); }
 *  4.  c2.port2.onmessage = function(e) {
 *  5.     e.ports[0].onmessage = function(f) {
 *  6.          console.log("3");
 *  7.      }
 *  8.  }
 *  9.  c1.port1.postMessage("test");
 *  10. c2.port1.postMessage("test2",[c1.port2])
 *
 * prints 1 or 3 depending on whether or not line 10 is included in code. Further if
 * it gets executed with a timeout, depending on timeout value, the printout value
 * changes.
 *
 * To prevent such problems, this implementation limits the transfer of ports
 * as below:
 * A port is put to a "started" state if:
 * 1. The port is ever used to post a message, or
 * 2. The port was ever registered a handler to receive a message.
 * A started port cannot be transferred.
 *
 * This restriction should not impact postmessage functionality in a big way,
 * because an app can still create as many channels as it wants to and use it for
 * transferring data. As a return, it simplifies implementation and prevents hard
 * to debug, racy corner cases while receiving/sending data.
 *
 * This object is not thread safe but public methods may be called from any thread.
 */
@JNINamespace("content::android")
public class AppWebMessagePort implements MessagePort {
    private static final String TAG = "AppWebMessagePort";

    private static class MessageHandler extends Handler {
        // The |what| value for handleMessage.
        private static final int MESSAGE_RECEIVED = 1;

        @NonNull private final MessageCallback mMessageCallback;

        MessageHandler(@NonNull MessageCallback callback, @Nullable Handler handler) {
            super(handler == null ? Looper.getMainLooper() : handler.getLooper());
            mMessageCallback = callback;
        }

        @Override
        public void handleMessage(@NonNull final Message msg) {
            if (msg.what == MESSAGE_RECEIVED) {
                final Pair<MessagePayload, MessagePort[]> obj =
                        (Pair<MessagePayload, MessagePort[]>) msg.obj;
                mMessageCallback.onMessage(obj.first, obj.second);
                return;
            }
            throw new IllegalStateException("undefined message");
        }

        @MainThread
        public void onMessage(final MessagePayload messagePayload, final MessagePort[] sentPorts) {
            ThreadUtils.assertOnUiThread();
            sendMessage(obtainMessage(MESSAGE_RECEIVED, Pair.create(messagePayload, sentPorts)));
        }
    }

    // Accessed on UI thread only.
    private long mNativeAppWebMessagePort;
    private MessageHandler mMessageHandler;

    // Can be accessed from any thread, client needs to keep thread safe. Need volatile since they
    // may be accessed concurrently from UI thread and client thread, which may be different.
    private volatile boolean mClosed;
    private volatile boolean mTransferred;
    private volatile boolean mStarted;

    @MainThread
    @CalledByNative
    private AppWebMessagePort(long nativeAppWebMessagePort) {
        mNativeAppWebMessagePort = nativeAppWebMessagePort;
    }

    // Called to create an entangled pair of ports.
    @MainThread
    public static AppWebMessagePort[] createPair() {
        return AppWebMessagePortJni.get().createPair();
    }

    @Override
    public void close() {
        if (isTransferred()) {
            throw new IllegalStateException("Port is already transferred");
        }
        if (isClosed()) return;
        mClosed = true;
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    if (mNativeAppWebMessagePort == 0L) return;
                    AppWebMessagePortJni.get().closeAndDestroy(mNativeAppWebMessagePort);
                });
    }

    @Override
    public boolean isClosed() {
        return mClosed;
    }

    @Override
    public boolean isTransferred() {
        return mTransferred;
    }

    @Override
    public boolean isStarted() {
        return mStarted;
    }

    @Override
    public void setMessageCallback(MessageCallback messageCallback, Handler handler) {
        if (isClosed() || isTransferred()) {
            throw new IllegalStateException("Port is already closed or transferred");
        }
        mStarted = true;
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    if (mNativeAppWebMessagePort == 0L) return;
                    mMessageHandler =
                            messageCallback == null
                                    ? null
                                    : new MessageHandler(messageCallback, handler);
                    AppWebMessagePortJni.get()
                            .setShouldReceiveMessages(
                                    mNativeAppWebMessagePort, messageCallback != null);
                });
    }

    @Override
    public void postMessage(MessagePayload messagePayload, MessagePort[] sentPorts)
            throws IllegalStateException {
        if (isClosed() || isTransferred()) {
            throw new IllegalStateException("Port is already closed or transferred");
        }
        if (sentPorts != null) {
            for (MessagePort port : sentPorts) {
                if (port.equals(this)) {
                    throw new IllegalStateException("Source port cannot be transferred");
                }
                if (port.isClosed() || port.isTransferred()) {
                    throw new IllegalStateException("Port is already closed or transferred");
                }
                if (port.isStarted()) {
                    throw new IllegalStateException("Port is already started");
                }
                // It's safe to cast since AppWebMessagePort is the only impl.
                // Port may be transferred over other MessageChannels (Like
                // WebContents#PostMessageToMainFrame).
                ((AppWebMessagePort) port).setTransferred();
            }
        }
        mStarted = true;
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    if (mNativeAppWebMessagePort == 0L) return;
                    AppWebMessagePortJni.get()
                            .postMessage(mNativeAppWebMessagePort, messagePayload, sentPorts);
                });
    }

    /**
     * A finalizer is required to ensure that the native object associated with
     * this descriptor gets torn down, otherwise there would be a memory leak.
     *
     * This is safe because posting a task is fast.
     *
     * TODO(chrisha): Chase down the existing offenders that don't call close,
     * and flip this to use LifetimeAssert.
     *
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize() throws Throwable {
        try {
            if (mNativeAppWebMessagePort == 0L) return;
            Log.d(TAG, "AppWebMessagePort was not closed before finalization");
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        if (mNativeAppWebMessagePort == 0L) return;
                        mClosed = true;
                        AppWebMessagePortJni.get().closeAndDestroy(mNativeAppWebMessagePort);
                    });
        } finally {
            super.finalize();
        }
    }

    @MainThread
    @CalledByNative
    private long getNativeObj() {
        ThreadUtils.assertOnUiThread();
        assert mNativeAppWebMessagePort != 0L;
        return mNativeAppWebMessagePort;
    }

    @MainThread
    @CalledByNative
    private void onMessage(@NonNull MessagePayload payload, @Nullable MessagePort[] ports) {
        ThreadUtils.assertOnUiThread();
        if (mMessageHandler != null) {
            mMessageHandler.onMessage(payload, ports);
        } else {
            // Their will be a case that the Java listener is cleared, but listeners in C++ is not
            // cleared yet. We can safely ignore those messages and close the ports to avoid
            // relaying on GC.
            if (ports != null) {
                for (final MessagePort port : ports) {
                    port.close();
                }
            }
        }
    }

    // Called when native object is destroyed.
    @MainThread
    @CalledByNative
    private void nativeDestroyed() {
        ThreadUtils.assertOnUiThread();
        assert mNativeAppWebMessagePort != 0L;
        // When calling this method, the port must be closed or transferred.
        assert mClosed || mTransferred;
        mNativeAppWebMessagePort = 0L;
    }

    // Called when MessagePort is transferred. The native object will be destroyed later.
    @CalledByNative
    private void setTransferred() {
        assert !mStarted;
        mTransferred = true;
    }

    @NativeMethods
    @MainThread
    interface Natives {
        @NonNull
        AppWebMessagePort[] createPair();

        void postMessage(
                long nativeAppWebMessagePort,
                MessagePayload messagePayload,
                MessagePort[] sentPorts);

        void setShouldReceiveMessages(long nativeAppWebMessagePort, boolean shouldReceiveMessage);

        void closeAndDestroy(long nativeAppWebMessagePort);
    }
}