chromium/chrome/android/webapk/libs/client/src/org/chromium/webapk/lib/client/WebApkServiceConnectionManager.java

// Copyright 2016 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.webapk.lib.client;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;

import org.chromium.base.Callback;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.Callable;

/**
 * Each WebAPK has several services. This class manages static global connections between the Chrome
 * application and the "WebAPK services."
 */
public class WebApkServiceConnectionManager {
    /** Interface for getting notified once Chrome is connected to a WebAPK service. */
    public interface ConnectionCallback {
        /**
         * Called once Chrome is connected to the WebAPK service.
         *
         * @param service The WebAPK service.
         */
        void onConnected(IBinder service);
    }

    /** Managed connection to WebAPK service. */
    private static class Connection implements ServiceConnection {
        /** The connection manager who owns this connection. */
        private WebApkServiceConnectionManager mConnectionManager;

        /** Callbacks to call once the connection is established. */
        private ArrayList<ConnectionCallback> mCallbacks = new ArrayList<>();

        /** WebAPK IBinder interface. */
        private IBinder mBinder;

        public Connection(WebApkServiceConnectionManager manager) {
            mConnectionManager = manager;
        }

        public IBinder getService() {
            return mBinder;
        }

        public void addCallback(ConnectionCallback callback) {
            mCallbacks.add(callback);
        }

        public boolean didAllCallbacksRun() {
            return mCallbacks.isEmpty();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mBinder = null;
            mConnectionManager.onServiceDisconnected(name.getPackageName());
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinder = service;
            Log.d(TAG, String.format("Got IBinder Service: %s", mBinder));
            for (ConnectionCallback callback : mCallbacks) {
                callback.onConnected(mBinder);
            }
            mCallbacks.clear();
        }
    }

    private static final String TAG = "WebApkService";

    /** The category of the service to connect to. */
    private String mCategory;

    /** The action of the service to connect to. */
    private String mAction;

    private @TaskTraits int mUiThreadTaskTraits;

    private TaskRunner mTaskRunner;

    /** Number of tasks posted via {@link #postTaskAndReply()} whose reply has not yet been run. */
    private int mNumPendingPostedTasks;

    /** Mapping of WebAPK package to WebAPK service connection. */
    private HashMap<String, Connection> mConnections = new HashMap<>();

    public WebApkServiceConnectionManager(
            @TaskTraits int uiThreadTaskTraits, String category, String action) {
        mUiThreadTaskTraits = uiThreadTaskTraits;
        mCategory = category;
        mAction = action;
    }

    /** Called when a WebAPK service connection is disconnected. */
    private void onServiceDisconnected(String webApkName) {
        mConnections.remove(webApkName);
        if (mConnections.isEmpty() && mNumPendingPostedTasks == 0) {
            destroyTaskRunner();
        }
    }

    /** Returns whether the callbacks for all of the {@link #connect()} calls have been run. */
    public boolean didAllConnectCallbacksRun() {
        for (Connection connection : mConnections.values()) {
            if (!connection.didAllCallbacksRun()) return false;
        }
        return true;
    }

    /**
     * Connects Chrome application to WebAPK service. Can be called from any thread.
     *
     * @param appContext Application context.
     * @param webApkPackage WebAPK package to create connection for.
     * @param callback Callback to call after connection has been established. Called synchronously
     */
    @SuppressLint("StaticFieldLeak")
    public void connect(
            final Context appContext, final String webApkPackage, ConnectionCallback callback) {
        Connection connection = mConnections.get(webApkPackage);
        if (connection != null) {
            IBinder service = connection.getService();
            if (service != null) {
                callback.onConnected(service);
            } else {
                connection.addCallback(callback);
            }
            return;
        }

        final Connection newConnection = new Connection(this);
        mConnections.put(webApkPackage, newConnection);
        newConnection.addCallback(callback);

        Callable<Boolean> backgroundTask =
                () -> {
                    Intent intent = createConnectIntent(webApkPackage);
                    try {
                        if (appContext.bindService(
                                intent, newConnection, Context.BIND_AUTO_CREATE)) {
                            return true;
                        } else {
                            appContext.unbindService(newConnection);
                        }
                    } catch (SecurityException e) {
                        Log.w(TAG, "Security exception binding.", e);
                    }
                    return false;
                };
        Callback<Boolean> uiThreadReply =
                (bindSuccessful) -> {
                    if (!bindSuccessful) {
                        newConnection.onServiceConnected(null, null);
                    }
                };

        postTaskAndReply(backgroundTask, uiThreadReply);
    }

    /**
     * Disconnect from all of the WebAPK services. Can be called from any thread.
     *
     * @param appContext The application context.
     */
    @SuppressLint("StaticFieldLeak")
    public void disconnectAll(final Context appContext) {
        if (mConnections.isEmpty()) return;

        final Connection[] connectionsToDisconnect =
                mConnections.values().toArray(new Connection[mConnections.size()]);
        mConnections.clear();

        // Notify any waiting ConnectionCallbacks that the connection failed.
        for (Connection connectionToDisconnect : connectionsToDisconnect) {
            connectionToDisconnect.onServiceConnected(null, null);
        }

        Callable<Boolean> backgroundTask =
                () -> {
                    for (Connection connectionToDisconnect : connectionsToDisconnect) {
                        appContext.unbindService(connectionToDisconnect);
                    }
                    return true;
                };
        Callback<Boolean> uiThreadReply =
                (unused) -> {
                    if (mConnections.isEmpty() && mNumPendingPostedTasks == 0) {
                        destroyTaskRunner();
                    }
                };

        postTaskAndReply(backgroundTask, uiThreadReply);
    }

    /**
     * Runs {@link backgroundTask} on the task runner. Calls {@link uiThreadReply} on the UI thread
     * with the result of running the background task.
     */
    private void postTaskAndReply(
            final Callable<Boolean> backgroundTask, final Callback<Boolean> uiThreadReply) {
        ++mNumPendingPostedTasks;
        getTaskRunner()
                .postTask(
                        () -> {
                            Boolean result = false;
                            try {
                                result = backgroundTask.call();
                            } catch (Exception e) {
                            }

                            final Boolean finalResult = result;
                            PostTask.postTask(
                                    mUiThreadTaskTraits,
                                    () -> {
                                        --mNumPendingPostedTasks;
                                        uiThreadReply.onResult(finalResult);
                                    });
                        });
    }

    /**
     * Creates intent to connect to WebAPK service.
     *
     * @param webApkPackage The package name of the WebAPK to connect to.
     */
    private Intent createConnectIntent(String webApkPackage) {
        Intent intent = new Intent();
        if (mCategory != null) intent.addCategory(mCategory);
        if (mAction != null) intent.setAction(mAction);
        intent.setPackage(webApkPackage);
        return intent;
    }

    private TaskRunner getTaskRunner() {
        if (mTaskRunner == null) {
            mTaskRunner = PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK);
        }
        return mTaskRunner;
    }

    private void destroyTaskRunner() {
        if (mTaskRunner == null) return;

        mTaskRunner = null;
    }
}