blob: d943abd1fa21b40ceeb5e1462b4f5549743bd8a3 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.PowerManager;
import org.chromium.components.devtools_bridge.ui.ServiceUIFactory;
import org.chromium.components.devtools_bridge.util.LooperExecutor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Android service mixin implementing DevTools Bridge features that not depend on
* WebRTC signaling. Ability to host this class in different service classes allows:
* 1. Parametrization.
* 2. Simplified signaling for tests.
*
* Service starts foreground once any remote client starts a debugging session. Stops when all
* remote clients disconnect.
*
* Must be called on service's main thread.
*/
public class DevToolsBridgeServer implements SignalingReceiver {
public final int NOTIFICATION_ID = 1;
public final String DISCONNECT_ALL_CLIENTS_ACTION =
"action.DISCONNECT_ALL_CLIENTS_ACTION";
public final String WAKELOCK_KEY = "wake_lock.DevToolsBridgeServer";
private final Service mHost;
private final String mSocketName;
private final ServiceUIFactory mServiceUIFactory;
private final LooperExecutor mExecutor;
private final SessionDependencyFactory mFactory = new SessionDependencyFactory();
private final Map<String, ServerSession> mSessions = new HashMap<String, ServerSession>();
private PowerManager.WakeLock mWakeLock;
private Runnable mForegroundCompletionCallback;
public DevToolsBridgeServer(Service host, String socketName, ServiceUIFactory uiFactory) {
mHost = host;
mSocketName = socketName;
mServiceUIFactory = uiFactory;
mExecutor = LooperExecutor.newInstanceForMainLooper(mHost);
checkCalledOnHostServiceThread();
}
private void checkCalledOnHostServiceThread() {
assert mExecutor.isCalledOnSessionThread();
}
public Service getContext() {
return mHost;
}
public SharedPreferences getPreferences() {
return getPreferences(mHost);
}
public static SharedPreferences getPreferences(Context context) {
return context.getSharedPreferences(
DevToolsBridgeServer.class.getName(), Context.MODE_PRIVATE);
}
/**
* Should be called in service's onStartCommand. If it can handle then the method should
* delegate the work to the server.
*/
public boolean canHandle(Intent intent) {
String action = intent.getAction();
return DISCONNECT_ALL_CLIENTS_ACTION.equals(action);
}
public int onStartCommand(Intent intent) {
assert canHandle(intent);
String action = intent.getAction();
if (DISCONNECT_ALL_CLIENTS_ACTION.equals(action)) {
closeAllSessions();
}
return Service.START_NOT_STICKY;
}
/**
* Should be called in service's onDestroy.
*/
public void dispose() {
checkCalledOnHostServiceThread();
for (ServerSession session : mSessions.values()) {
session.dispose();
}
mFactory.dispose();
}
@Override
public void startSession(
String sessionId,
RTCConfiguration config,
String offer,
SessionBase.NegotiationCallback callback) {
checkCalledOnHostServiceThread();
if (mSessions.containsKey(sessionId)) {
callback.onFailure("Session already exists");
return;
}
ServerSession session = new ServerSession(mFactory, mExecutor, mSocketName);
session.setEventListener(new SessionEventListener(sessionId));
mSessions.put(sessionId, session);
session.startSession(config, offer, callback);
if (mSessions.size() == 1)
startForeground();
}
@Override
public void renegotiate(
String sessionId,
String offer,
SessionBase.NegotiationCallback callback) {
checkCalledOnHostServiceThread();
if (!mSessions.containsKey(sessionId)) {
callback.onFailure("Session does not exist");
return;
}
ServerSession session = mSessions.get(sessionId);
session.renegotiate(offer, callback);
}
@Override
public void iceExchange(
String sessionId,
List<String> clientCandidates,
SessionBase.IceExchangeCallback callback) {
checkCalledOnHostServiceThread();
if (!mSessions.containsKey(sessionId)) {
callback.onFailure("Session does not exist");
return;
}
ServerSession session = mSessions.get(sessionId);
session.iceExchange(clientCandidates, callback);
}
protected void startForeground() {
mForegroundCompletionCallback = startSticky();
checkCalledOnHostServiceThread();
mHost.startForeground(
NOTIFICATION_ID,
mServiceUIFactory.newForegroundNotification(mHost, DISCONNECT_ALL_CLIENTS_ACTION));
}
protected void stopForeground() {
checkCalledOnHostServiceThread();
mHost.stopForeground(true);
mForegroundCompletionCallback.run();
mForegroundCompletionCallback = null;
}
public void postOnServiceThread(Runnable runnable) {
mExecutor.postOnSessionThread(0, runnable);
}
private class SessionEventListener implements SessionBase.EventListener {
private final String mSessionId;
public SessionEventListener(String sessionId) {
mSessionId = sessionId;
}
public void onCloseSelf() {
checkCalledOnHostServiceThread();
mSessions.remove(mSessionId);
if (mSessions.size() == 0) {
stopForeground();
}
}
}
private void closeAllSessions() {
if (mSessions.isEmpty()) return;
for (ServerSession session : mSessions.values()) {
session.stop();
}
mSessions.clear();
stopForeground();
}
/**
* Helper method for doing background tasks. Usage:
*
* int onStartCommand(...) {
* if (..*) {
* startWorkInBackground(startSticky());
* return START_STICKY;
* }
* ...
* }
*
* void doWorkInBackground(final Runable completionHandler) {
* ... start background task
* @Override
* void run() {
* ...
* completionHandler.run();
* }
* }
*/
public Runnable startSticky() {
checkCalledOnHostServiceThread();
if (mWakeLock == null) {
PowerManager pm = (PowerManager) mHost.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
}
mWakeLock.acquire();
return new StartStickyCompletionHandler();
}
private class StartStickyCompletionHandler implements Runnable {
@Override
public void run() {
postOnServiceThread(new Runnable() {
@Override
public void run() {
mWakeLock.release();
if (!mWakeLock.isHeld()) mHost.stopSelf();
}
});
}
}
}