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