/*
* Copyright (C) 2014 Andrew Comminos
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package se.lublin.humla;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import se.lublin.humla.audio.AudioOutput;
import se.lublin.humla.audio.BluetoothScoReceiver;
import se.lublin.humla.audio.inputmode.ActivityInputMode;
import se.lublin.humla.audio.inputmode.ContinuousInputMode;
import se.lublin.humla.audio.inputmode.IInputMode;
import se.lublin.humla.audio.inputmode.ToggleInputMode;
import se.lublin.humla.audio.javacpp.CELT7;
import se.lublin.humla.exception.AudioException;
import se.lublin.humla.exception.NotConnectedException;
import se.lublin.humla.exception.NotSynchronizedException;
import se.lublin.humla.model.Channel;
import se.lublin.humla.model.IChannel;
import se.lublin.humla.model.IUser;
import se.lublin.humla.model.Message;
import se.lublin.humla.model.Server;
import se.lublin.humla.model.TalkState;
import se.lublin.humla.model.User;
import se.lublin.humla.model.WhisperTarget;
import se.lublin.humla.model.WhisperTargetList;
import se.lublin.humla.net.HumlaConnection;
import se.lublin.humla.net.HumlaTCPMessageType;
import se.lublin.humla.net.HumlaUDPMessageType;
import se.lublin.humla.protobuf.Mumble;
import se.lublin.humla.protocol.AudioHandler;
import se.lublin.humla.protocol.ModelHandler;
import se.lublin.humla.util.HumlaCallbacks;
import se.lublin.humla.util.HumlaDisconnectedException;
import se.lublin.humla.util.HumlaException;
import se.lublin.humla.util.HumlaLogger;
import se.lublin.humla.util.IHumlaObserver;
import se.lublin.humla.util.VoiceTargetMode;
public class HumlaService extends Service implements IHumlaService, IHumlaSession, HumlaConnection.HumlaConnectionListener, HumlaLogger, BluetoothScoReceiver.Listener {
static {
// Use Spongy Castle for crypto implementation so we can create and manage PKCS #12 (.p12) certificates.
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
/**
* An action to immediately connect to a given Mumble server.
* Requires that {@link #EXTRAS_SERVER} is provided.
*/
public static final String ACTION_CONNECT = "se.lublin.humla.CONNECT";
/** A {@link Server} specifying the server to connect to. */
public static final String EXTRAS_SERVER = "server";
public static final String EXTRAS_AUTO_RECONNECT = "auto_reconnect";
public static final String EXTRAS_AUTO_RECONNECT_DELAY = "auto_reconnect_delay";
public static final String EXTRAS_CERTIFICATE = "certificate";
public static final String EXTRAS_CERTIFICATE_PASSWORD = "certificate_password";
public static final String EXTRAS_DETECTION_THRESHOLD = "detection_threshold";
public static final String EXTRAS_AMPLITUDE_BOOST = "amplitude_boost";
public static final String EXTRAS_TRANSMIT_MODE = "transmit_mode";
public static final String EXTRAS_INPUT_RATE = "input_frequency";
public static final String EXTRAS_INPUT_QUALITY = "input_quality";
public static final String EXTRAS_USE_OPUS = "use_opus";
public static final String EXTRAS_FORCE_TCP = "force_tcp";
public static final String EXTRAS_USE_TOR = "use_tor";
public static final String EXTRAS_CLIENT_NAME = "client_name";
public static final String EXTRAS_ACCESS_TOKENS = "access_tokens";
public static final String EXTRAS_AUDIO_SOURCE = "audio_source";
public static final String EXTRAS_AUDIO_STREAM = "audio_stream";
public static final String EXTRAS_FRAMES_PER_PACKET = "frames_per_packet";
/** An optional path to a trust store for CA certificates. */
public static final String EXTRAS_TRUST_STORE = "trust_store";
/** The trust store's password. */
public static final String EXTRAS_TRUST_STORE_PASSWORD = "trust_store_password";
/** The trust store's format. */
public static final String EXTRAS_TRUST_STORE_FORMAT = "trust_store_format";
public static final String EXTRAS_HALF_DUPLEX = "half_duplex";
/** A list of users that should be local muted upon connection. */
public static final String EXTRAS_LOCAL_MUTE_HISTORY = "local_mute_history";
/** A list of users that should be local ignored upon connection. */
public static final String EXTRAS_LOCAL_IGNORE_HISTORY = "local_ignore_history";
public static final String EXTRAS_ENABLE_PREPROCESSOR = "enable_preprocessor";
// Service settings
private Server mServer;
private boolean mAutoReconnect;
private int mAutoReconnectDelay;
private byte[] mCertificate;
private String mCertificatePassword;
private boolean mUseOpus;
private boolean mForceTcp;
private boolean mUseTor;
private String mClientName;
private List mAccessTokens;
private String mTrustStore;
private String mTrustStorePassword;
private String mTrustStoreFormat;
private List mLocalMuteHistory;
private List mLocalIgnoreHistory;
private AudioHandler.Builder mAudioBuilder;
private int mTransmitMode;
private byte mVoiceTargetId;
private WhisperTargetList mWhisperTargetList;
private PowerManager.WakeLock mWakeLock;
private Handler mHandler;
private HumlaCallbacks mCallbacks;
private HumlaConnection mConnection;
private ConnectionState mConnectionState;
private ModelHandler mModelHandler;
private AudioHandler mAudioHandler;
private BluetoothScoReceiver mBluetoothReceiver;
private ActivityInputMode mActivityInputMode;
private ToggleInputMode mToggleInputMode;
private ContinuousInputMode mContinuousInputMode;
private boolean mReconnecting;
/**
* Listen for connectivity changes in the reconnection state, and reconnect accordingly.
*/
private BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!mReconnecting) {
unregisterReceiver(this);
return;
}
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
Log.v(Constants.TAG, "Connectivity restored, attempting reconnect.");
connect();
}
}
};
private AudioHandler.AudioEncodeListener mAudioInputListener =
new AudioHandler.AudioEncodeListener() {
@Override
public void onAudioEncoded(byte[] data, int length) {
if(mConnection != null && mConnection.isSynchronized()) {
mConnection.sendUDPMessage(data, length, false);
}
}
@Override
public void onTalkingStateChanged(final boolean talking) {
mHandler.post(new Runnable() {
@Override
public void run() {
try {
// If the server session is inactive, ignore this message.
// It's likely that this is leftover from a terminated connection.
if (!isSynchronized())
return;
if (mModelHandler == null || mConnection == null) {
return;
}
final User currentUser = mModelHandler.getUser(mConnection.getSession());
if (currentUser == null) return;
currentUser.setTalkState(talking ? TalkState.TALKING : TalkState.PASSIVE);
mCallbacks.onUserTalkStateUpdated(currentUser);
} catch (NotSynchronizedException e) {
e.printStackTrace();
}
}
});
}
};
private AudioOutput.AudioOutputListener mAudioOutputListener = new AudioOutput.AudioOutputListener() {
@Override
public void onUserTalkStateUpdated(final User user) {
mCallbacks.onUserTalkStateUpdated(user);
}
@Override
public User getUser(int session) {
if (mModelHandler != null) {
return mModelHandler.getUser(session);
}
return null;
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
try {
configureExtras(extras);
} catch (AudioException e) {
throw new RuntimeException("Attempted to initialize audio in onStartCommand erroneously.");
}
}
if (ACTION_CONNECT.equals(intent.getAction())) {
if (extras == null || !extras.containsKey(EXTRAS_SERVER)) {
// Ensure that we have been provided all required attributes.```
throw new RuntimeException(ACTION_CONNECT + " requires a server provided in extras.");
}
connect();
}
}
return START_NOT_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Humla:HumlaService");
mHandler = new Handler(getMainLooper());
mCallbacks = new HumlaCallbacks();
mAudioBuilder = new AudioHandler.Builder()
.setContext(this)
.setLogger(this)
.setEncodeListener(mAudioInputListener)
.setTalkingListener(mAudioOutputListener);
mConnectionState = ConnectionState.DISCONNECTED;
mBluetoothReceiver = new BluetoothScoReceiver(this, this);
registerReceiver(mBluetoothReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
mToggleInputMode = new ToggleInputMode();
mActivityInputMode = new ActivityInputMode(0); // FIXME: reasonable default
mContinuousInputMode = new ContinuousInputMode();
mWhisperTargetList = new WhisperTargetList();
}
@Override
public void onDestroy() {
unregisterReceiver(mBluetoothReceiver);
super.onDestroy();
}
public IBinder onBind(Intent intent) {
return new HumlaBinder(this);
}
protected void connect() {
try {
setReconnecting(false);
mConnectionState = ConnectionState.DISCONNECTED;
mVoiceTargetId = 0;
mWhisperTargetList.clear();
mConnection = new HumlaConnection(this);
mConnection.setForceTCP(mForceTcp);
mConnection.setUseTor(mUseTor);
mConnection.setKeys(mCertificate, mCertificatePassword);
mConnection.setTrustStore(mTrustStore, mTrustStorePassword, mTrustStoreFormat);
mModelHandler = new ModelHandler(this, mCallbacks, this,
mLocalMuteHistory, mLocalIgnoreHistory);
mConnection.addTCPMessageHandlers(mModelHandler);
mConnectionState = ConnectionState.CONNECTING;
mCallbacks.onConnecting();
mConnection.connect(mServer.getSrvHost(), mServer.getSrvPort());
} catch (HumlaException e) {
e.printStackTrace();
mCallbacks.onDisconnected(e);
}
}
public void disconnect() {
if (mConnection != null) {
mConnection.disconnect();
}
}
public boolean isConnectionEstablished() {
return mConnection != null && mConnection.isConnected();
}
/**
* @return true if Humla has received the ServerSync message, indicating synchronization with
* the server's model and settings. This is the main state of the service.
*/
public boolean isSynchronized() {
return mConnection != null && mConnection.isSynchronized();
}
@Override
public void onConnectionEstablished() {
// Send version information and authenticate.
final Mumble.Version.Builder version = Mumble.Version.newBuilder();
version.setRelease(mClientName);
version.setVersion(Constants.PROTOCOL_VERSION);
version.setOs("Android");
version.setOsVersion(Build.VERSION.RELEASE);
final Mumble.Authenticate.Builder auth = Mumble.Authenticate.newBuilder();
auth.setUsername(mServer.getUsername());
auth.setPassword(mServer.getPassword());
auth.addCeltVersions(CELT7.getBitstreamVersion());
// FIXME: resolve issues with CELT 11 robot voices.
// auth.addCeltVersions(Constants.CELT_11_VERSION);
auth.setOpus(mUseOpus);
auth.addAllTokens(mAccessTokens);
mConnection.sendTCPMessage(version.build(), HumlaTCPMessageType.Version);
mConnection.sendTCPMessage(auth.build(), HumlaTCPMessageType.Authenticate);
}
@Override
public void onConnectionSynchronized() {
// early disconned?
if (!mConnection.isConnected()) {
return;
}
// TODO hackish, but this seems to happen?!
if (mModelHandler == null) {
Log.e(Constants.TAG, "Error in HumlaService.onConnectionSynchronized: mAudioHandler is null");
return;
}
mConnectionState = ConnectionState.CONNECTED;
Log.v(Constants.TAG, "Connected");
mWakeLock.acquire();
try {
mAudioHandler = mAudioBuilder.initialize(
mModelHandler.getUser(mConnection.getSession()),
mConnection.getMaxBandwidth(), mConnection.getCodec(),
mVoiceTargetId);
mConnection.addTCPMessageHandlers(mAudioHandler);
mConnection.addUDPMessageHandlers(mAudioHandler);
} catch (AudioException e) {
e.printStackTrace();
onConnectionWarning(e.getMessage());
} catch (NotSynchronizedException e) {
throw new RuntimeException("Connection should be synchronized in callback for synchronization!", e);
}
mCallbacks.onConnected();
}
@Override
public void onConnectionHandshakeFailed(X509Certificate[] chain) {
mCallbacks.onTLSHandshakeFailed(chain);
}
@Override
public void onConnectionDisconnected(HumlaException e) {
if (e != null) {
Log.e(Constants.TAG, "Error: " + e.getMessage() +
" (reason: " + e.getReason().name() + ")");
mConnectionState = ConnectionState.CONNECTION_LOST;
setReconnecting(mAutoReconnect
&& e.getReason() == HumlaException.HumlaDisconnectReason.CONNECTION_ERROR);
} else {
Log.v(Constants.TAG, "Disconnected");
mConnectionState = ConnectionState.DISCONNECTED;
}
if(mWakeLock.isHeld()) {
mWakeLock.release();
}
if (mAudioHandler != null) {
mAudioHandler.shutdown();
}
mModelHandler = null;
mAudioHandler = null;
mVoiceTargetId = 0;
mWhisperTargetList.clear();
// Halt SCO connection on shutdown.
mBluetoothReceiver.stopBluetoothSco();
mCallbacks.onDisconnected(e);
}
@Override
public void onConnectionWarning(String warning) {
logWarning(warning);
}
@Override
public void logInfo(String message) {
if (mConnection == null || !mConnection.isSynchronized())
return; // don't log info prior to synchronization
mCallbacks.onLogInfo(message);
}
@Override
public void logWarning(String message) {
mCallbacks.onLogWarning(message);
}
@Override
public void logError(String message) {
mCallbacks.onLogError(message);
}
public void setReconnecting(boolean reconnecting) {
if (mReconnecting == reconnecting)
return;
mReconnecting = reconnecting;
if (reconnecting) {
ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
Log.v(Constants.TAG, "Connection lost due to non-connectivity issue. Start reconnect polling.");
Handler mainHandler = new Handler();
mainHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mReconnecting) connect();
}
}, mAutoReconnectDelay);
} else {
// In the event that we've lost connectivity, don't poll. Wait until network
// returns before we resume connection attempts.
Log.v(Constants.TAG, "Connection lost due to connectivity issue. Waiting until network returns.");
try {
registerReceiver(mConnectivityReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
} else {
try {
unregisterReceiver(mConnectivityReceiver);
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
}
/**
* Instantiates an audio handler with the current service settings, destroying any previous
* handler. Requires synchronization with the server, as the maximum bandwidth and session must
* be known.
*/
private void createAudioHandler() throws AudioException {
if (BuildConfig.DEBUG && mConnectionState != ConnectionState.CONNECTED) {
throw new AssertionError("Attempted to instantiate audio handler when not connected!");
}
if (mAudioHandler != null) {
mConnection.removeTCPMessageHandler(mAudioHandler);
mConnection.removeUDPMessageHandler(mAudioHandler);
mAudioHandler.shutdown();
}
try {
mAudioHandler = mAudioBuilder.initialize(
mModelHandler.getUser(mConnection.getSession()),
mConnection.getMaxBandwidth(), mConnection.getCodec(),
mVoiceTargetId);
mConnection.addTCPMessageHandlers(mAudioHandler);
mConnection.addUDPMessageHandlers(mAudioHandler);
} catch (NotSynchronizedException e) {
throw new RuntimeException("Attempted to create audio handler when not synchronized!");
}
}
/**
* Loads all defined settings from the given bundle into the HumlaService.
* Some settings may only take effect after a reconnect.
* @param extras A bundle with settings.
* @return true if a reconnect is required for changes to take effect.
* @see se.lublin.humla.HumlaService
*/
public boolean configureExtras(Bundle extras) throws AudioException {
boolean reconnectNeeded = false;
if (extras.containsKey(EXTRAS_SERVER)) {
mServer = extras.getParcelable(EXTRAS_SERVER);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_AUTO_RECONNECT)) {
mAutoReconnect = extras.getBoolean(EXTRAS_AUTO_RECONNECT);
}
if (extras.containsKey(EXTRAS_AUTO_RECONNECT_DELAY)) {
mAutoReconnectDelay = extras.getInt(EXTRAS_AUTO_RECONNECT_DELAY);
}
if (extras.containsKey(EXTRAS_CERTIFICATE)) {
mCertificate = extras.getByteArray(EXTRAS_CERTIFICATE);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_CERTIFICATE_PASSWORD)) {
mCertificatePassword = extras.getString(EXTRAS_CERTIFICATE_PASSWORD);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_DETECTION_THRESHOLD)) {
mActivityInputMode.setThreshold(extras.getFloat(EXTRAS_DETECTION_THRESHOLD));
}
if (extras.containsKey(EXTRAS_AMPLITUDE_BOOST)) {
mAudioBuilder.setAmplitudeBoost(extras.getFloat(EXTRAS_AMPLITUDE_BOOST));
}
if (extras.containsKey(EXTRAS_TRANSMIT_MODE)) {
mTransmitMode = extras.getInt(EXTRAS_TRANSMIT_MODE);
IInputMode inputMode;
switch (mTransmitMode) {
case Constants.TRANSMIT_PUSH_TO_TALK:
inputMode = mToggleInputMode;
break;
case Constants.TRANSMIT_CONTINUOUS:
inputMode = mContinuousInputMode;
break;
case Constants.TRANSMIT_VOICE_ACTIVITY:
inputMode = mActivityInputMode;
break;
default:
throw new IllegalArgumentException();
}
mAudioBuilder.setInputMode(inputMode);
}
if (extras.containsKey(EXTRAS_INPUT_RATE)) {
mAudioBuilder.setInputSampleRate(extras.getInt(EXTRAS_INPUT_RATE));
}
if (extras.containsKey(EXTRAS_INPUT_QUALITY)) {
mAudioBuilder.setTargetBitrate(extras.getInt(EXTRAS_INPUT_QUALITY));
}
if (extras.containsKey(EXTRAS_USE_OPUS)) {
mUseOpus = extras.getBoolean(EXTRAS_USE_OPUS);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_USE_TOR)) {
mUseTor = extras.getBoolean(EXTRAS_USE_TOR);
mForceTcp |= mUseTor; // Tor requires TCP connections to work- if it's on, force TCP.
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_FORCE_TCP)) {
mForceTcp |= extras.getBoolean(EXTRAS_FORCE_TCP);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_CLIENT_NAME)) {
mClientName = extras.getString(EXTRAS_CLIENT_NAME);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_ACCESS_TOKENS)) {
mAccessTokens = extras.getStringArrayList(EXTRAS_ACCESS_TOKENS);
if (mConnection != null && mConnection.isConnected()) {
mConnection.sendAccessTokens(mAccessTokens);
}
}
if (extras.containsKey(EXTRAS_AUDIO_SOURCE)) {
mAudioBuilder.setAudioSource(extras.getInt(EXTRAS_AUDIO_SOURCE));
}
if (extras.containsKey(EXTRAS_AUDIO_STREAM)) {
mAudioBuilder.setAudioStream(extras.getInt(EXTRAS_AUDIO_STREAM));
}
if (extras.containsKey(EXTRAS_FRAMES_PER_PACKET)) {
mAudioBuilder.setTargetFramesPerPacket(extras.getInt(EXTRAS_FRAMES_PER_PACKET));
}
if (extras.containsKey(EXTRAS_TRUST_STORE)) {
mTrustStore = extras.getString(EXTRAS_TRUST_STORE);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_TRUST_STORE_PASSWORD)) {
mTrustStorePassword = extras.getString(EXTRAS_TRUST_STORE_PASSWORD);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_TRUST_STORE_FORMAT)) {
mTrustStoreFormat = extras.getString(EXTRAS_TRUST_STORE_FORMAT);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_HALF_DUPLEX)) {
mAudioBuilder.setHalfDuplexEnabled(
extras.getInt(EXTRAS_TRANSMIT_MODE) == Constants.TRANSMIT_PUSH_TO_TALK
&& extras.getBoolean(EXTRAS_HALF_DUPLEX));
}
if (extras.containsKey(EXTRAS_LOCAL_MUTE_HISTORY)) {
mLocalMuteHistory = extras.getIntegerArrayList(EXTRAS_LOCAL_MUTE_HISTORY);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_LOCAL_IGNORE_HISTORY)) {
mLocalIgnoreHistory = extras.getIntegerArrayList(EXTRAS_LOCAL_IGNORE_HISTORY);
reconnectNeeded = true;
}
if (extras.containsKey(EXTRAS_ENABLE_PREPROCESSOR)) {
mAudioBuilder.setPreprocessorEnabled(extras.getBoolean(EXTRAS_ENABLE_PREPROCESSOR));
}
// Reload audio subsystem if initialized
if (mAudioHandler != null && mAudioHandler.isInitialized()) {
createAudioHandler();
Log.i(Constants.TAG, "Audio subsystem reloaded after settings change.");
}
return reconnectNeeded;
}
@Override
public void onBluetoothScoConnected() {
// After an SCO connection is established, audio is rerouted to be compatible with SCO.
mAudioBuilder.setBluetoothEnabled(true);
if (mAudioHandler != null) {
try {
createAudioHandler();
} catch (AudioException e) {
e.printStackTrace();
}
}
}
@Override
public void onBluetoothScoDisconnected() {
// Restore audio settings after disconnection.
mAudioBuilder.setBluetoothEnabled(false);
if (mAudioHandler != null) {
try {
createAudioHandler();
} catch (AudioException e) {
e.printStackTrace();
}
}
}
/**
* Exposes the current connection. The current connection is set once an attempt to connect to
* a server is made, and remains set until a subsequent connection. It remains available
* after disconnection to provide information regarding the terminated connection.
* @return The active {@link HumlaConnection}.
*/
public HumlaConnection getConnection() {
return mConnection;
}
/**
* Returnes the current {@link AudioHandler}. An AudioHandler is instantiated upon connection
* to a server, and destroyed upon disconnection.
* @return the active AudioHandler, or null if there is no active connection.
*/
private AudioHandler getAudioHandler() throws NotSynchronizedException {
if (!isSynchronized())
throw new NotSynchronizedException();
if (mAudioHandler == null && mConnectionState == ConnectionState.CONNECTED)
throw new RuntimeException("Audio handler should always be instantiated while connected!");
return mAudioHandler;
}
/**
* Returns the current {@link ModelHandler}, containing the channel tree. A model handler is
* valid for the lifetime of a connection.
* @return the active ModelHandler, or null if there is no active connection.
*/
private ModelHandler getModelHandler() throws NotSynchronizedException {
if (!isSynchronized())
throw new NotSynchronizedException();
if (mModelHandler == null && mConnectionState == ConnectionState.CONNECTED)
throw new RuntimeException("Model handler should always be instantiated while connected!");
return mModelHandler;
}
/**
* Returns the bluetooth service provider, established after synchronization.
* @return The {@link BluetoothScoReceiver} attached to this service.
*/
private BluetoothScoReceiver getBluetoothReceiver() throws NotSynchronizedException {
if (!isSynchronized())
throw new NotSynchronizedException();
return mBluetoothReceiver;
}
@Override
public HumlaService.ConnectionState getConnectionState() {
return mConnectionState;
}
@Override
public HumlaException getConnectionError() {
HumlaConnection connection = getConnection();
return connection != null ? connection.getError() : null;
}
@Override
public boolean isReconnecting() {
return mReconnecting;
}
@Override
public void cancelReconnect() {
setReconnecting(false);
}
@Override
public Server getTargetServer() {
return mServer;
}
@Override
public IHumlaSession getSession() throws HumlaDisconnectedException {
if (mConnectionState != ConnectionState.CONNECTED)
throw new HumlaDisconnectedException();
return this;
}
@Override
public long getTCPLatency() {
try {
return getConnection().getTCPLatency();
} catch (NotConnectedException e) {
throw new IllegalStateException(e);
}
}
@Override
public long getUDPLatency() {
try {
return getConnection().getUDPLatency();
} catch (NotConnectedException e) {
throw new IllegalStateException(e);
}
}
@Override
public int getMaxBandwidth() {
try {
return getConnection().getMaxBandwidth();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public int getCurrentBandwidth() {
try {
return getAudioHandler().getCurrentBandwidth();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public int getServerVersion() {
try {
return getConnection().getServerVersion();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public String getServerRelease() {
try {
return getConnection().getServerRelease();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public String getServerOSName() {
try {
return getConnection().getServerOSName();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public String getServerOSVersion() {
try {
return getConnection().getServerOSVersion();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public int getSessionId() {
try {
return getConnection().getSession();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public IUser getSessionUser() {
try {
return getModelHandler().getUser(getSessionId());
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public IChannel getSessionChannel() {
IUser user = getSessionUser();
if (user != null)
return user.getChannel();
throw new IllegalStateException("Session user should be set post-synchronization!");
}
@Override
public IUser getUser(int session) {
try {
return getModelHandler().getUser(session);
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public IChannel getChannel(int id) {
try {
return getModelHandler().getChannel(id);
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public IChannel getRootChannel() {
return getChannel(0);
}
@Override
public int getPermissions() {
try {
return getModelHandler().getPermissions();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public int getTransmitMode() {
return mTransmitMode;
}
@Override
public HumlaUDPMessageType getCodec() {
try {
return getConnection().getCodec();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public boolean usingBluetoothSco() {
try {
return getBluetoothReceiver().isBluetoothScoOn();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public void enableBluetoothSco() {
try {
getBluetoothReceiver().startBluetoothSco();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public void disableBluetoothSco() {
try {
getBluetoothReceiver().stopBluetoothSco();
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public boolean isTalking() {
return mToggleInputMode.isTalkingOn();
}
@Override
public void setTalkingState(boolean talking) {
mToggleInputMode.setTalkingOn(talking);
}
@Override
public void joinChannel(int channel) {
moveUserToChannel(getSessionId(), channel);
}
@Override
public void moveUserToChannel(int session, int channel) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSession(session);
usb.setChannelId(channel);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
@Override
public void createChannel(int parent, String name, String description, int position, boolean temporary) {
Mumble.ChannelState.Builder csb = Mumble.ChannelState.newBuilder();
csb.setParent(parent);
csb.setName(name);
csb.setDescription(description);
csb.setPosition(position);
csb.setTemporary(temporary);
getConnection().sendTCPMessage(csb.build(), HumlaTCPMessageType.ChannelState);
}
@Override
public void sendAccessTokens(final List tokens) {
getConnection().sendAccessTokens(tokens);
}
@Override
public void requestBanList() {
throw new UnsupportedOperationException("Not yet implemented"); // TODO
}
@Override
public void requestUserList() {
throw new UnsupportedOperationException("Not yet implemented"); // TODO
}
@Override
public void requestPermissions(int channel) {
Mumble.PermissionQuery.Builder pqb = Mumble.PermissionQuery.newBuilder();
pqb.setChannelId(channel);
getConnection().sendTCPMessage(pqb.build(), HumlaTCPMessageType.PermissionQuery);
}
@Override
public void requestComment(int session) {
Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder();
rbb.addSessionComment(session);
getConnection().sendTCPMessage(rbb.build(), HumlaTCPMessageType.RequestBlob);
}
@Override
public void requestAvatar(int session) {
Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder();
rbb.addSessionTexture(session);
getConnection().sendTCPMessage(rbb.build(), HumlaTCPMessageType.RequestBlob);
}
@Override
public void requestChannelDescription(int channel) {
Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder();
rbb.addChannelDescription(channel);
getConnection().sendTCPMessage(rbb.build(), HumlaTCPMessageType.RequestBlob);
}
@Override
public void registerUser(int session) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSession(session);
usb.setUserId(0);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
@Override
public void kickBanUser(int session, String reason, boolean ban) {
Mumble.UserRemove.Builder urb = Mumble.UserRemove.newBuilder();
urb.setSession(session);
urb.setReason(reason);
urb.setBan(ban);
getConnection().sendTCPMessage(urb.build(), HumlaTCPMessageType.UserRemove);
}
@Override
public Message sendUserTextMessage(int session, String message) {
try {
if (!isSynchronized())
throw new NotSynchronizedException();
Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder();
tmb.addSession(session);
tmb.setMessage(message);
getConnection().sendTCPMessage(tmb.build(), HumlaTCPMessageType.TextMessage);
User self = getModelHandler().getUser(getSessionId());
User user = getModelHandler().getUser(session);
List users = new ArrayList(1);
users.add(user);
return new Message(getSessionId(), self.getName(), new ArrayList(0), new ArrayList(0), users, message);
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public Message sendChannelTextMessage(int channel, String message, boolean tree) {
try {
if (!isSynchronized())
throw new NotSynchronizedException();
Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder();
if (tree) tmb.addTreeId(channel);
else tmb.addChannelId(channel);
tmb.setMessage(message);
getConnection().sendTCPMessage(tmb.build(), HumlaTCPMessageType.TextMessage);
User self = getModelHandler().getUser(getSessionId());
Channel targetChannel = getModelHandler().getChannel(channel);
List targetChannels = new ArrayList();
targetChannels.add(targetChannel);
return new Message(getSessionId(), self.getName(), targetChannels, tree ? targetChannels : new ArrayList(0), new ArrayList(0), message);
} catch (NotSynchronizedException e) {
throw new IllegalStateException(e);
}
}
@Override
public void setUserComment(int session, String comment) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSession(session);
usb.setComment(comment);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
@Override
public void setPrioritySpeaker(int session, boolean priority) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSession(session);
usb.setPrioritySpeaker(priority);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
@Override
public void removeChannel(int channel) {
Mumble.ChannelRemove.Builder crb = Mumble.ChannelRemove.newBuilder();
crb.setChannelId(channel);
getConnection().sendTCPMessage(crb.build(), HumlaTCPMessageType.ChannelRemove);
}
@Override
public void setMuteDeafState(int session, boolean mute, boolean deaf) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSession(session);
usb.setMute(mute);
usb.setDeaf(deaf);
if (!mute) usb.setSuppress(false);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
@Override
public void setSelfMuteDeafState(boolean mute, boolean deaf) {
Mumble.UserState.Builder usb = Mumble.UserState.newBuilder();
usb.setSelfMute(mute);
usb.setSelfDeaf(deaf);
getConnection().sendTCPMessage(usb.build(), HumlaTCPMessageType.UserState);
}
public void registerObserver(IHumlaObserver observer) {
mCallbacks.registerObserver(observer);
}
public void unregisterObserver(IHumlaObserver observer) {
mCallbacks.unregisterObserver(observer);
}
@Override
public boolean isConnected() {
return mConnectionState == ConnectionState.CONNECTED;
}
@Override
public void linkChannels(IChannel channelA, IChannel channelB) {
Mumble.ChannelState.Builder csb = Mumble.ChannelState.newBuilder();
csb.setChannelId(channelA.getId());
csb.addLinksAdd(channelB.getId());
getConnection().sendTCPMessage(csb.build(), HumlaTCPMessageType.ChannelState);
}
@Override
public void unlinkChannels(IChannel channelA, IChannel channelB) {
Mumble.ChannelState.Builder csb = Mumble.ChannelState.newBuilder();
csb.setChannelId(channelA.getId());
csb.addLinksRemove(channelB.getId());
getConnection().sendTCPMessage(csb.build(), HumlaTCPMessageType.ChannelState);
}
@Override
public void unlinkAllChannels(IChannel channel) {
Mumble.ChannelState.Builder csb = Mumble.ChannelState.newBuilder();
csb.setChannelId(channel.getId());
for (IChannel linked : channel.getLinks()) {
csb.addLinksRemove(linked.getId());
}
getConnection().sendTCPMessage(csb.build(), HumlaTCPMessageType.ChannelState);
}
@Override
public byte registerWhisperTarget(final WhisperTarget target) {
byte id = mWhisperTargetList.append(target);
if (id < 0) {
return -1;
}
Mumble.VoiceTarget.Target voiceTarget = target.createTarget();
Mumble.VoiceTarget.Builder vtb = Mumble.VoiceTarget.newBuilder();
vtb.setId(id);
vtb.addTargets(voiceTarget);
getConnection().sendTCPMessage(vtb.build(), HumlaTCPMessageType.VoiceTarget);
return id;
}
@Override
public void unregisterWhisperTarget(byte targetId) {
mWhisperTargetList.free(targetId);
}
@Override
public void setVoiceTargetId(byte targetId) {
if ((targetId & ~0x1F) > 0) {
throw new IllegalArgumentException("Target ID must be at most 5 bits.");
}
mVoiceTargetId = targetId;
mAudioHandler.setVoiceTargetId(targetId);
mCallbacks.onVoiceTargetChanged(VoiceTargetMode.fromId(targetId));
}
@Override
public byte getVoiceTargetId() {
return mVoiceTargetId;
}
@Override
public VoiceTargetMode getVoiceTargetMode() {
return VoiceTargetMode.fromId(mVoiceTargetId);
}
@Override
public WhisperTarget getWhisperTarget() {
if (VoiceTargetMode.fromId(mVoiceTargetId) == VoiceTargetMode.WHISPER) {
return mWhisperTargetList.get(mVoiceTargetId);
}
return null;
}
/**
* The current connection state of the service.
*/
public enum ConnectionState {
/**
* The default state of Humla, before connection to a server and after graceful/expected
* disconnection from a server.
*/
DISCONNECTED,
/**
* A connection to the server is currently in progress.
*/
CONNECTING,
/**
* Humla has received all data necessary for normal protocol communication with the server.
*/
CONNECTED,
/**
* The connection was lost due to either a kick/ban or socket I/O error.
* Humla may be reconnecting in this state.
* @see #isReconnecting()
* @see #cancelReconnect()
*/
CONNECTION_LOST
}
public static class HumlaBinder extends Binder {
private final IHumlaService mService;
private HumlaBinder(IHumlaService service) {
mService = service;
}
public IHumlaService getService() {
return mService;
}
}
}