diff options
author | Andrew Comminos <andrew@comminos.com> | 2015-10-19 09:02:29 +0300 |
---|---|---|
committer | Andrew Comminos <andrew@comminos.com> | 2015-10-19 09:02:29 +0300 |
commit | 523e9357752a68024e1525a8354a804d76eedf5f (patch) | |
tree | 10e42a12686a025bd3800e40956e84e06a86ba13 | |
parent | b6bac053a8e45768b9c75463d01e9610f5474663 (diff) |
Expose Jumble API through JumbleBinder instead of AIDL. Add public interfaces for exposing Jumble internal types.
15 files changed, 901 insertions, 704 deletions
diff --git a/src/androidTest/java/com/morlunk/jumble/test/JumbleServiceTest.java b/src/androidTest/java/com/morlunk/jumble/test/JumbleServiceTest.java index e4f0d68..d309f55 100644 --- a/src/androidTest/java/com/morlunk/jumble/test/JumbleServiceTest.java +++ b/src/androidTest/java/com/morlunk/jumble/test/JumbleServiceTest.java @@ -21,7 +21,7 @@ import android.content.Intent; import android.os.RemoteException; import android.test.ServiceTestCase; -import com.morlunk.jumble.IJumbleService; +import com.morlunk.jumble.JumbleBinder; import com.morlunk.jumble.JumbleService; import com.morlunk.jumble.model.Server; @@ -45,11 +45,10 @@ public class JumbleServiceTest extends ServiceTestCase<JumbleService> { intent.putExtra(JumbleService.EXTRAS_SERVER, DUMMY_SERVER); startService(intent); JumbleService service = getService(); - IJumbleService binder = service.getBinder(); + JumbleBinder binder = (JumbleBinder) service.onBind(intent); assertFalse(binder.isReconnecting()); assertNull(binder.getConnectionError()); - assertEquals(JumbleService.STATE_DISCONNECTED, binder.getConnectionState()); - assertFalse(binder.isTalking()); + assertEquals(JumbleService.ConnectionState.DISCONNECTED, binder.getConnectionState()); assertEquals(DUMMY_SERVER, binder.getConnectedServer()); } diff --git a/src/main/java/com/morlunk/jumble/JumbleBinder.java b/src/main/java/com/morlunk/jumble/JumbleBinder.java new file mode 100644 index 0000000..b0db425 --- /dev/null +++ b/src/main/java/com/morlunk/jumble/JumbleBinder.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2015 Andrew Comminos <andrew@comminos.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble; + +import android.os.Binder; +import android.util.Log; + +import com.morlunk.jumble.exception.AudioException; +import com.morlunk.jumble.model.Channel; +import com.morlunk.jumble.model.IChannel; +import com.morlunk.jumble.model.IUser; +import com.morlunk.jumble.model.Message; +import com.morlunk.jumble.model.Server; +import com.morlunk.jumble.model.User; +import com.morlunk.jumble.net.JumbleConnection; +import com.morlunk.jumble.net.JumbleTCPMessageType; +import com.morlunk.jumble.net.JumbleUDPMessageType; +import com.morlunk.jumble.protobuf.Mumble; +import com.morlunk.jumble.util.IJumbleObserver; +import com.morlunk.jumble.util.JumbleException; + +import java.util.ArrayList; +import java.util.List; + +/** + * A public interface for clients to communicate with a {@link JumbleService}. + * The long-term goal for this class is to migrate of the complexity out of this class into a + * JumbleProtocol class that is owned by a {@link com.morlunk.jumble.net.JumbleConnection}. + * <br><br> + * Calls are not guaranteed to be thread-safe, so only call the binder from the main thread. + * Service state changes related to connection state are only guaranteed to work if isConnected() + * is checked to be true. + * <br><br> + * If not explicitly stated in the method documentation, any call that depends on connection state + * will throw IllegalStateException if disconnected or not synchronized. + */ +public class JumbleBinder extends Binder { + private final JumbleService mService; + + protected JumbleBinder(JumbleService service) { + mService = service; + } + + /** + * Returns the current connection state of the service. + * @return one of {@link com.morlunk.jumble.JumbleService.ConnectionState}. + */ + public JumbleService.ConnectionState getConnectionState() { + return mService.getConnectionState(); + } + + /** + * If the {@link JumbleService} disconnected due to an error, returns that error. + * @return The error causing disconnection. If the last disconnection was successful or a + * connection has yet to be established, returns null. + */ + public JumbleException getConnectionError() { + JumbleConnection connection = mService.getConnection(); + return connection != null ? connection.getError() : null; + } + + /** + * Returns the reconnection state of the {@link JumbleService}. + * @return true if the service will attempt to automatically reconnect in the future. + */ + public boolean isReconnecting() { + return mService.isReconnecting(); + } + + /** + * Cancels any future reconnection attempts. Does nothing if reconnection is not in progress. + */ + public void cancelReconnect() { + mService.setReconnecting(false); + } + + /** + * @return the latency in milliseconds for the TCP connection. + * @throws IllegalStateException if not connected. + */ + public long getTCPLatency() { + return mService.getConnection().getTCPLatency(); + } + + /** + * @return the latency in milliseconds for the UDP connection. + * @throws IllegalStateException if not connected. + */ + public long getUDPLatency() { + return mService.getConnection().getUDPLatency(); + } + + /** + * @return the maximum bandwidth in bps for audio allowed by the server, or -1 if not set. + * @throws IllegalStateException if not synchronized. + */ + public int getMaxBandwidth() { + return mService.getConnection().getMaxBandwidth(); + } + + /** + * @return the current bandwidth in bps for audio sent to the server, or a negative integer + * if unknown (prior to connection or after disconnection). + * @throws IllegalStateException if not synchronized. + */ + public int getCurrentBandwidth() { + return mService.getAudioHandler().getCurrentBandwidth(); + } + + /** + * Returns the protocol version returned by the server in the format 0xAABBCC, where AA + * indicates the major version, BB indicates the minor version, and CC indicates the patch + * version. This is the same formatting used by the Mumble protocol in big-endian format. + * @return the current bandwidth in bps for audio sent to the server, or a negative integer + * if unknown (prior to connection or after disconnection). + * @throws IllegalStateException if not synchronized. + */ + public int getServerVersion() { + return mService.getConnection().getServerVersion(); + } + + /** + * @return a user-readable string with the server's Mumble release info. + * @throws IllegalStateException if not synchronized. + */ + public String getServerRelease() { + return mService.getConnection().getServerRelease(); + } + + /** + * @return a user-readable string with the server's OS name. + * @throws IllegalStateException if not connected. + */ + public String getServerOSName() { + return mService.getConnection().getServerOSName(); + } + + /** + * @return a user-readable string with the server's OS version. + * @throws IllegalStateException if not synchronized. + */ + public String getServerOSVersion() { + return mService.getConnection().getServerOSVersion(); + } + + /** + * Returns the current user's session. Set during server synchronization. + * @return an integer identifying the current user's connection. + * @throws IllegalStateException if not synchronized. + */ + public int getSession() { + return mService.getConnection().getSession(); + } + + /** + * Returns the current user. Set during server synchronization. + * @return the {@link IUser} representing the current user. + * @throws IllegalStateException if not synchronized. + */ + public IUser getSessionUser() { + return mService.getModelHandler().getUser(getSession()); + } + + /** + * Returns the user's current channel. + * @return the {@link IChannel} representing the user's current channel. + * @throws IllegalStateException if not synchronized. + */ + public IChannel getSessionChannel() { + IUser user = getSessionUser(); + if (user != null) + return user.getChannel(); + throw new IllegalStateException("Session user should be set post-synchronization!"); + } + + /** + * @return the server that Jumble is currently connected to (or attempted connection to). + */ + public Server getConnectedServer() { + return mService.getConnectedServer(); + } + + /** + * Retrieves the user with the given session ID. + * @param session An integer ID identifying a user's session. See {@link IUser#getSession()}. + * @return A user with the given session, or null if not found. + * @throws IllegalStateException if not synchronized. + */ + public IUser getUser(int session) { + return mService.getModelHandler().getUser(session); + } + + /** + * Retrieves the channel with the given ID. + * @param id An integer ID identifying a channel. See {@link IChannel#getId()}. + * @return A channel with the given session, or null if not found. + * @throws IllegalStateException if not synchronized. + */ + public IChannel getChannel(int id) { + return mService.getModelHandler().getChannel(id); + } + + /** + * @return the root channel of the server. + * @throws IllegalStateException if not synchronized. + */ + public IChannel getRootChannel() { + return getChannel(0); + } + + public int getPermissions() { + return mService.getModelHandler().getPermissions(); + } + + public int getTransmitMode() { + return mService.getAudioHandler().getTransmitMode(); + } + + public JumbleUDPMessageType getCodec() { + return mService.getConnection().getCodec(); + } + + public boolean usingBluetoothSco() { + return mService.getBluetoothReceiver().isBluetoothScoOn(); + } + + public void enableBluetoothSco() { + mService.getBluetoothReceiver().startBluetoothSco(); + } + + public void disableBluetoothSco() { + mService.getBluetoothReceiver().stopBluetoothSco(); + } + + public boolean isTalking() { + return mService.getAudioHandler().isRecording(); + } + + public void setTalkingState(boolean talking) { + if (getSessionUser().isSelfMuted() || getSessionUser().isMuted()) + return; + + if (mService.getAudioHandler().getTransmitMode() != Constants.TRANSMIT_PUSH_TO_TALK) { + Log.w(Constants.TAG, "Attempted to set talking state when not using PTT"); + return; + } + + try { + mService.getAudioHandler().setTalking(talking); + } catch (AudioException e) { + mService.logError(e.getMessage()); + } + } + + public void joinChannel(int channel) { + moveUserToChannel(getSession(), channel); + } + + public void moveUserToChannel(int session, int channel) { + Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); + usb.setSession(session); + usb.setChannelId(channel); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + 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); + mService.getConnection().sendTCPMessage(csb.build(), JumbleTCPMessageType.ChannelState); + } + + public void sendAccessTokens(final List<String> tokens) { + mService.getConnection().sendAccessTokens(tokens); + } + + public void requestBanList() { + throw new UnsupportedOperationException("Not yet implemented"); // TODO + } + + public void requestUserList() { + throw new UnsupportedOperationException("Not yet implemented"); // TODO + } + + public void requestPermissions(int channel) { + Mumble.PermissionQuery.Builder pqb = Mumble.PermissionQuery.newBuilder(); + pqb.setChannelId(channel); + mService.getConnection().sendTCPMessage(pqb.build(), JumbleTCPMessageType.PermissionQuery); + } + + public void requestComment(int session) { + Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); + rbb.addSessionComment(session); + mService.getConnection().sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); + } + + public void requestAvatar(int session) { + Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); + rbb.addSessionTexture(session); + mService.getConnection().sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); + } + + public void requestChannelDescription(int channel) { + Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); + rbb.addChannelDescription(channel); + mService.getConnection().sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); + } + + public void registerUser(int session) { + Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); + usb.setSession(session); + usb.setUserId(0); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + 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); + mService.getConnection().sendTCPMessage(urb.build(), JumbleTCPMessageType.UserRemove); + } + + public Message sendUserTextMessage(int session, String message) { + Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder(); + tmb.addSession(session); + tmb.setMessage(message); + mService.getConnection().sendTCPMessage(tmb.build(), JumbleTCPMessageType.TextMessage); + + User self = mService.getModelHandler().getUser(getSession()); + User user = mService.getModelHandler().getUser(session); + List<User> users = new ArrayList<User>(1); + users.add(user); + return new Message(getSession(), self.getName(), new ArrayList<Channel>(0), new ArrayList<Channel>(0), users, message); + } + + public Message sendChannelTextMessage(int channel, String message, boolean tree) { + Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder(); + if(tree) tmb.addTreeId(channel); + else tmb.addChannelId(channel); + tmb.setMessage(message); + mService.getConnection().sendTCPMessage(tmb.build(), JumbleTCPMessageType.TextMessage); + + User self = mService.getModelHandler().getUser(getSession()); + Channel targetChannel = mService.getModelHandler().getChannel(channel); + List<Channel> targetChannels = new ArrayList<Channel>(); + targetChannels.add(targetChannel); + return new Message(getSession(), self.getName(), targetChannels, tree ? targetChannels : new ArrayList<Channel>(0), new ArrayList<User>(0), message); + } + + public void setUserComment(int session, String comment) { + Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); + usb.setSession(session); + usb.setComment(comment); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + public void setPrioritySpeaker(int session, boolean priority) { + Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); + usb.setSession(session); + usb.setPrioritySpeaker(priority); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + public void removeChannel(int channel) { + Mumble.ChannelRemove.Builder crb = Mumble.ChannelRemove.newBuilder(); + crb.setChannelId(channel); + mService.getConnection().sendTCPMessage(crb.build(), JumbleTCPMessageType.ChannelRemove); + } + + 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); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + public void setSelfMuteDeafState(boolean mute, boolean deaf) { + Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); + usb.setSelfMute(mute); + usb.setSelfDeaf(deaf); + mService.getConnection().sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); + } + + public void registerObserver(IJumbleObserver observer) { + mService.registerObserver(observer); + } + + public void unregisterObserver(IJumbleObserver observer) { + mService.unregisterObserver(observer); + } +} diff --git a/src/main/java/com/morlunk/jumble/JumbleService.java b/src/main/java/com/morlunk/jumble/JumbleService.java index 63e4b3c..4d358f0 100644 --- a/src/main/java/com/morlunk/jumble/JumbleService.java +++ b/src/main/java/com/morlunk/jumble/JumbleService.java @@ -30,20 +30,16 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; -import android.os.RemoteException; import android.util.Log; import com.morlunk.jumble.audio.AudioOutput; import com.morlunk.jumble.audio.BluetoothScoReceiver; import com.morlunk.jumble.exception.AudioException; -import com.morlunk.jumble.model.Channel; -import com.morlunk.jumble.model.IChannel; -import com.morlunk.jumble.model.IUser; -import com.morlunk.jumble.model.Message; import com.morlunk.jumble.model.Server; import com.morlunk.jumble.model.TalkState; import com.morlunk.jumble.model.User; import com.morlunk.jumble.net.JumbleConnection; +import com.morlunk.jumble.util.IJumbleObserver; import com.morlunk.jumble.util.JumbleException; import com.morlunk.jumble.net.JumbleTCPMessageType; import com.morlunk.jumble.protobuf.Mumble; @@ -51,12 +47,9 @@ import com.morlunk.jumble.protocol.AudioHandler; import com.morlunk.jumble.protocol.ModelHandler; import com.morlunk.jumble.util.JumbleCallbacks; import com.morlunk.jumble.util.JumbleLogger; -import com.morlunk.jumble.util.ParcelableByteArray; import java.security.Security; -import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.List; public class JumbleService extends Service implements JumbleConnection.JumbleConnectionListener, JumbleLogger, BluetoothScoReceiver.Listener { @@ -67,27 +60,6 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon } /** - * The default state of Jumble, before connection to a server and after graceful/expected - * disconnection from a server. - */ - public static final int STATE_DISCONNECTED = 0; - /** - * A connection to the server is currently in progress. - */ - public static final int STATE_CONNECTING = 1; - /** - * Jumble has received all data necessary for normal protocol communication with the server. - */ - public static final int STATE_CONNECTED = 2; - /** - * The connection was lost due to either a kick/ban or socket I/O error. - * Jumble can be reconnecting in this state. - * @see IJumbleService#isReconnecting() - * @see IJumbleService#cancelReconnect() - */ - public static final int STATE_CONNECTION_LOST = 3; - - /** * An action to immediately connect to a given Mumble server. * Requires that {@link #EXTRAS_SERVER} is provided. */ @@ -146,10 +118,9 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon private PowerManager.WakeLock mWakeLock; private Handler mHandler; private JumbleCallbacks mCallbacks; - private IJumbleService.Stub mBinder = new JumbleBinder(); private JumbleConnection mConnection; - private int mConnectionState; + private ConnectionState mConnectionState; private ModelHandler mModelHandler; private AudioHandler mAudioHandler; private BluetoothScoReceiver mBluetoothReceiver; @@ -189,16 +160,12 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon mHandler.post(new Runnable() { @Override public void run() { - if(!isConnected()) return; + if(!isConnectionEstablished()) return; final User currentUser = mModelHandler.getUser(mConnection.getSession()); if(currentUser == null) return; currentUser.setTalkState(state); - try { - mCallbacks.onUserTalkStateUpdated(currentUser); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onUserTalkStateUpdated(currentUser); } }); } @@ -207,11 +174,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon private AudioOutput.AudioOutputListener mAudioOutputListener = new AudioOutput.AudioOutputListener() { @Override public void onUserTalkStateUpdated(final User user) { - try { - mCallbacks.onUserTalkStateUpdated(user); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onUserTalkStateUpdated(user); } @Override @@ -234,7 +197,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon if (ACTION_CONNECT.equals(intent.getAction())) { if (extras == null || !extras.containsKey(EXTRAS_SERVER)) { - // Ensure that we have been provided all required attributes. + // Ensure that we have been provided all required attributes.``` throw new RuntimeException(ACTION_CONNECT + " requires a server provided in extras."); } connect(); @@ -256,7 +219,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon .setLogger(this) .setEncodeListener(mAudioInputListener) .setTalkingListener(mAudioOutputListener); - mConnectionState = STATE_DISCONNECTED; + mConnectionState = ConnectionState.DISCONNECTED; mBluetoothReceiver = new BluetoothScoReceiver(this, this); registerReceiver(mBluetoothReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED)); } @@ -264,22 +227,17 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon @Override public void onDestroy() { unregisterReceiver(mBluetoothReceiver); - mCallbacks.kill(); super.onDestroy(); } public IBinder onBind(Intent intent) { - return mBinder; - } - - public IJumbleService getBinder() { - return mBinder; + return new JumbleBinder(this); } - public void connect() { + private void connect() { try { setReconnecting(false); - mConnectionState = STATE_DISCONNECTED; + mConnectionState = ConnectionState.DISCONNECTED; mConnection = new JumbleConnection(this); mConnection.setForceTCP(mForceTcp); @@ -291,22 +249,14 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon mLocalMuteHistory, mLocalIgnoreHistory); mConnection.addTCPMessageHandlers(mModelHandler); - mConnectionState = STATE_CONNECTING; + mConnectionState = ConnectionState.CONNECTING; - try { - mCallbacks.onConnecting(); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onConnecting(); mConnection.connect(mServer.getHost(), mServer.getPort()); } catch (JumbleException e) { e.printStackTrace(); - try { - mCallbacks.onDisconnected(e); - } catch (RemoteException e1) { - e1.printStackTrace(); - } + mCallbacks.onDisconnected(e); } } @@ -314,10 +264,18 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon mConnection.disconnect(); } - public boolean isConnected() { + public boolean isConnectionEstablished() { return mConnection != null && mConnection.isConnected(); } + /** + * @return true if Jumble 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 mConnectionState == ConnectionState.CONNECTED; + } + @Override public void onConnectionEstablished() { // Send version information and authenticate. @@ -342,7 +300,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon @Override public void onConnectionSynchronized() { - mConnectionState = STATE_CONNECTED; + mConnectionState = ConnectionState.CONNECTED; Log.v(Constants.TAG, "Connected"); mWakeLock.acquire(); @@ -358,23 +316,12 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon onConnectionWarning(e.getMessage()); } - try { - mCallbacks.onConnected(); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onConnected(); } @Override public void onConnectionHandshakeFailed(X509Certificate[] chain) { - try { - final ParcelableByteArray encodedCert = new ParcelableByteArray(chain[0].getEncoded()); - mCallbacks.onTLSHandshakeFailed(encodedCert); - } catch (CertificateEncodingException e) { - e.printStackTrace(); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onTLSHandshakeFailed(chain); } @Override @@ -382,13 +329,13 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon if (e != null) { Log.e(Constants.TAG, "Error: " + e.getMessage() + " (reason: " + e.getReason().name() + ")"); - mConnectionState = STATE_CONNECTION_LOST; + mConnectionState = ConnectionState.CONNECTION_LOST; setReconnecting(mAutoReconnect && e.getReason() == JumbleException.JumbleDisconnectReason.CONNECTION_ERROR); } else { Log.v(Constants.TAG, "Disconnected"); - mConnectionState = STATE_DISCONNECTED; + mConnectionState = ConnectionState.DISCONNECTED; } if(mWakeLock.isHeld()) { @@ -405,11 +352,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon // Halt SCO connection on shutdown. mBluetoothReceiver.stopBluetoothSco(); - try { - mCallbacks.onDisconnected(e); - } catch (RemoteException re) { - re.printStackTrace(); - } + mCallbacks.onDisconnected(e); } @Override @@ -421,32 +364,23 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon public void logInfo(String message) { if (mConnection == null || !mConnection.isSynchronized()) return; // don't log info prior to synchronization - try { - mCallbacks.onLogInfo(message); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onLogInfo(message); } @Override public void logWarning(String message) { - try { - mCallbacks.onLogWarning(message); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onLogWarning(message); } @Override public void logError(String message) { - try { - mCallbacks.onLogError(message); - } catch (RemoteException e) { - e.printStackTrace(); - } + mCallbacks.onLogError(message); } - private void setReconnecting(boolean reconnecting) { + public void setReconnecting(boolean reconnecting) { + if (mReconnecting == reconnecting) + return; + mReconnecting = reconnecting; if (reconnecting) { ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); @@ -486,7 +420,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon * be known. */ private void createAudioHandler() throws AudioException { - if (BuildConfig.DEBUG && mConnectionState != STATE_CONNECTED) { + if (BuildConfig.DEBUG && mConnectionState != ConnectionState.CONNECTED) { throw new AssertionError("Attempted to instantiate audio handler when not connected!"); } @@ -510,7 +444,7 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon * @return true if a reconnect is required for changes to take effect. * @see com.morlunk.jumble.JumbleService */ - private boolean configureExtras(Bundle extras) throws AudioException { + public boolean configureExtras(Bundle extras) throws AudioException { boolean reconnectNeeded = false; if (extras.containsKey(EXTRAS_SERVER)) { mServer = extras.getParcelable(EXTRAS_SERVER); @@ -638,342 +572,97 @@ public class JumbleService extends Service implements JumbleConnection.JumbleCon } } - public class JumbleBinder extends IJumbleService.Stub { - @Override - public int getConnectionState() throws RemoteException { - return mConnectionState; - } - - @Override - public JumbleException getConnectionError() throws RemoteException { - return mConnection != null ? mConnection.getError() : null; - } - - @Override - public boolean isReconnecting() throws RemoteException { - return mReconnecting; - } - - @Override - public void cancelReconnect() throws RemoteException { - setReconnecting(false); - } - - @Override - public void disconnect() throws RemoteException { - JumbleService.this.disconnect(); - } - - @Override - public long getTCPLatency() throws RemoteException { - return mConnection.getTCPLatency(); - } - - @Override - public long getUDPLatency() throws RemoteException { - return mConnection.getUDPLatency(); - } - - @Override - public int getMaxBandwidth() throws RemoteException { - return mConnection.getMaxBandwidth(); - } - - @Override - public int getCurrentBandwidth() throws RemoteException { - return mAudioHandler.getCurrentBandwidth(); - } - - @Override - public int getServerVersion() throws RemoteException { - return mConnection.getServerVersion(); - } - - @Override - public String getServerRelease() throws RemoteException { - return mConnection.getServerRelease(); - } - - @Override - public String getServerOSName() throws RemoteException { - return mConnection.getServerOSName(); - } - - @Override - public String getServerOSVersion() throws RemoteException { - return mConnection.getServerOSVersion(); - } - - @Override - public int getSession() throws RemoteException { - return mConnection != null ? mConnection.getSession() : -1; - } - - @Override - public IUser getSessionUser() throws RemoteException { - return mModelHandler != null ? mModelHandler.getUser(getSession()) : null; - } - - @Override - public IChannel getSessionChannel() throws RemoteException { - IUser user = getSessionUser(); - if (user != null) { - return user.getChannel(); - } - return null; - } - - @Override - public Server getConnectedServer() throws RemoteException { - return mServer; - } - - @Override - public IUser getUser(int id) throws RemoteException { - if (mModelHandler != null) - return mModelHandler.getUser(id); - return null; - } - - @Override - public IChannel getChannel(int id) throws RemoteException { - if (mModelHandler != null) - return mModelHandler.getChannel(id); - return null; - } - - @Override - public IChannel getRootChannel() throws RemoteException { - return getChannel(0); - } - - @Override - public int getPermissions() throws RemoteException { - return mModelHandler != null ? mModelHandler.getPermissions() : 0; - } - - @Override - public int getTransmitMode() throws RemoteException { - return mAudioHandler.getTransmitMode(); - } - - @Override - public int getCodec() throws RemoteException { - return mConnection.getCodec().ordinal(); // FIXME: ordinal is bad, make enum method - } - - @Override - public boolean usingBluetoothSco() throws RemoteException { - return mBluetoothReceiver.isBluetoothScoOn(); - } - - @Override - public void enableBluetoothSco() throws RemoteException { - mBluetoothReceiver.startBluetoothSco(); - } - - @Override - public void disableBluetoothSco() throws RemoteException { - mBluetoothReceiver.stopBluetoothSco(); - } - - @Override - public boolean isTalking() throws RemoteException { - return mAudioHandler != null && mAudioHandler.isRecording(); - } - - @Override - public void setTalkingState(boolean talking) throws RemoteException { - if(getSessionUser() != null && - (getSessionUser().isSelfMuted() || getSessionUser().isMuted())) { - return; - } - - if (mAudioHandler.getTransmitMode() != Constants.TRANSMIT_PUSH_TO_TALK) { - Log.w(Constants.TAG, "Attempted to set talking state when not using PTT"); - return; - } - - try { - mAudioHandler.setTalking(talking); - } catch (AudioException e) { - logError(e.getMessage()); - } - } - - @Override - public void joinChannel(int channel) throws RemoteException { - moveUserToChannel(getSession(), channel); - } - - @Override - public void moveUserToChannel(int session, int channel) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSession(session); - usb.setChannelId(channel); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } - - @Override - public void createChannel(int parent, String name, String description, int position, boolean temporary) throws RemoteException { - Mumble.ChannelState.Builder csb = Mumble.ChannelState.newBuilder(); - csb.setParent(parent); - csb.setName(name); - csb.setDescription(description); - csb.setPosition(position); - csb.setTemporary(temporary); - mConnection.sendTCPMessage(csb.build(), JumbleTCPMessageType.ChannelState); - } - - @Override - public void sendAccessTokens(List tokens) throws RemoteException { - mConnection.sendAccessTokens(tokens); - } - - @Override - public void requestBanList() throws RemoteException { - throw new UnsupportedOperationException("Not yet implemented"); // TODO - } - - @Override - public void requestUserList() throws RemoteException { - throw new UnsupportedOperationException("Not yet implemented"); // TODO - } - - @Override - public void requestPermissions(int channel) throws RemoteException { - Mumble.PermissionQuery.Builder pqb = Mumble.PermissionQuery.newBuilder(); - pqb.setChannelId(channel); - mConnection.sendTCPMessage(pqb.build(), JumbleTCPMessageType.PermissionQuery); - } - - @Override - public void requestComment(int session) throws RemoteException { - Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); - rbb.addSessionComment(session); - mConnection.sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); - } - - @Override - public void requestAvatar(int session) throws RemoteException { - Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); - rbb.addSessionTexture(session); - mConnection.sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); - } - - @Override - public void requestChannelDescription(int channel) throws RemoteException { - Mumble.RequestBlob.Builder rbb = Mumble.RequestBlob.newBuilder(); - rbb.addChannelDescription(channel); - mConnection.sendTCPMessage(rbb.build(), JumbleTCPMessageType.RequestBlob); - } - - @Override - public void registerUser(int session) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSession(session); - usb.setUserId(0); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } - - @Override - public void kickBanUser(int session, String reason, boolean ban) throws RemoteException { - Mumble.UserRemove.Builder urb = Mumble.UserRemove.newBuilder(); - urb.setSession(session); - urb.setReason(reason); - urb.setBan(ban); - mConnection.sendTCPMessage(urb.build(), JumbleTCPMessageType.UserRemove); - } - - @Override - public Message sendUserTextMessage(int session, String message) throws RemoteException { - Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder(); - tmb.addSession(session); - tmb.setMessage(message); - mConnection.sendTCPMessage(tmb.build(), JumbleTCPMessageType.TextMessage); - - User self = mModelHandler.getUser(getSession()); - User user = mModelHandler.getUser(session); - List<User> users = new ArrayList<User>(1); - users.add(user); - return new Message(getSession(), self.getName(), new ArrayList<Channel>(0), new ArrayList<Channel>(0), users, message); - } - - @Override - public Message sendChannelTextMessage(int channel, String message, boolean tree) throws RemoteException { - Mumble.TextMessage.Builder tmb = Mumble.TextMessage.newBuilder(); - if(tree) tmb.addTreeId(channel); - else tmb.addChannelId(channel); - tmb.setMessage(message); - mConnection.sendTCPMessage(tmb.build(), JumbleTCPMessageType.TextMessage); + /** + * 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 JumbleConnection}, or null if a connection has not been + * established yet. + */ + public JumbleConnection getConnection() { + return mConnection; + } - User self = mModelHandler.getUser(getSession()); - Channel targetChannel = mModelHandler.getChannel(channel); - List<Channel> targetChannels = new ArrayList<Channel>(); - targetChannels.add(targetChannel); - return new Message(getSession(), self.getName(), targetChannels, tree ? targetChannels : new ArrayList<Channel>(0), new ArrayList<User>(0), message); - } + /** + * 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. + */ + public AudioHandler getAudioHandler() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); + if (mAudioHandler == null && mConnectionState == ConnectionState.CONNECTED) + throw new RuntimeException("Audio handler should always be instantiated while connected!"); + return mAudioHandler; + } - @Override - public void setUserComment(int session, String comment) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSession(session); - usb.setComment(comment); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } + /** + * 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. + */ + public ModelHandler getModelHandler() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); + if (mModelHandler == null && mConnectionState == ConnectionState.CONNECTED) + throw new RuntimeException("Model handler should always be instantiated while connected!"); + return mModelHandler; + } - @Override - public void setPrioritySpeaker(int session, boolean priority) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSession(session); - usb.setPrioritySpeaker(priority); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } + /** + * Returns the bluetooth service provider, established after synchronization. + * @return The {@link BluetoothScoReceiver} attached to this service. + * @throws IllegalStateException if not synchronized or disconnected. + */ + public BluetoothScoReceiver getBluetoothReceiver() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); + return mBluetoothReceiver; + } - @Override - public void removeChannel(int channel) throws RemoteException { - Mumble.ChannelRemove.Builder crb = Mumble.ChannelRemove.newBuilder(); - crb.setChannelId(channel); - mConnection.sendTCPMessage(crb.build(), JumbleTCPMessageType.ChannelRemove); - } + public boolean isReconnecting() { + return mReconnecting; + } - @Override - public void setMuteDeafState(int session, boolean mute, boolean deaf) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSession(session); - usb.setMute(mute); - usb.setDeaf(deaf); - if (!mute) usb.setSuppress(false); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } + public ConnectionState getConnectionState() { + return mConnectionState; + } - @Override - public void setSelfMuteDeafState(boolean mute, boolean deaf) throws RemoteException { - Mumble.UserState.Builder usb = Mumble.UserState.newBuilder(); - usb.setSelfMute(mute); - usb.setSelfDeaf(deaf); - mConnection.sendTCPMessage(usb.build(), JumbleTCPMessageType.UserState); - } + public Server getConnectedServer() { + return mServer; + } - @Override - public void registerObserver(IJumbleObserver observer) throws RemoteException { - mCallbacks.registerObserver(observer); - } + public void registerObserver(IJumbleObserver observer) { + mCallbacks.registerObserver(observer); + } - @Override - public void unregisterObserver(IJumbleObserver observer) throws RemoteException { - mCallbacks.unregisterObserver(observer); - } + public void unregisterObserver(IJumbleObserver observer) { + mCallbacks.unregisterObserver(observer); + } - @Override - public boolean reconfigure(Bundle extras) throws RemoteException { - try { - return configureExtras(extras); - } catch (AudioException e) { - e.printStackTrace(); - // TODO - return true; - } - } + /** + * The current connection state of the service. + */ + public enum ConnectionState { + /** + * The default state of Jumble, before connection to a server and after graceful/expected + * disconnection from a server. + */ + DISCONNECTED, + /** + * A connection to the server is currently in progress. + */ + CONNECTING, + /** + * Jumble 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. + * Jumble may be reconnecting in this state. + * @see JumbleBinder#isReconnecting() + * @see JumbleBinder#cancelReconnect() + */ + CONNECTION_LOST } } diff --git a/src/main/java/com/morlunk/jumble/model/Channel.java b/src/main/java/com/morlunk/jumble/model/Channel.java index 7edc95f..142754f 100644 --- a/src/main/java/com/morlunk/jumble/model/Channel.java +++ b/src/main/java/com/morlunk/jumble/model/Channel.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -public final class Channel extends IChannel.Stub implements Comparable<Channel> { +public final class Channel implements IChannel, Comparable<Channel> { private int mId; private int mPosition; private int mLevel; @@ -68,10 +68,12 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> mUsers.remove(user); } + @Override public List<User> getUsers() { return Collections.unmodifiableList(mUsers); } + @Override public int getId() { return mId; } @@ -80,6 +82,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mId = mId; } + @Override public int getPosition() { return mPosition; } @@ -88,6 +91,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mPosition = mPosition; } + @Override public boolean isTemporary() { return mTemporary; } @@ -96,6 +100,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mTemporary = mTemporary; } + @Override public Channel getParent() { return mParent; } @@ -104,6 +109,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mParent = mParent; } + @Override public String getName() { return mName; } @@ -112,6 +118,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mName = mName; } + @Override public String getDescription() { return mDescription; } @@ -120,6 +127,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mDescription = mDescription; } + @Override public byte[] getDescriptionHash() { return mDescriptionHash; } @@ -128,6 +136,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> this.mDescriptionHash = mDescriptionHash; } + @Override public List<Channel> getSubchannels() { return Collections.unmodifiableList(mSubchannels); } @@ -147,6 +156,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> mSubchannels.remove(channel); } + @Override public List<Channel> getLinks() { return Collections.unmodifiableList(mLinks); } @@ -183,6 +193,7 @@ public final class Channel extends IChannel.Stub implements Comparable<Channel> return userCount; } + @Override public int getPermissions() { return mPermissions; } diff --git a/src/main/java/com/morlunk/jumble/model/IChannel.java b/src/main/java/com/morlunk/jumble/model/IChannel.java new file mode 100644 index 0000000..e261ff3 --- /dev/null +++ b/src/main/java/com/morlunk/jumble/model/IChannel.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Andrew Comminos <andrew@comminos.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.model; + +import java.util.List; + +public interface IChannel { + List<? extends IUser> getUsers(); + + int getId(); + + int getPosition(); + + boolean isTemporary(); + + IChannel getParent(); + + String getName(); + + String getDescription(); + + byte[] getDescriptionHash(); + + List<? extends IChannel> getSubchannels(); + + List<? extends IChannel> getLinks(); + + int getPermissions(); +} diff --git a/src/main/java/com/morlunk/jumble/model/IMessage.java b/src/main/java/com/morlunk/jumble/model/IMessage.java new file mode 100644 index 0000000..9a3ec2c --- /dev/null +++ b/src/main/java/com/morlunk/jumble/model/IMessage.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 Andrew Comminos <andrew@comminos.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.model; + +import java.util.List; + +public interface IMessage { + int getActor(); + + String getActorName(); + + List<Channel> getTargetChannels(); + + List<Channel> getTargetTrees(); + + List<User> getTargetUsers(); + + String getMessage(); + + long getReceivedTime(); +} diff --git a/src/main/java/com/morlunk/jumble/model/IUser.java b/src/main/java/com/morlunk/jumble/model/IUser.java new file mode 100644 index 0000000..b0311b7 --- /dev/null +++ b/src/main/java/com/morlunk/jumble/model/IUser.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Andrew Comminos <andrew@comminos.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.model; + +/** + * Created by andrew on 15/10/15. + */ +public interface IUser { + int getSession(); + + Channel getChannel(); + + int getUserId(); + + String getName(); + + String getComment(); + + byte[] getCommentHash(); + + byte[] getTexture(); + + byte[] getTextureHash(); + + String getHash(); + + boolean isMuted(); + + boolean isDeafened(); + + boolean isSuppressed(); + + boolean isSelfMuted(); + + boolean isSelfDeafened(); + + boolean isPrioritySpeaker(); + + boolean isRecording(); + + boolean isLocalMuted(); + + boolean isLocalIgnored(); + + TalkState getTalkState(); +} diff --git a/src/main/java/com/morlunk/jumble/model/Message.java b/src/main/java/com/morlunk/jumble/model/Message.java index 17270da..202e803 100644 --- a/src/main/java/com/morlunk/jumble/model/Message.java +++ b/src/main/java/com/morlunk/jumble/model/Message.java @@ -32,7 +32,7 @@ import java.util.List; * as the actor may no longer be on the server. * Created by andrew on 03/12/13. */ -public class Message extends IMessage.Stub { +public class Message implements IMessage { private int mActor; private String mActorName; private List<Channel> mChannels; @@ -58,30 +58,37 @@ public class Message extends IMessage.Stub { mTrees = trees; mUsers = users; } + @Override public int getActor() { return mActor; } + @Override public String getActorName() { return mActorName; } + @Override public List<Channel> getTargetChannels() { return Collections.unmodifiableList(mChannels); } + @Override public List<Channel> getTargetTrees() { return Collections.unmodifiableList(mTrees); } + @Override public List<User> getTargetUsers() { return Collections.unmodifiableList(mUsers); } + @Override public String getMessage() { return mMessage; } + @Override public long getReceivedTime() { return mReceivedTime; } diff --git a/src/main/java/com/morlunk/jumble/model/User.java b/src/main/java/com/morlunk/jumble/model/User.java index 559cbdf..3bfd31d 100644 --- a/src/main/java/com/morlunk/jumble/model/User.java +++ b/src/main/java/com/morlunk/jumble/model/User.java @@ -19,7 +19,7 @@ package com.morlunk.jumble.model; import com.google.protobuf.ByteString; -public class User extends IUser.Stub implements Comparable<User> { +public class User implements IUser, Comparable<User> { private int mSession; private int mId = -1; @@ -60,10 +60,12 @@ public class User extends IUser.Stub implements Comparable<User> { mName = name; } + @Override public int getSession() { return mSession; } + @Override public Channel getChannel() { return mChannel; } @@ -82,6 +84,7 @@ public class User extends IUser.Stub implements Comparable<User> { mChannel.addUser(this); } + @Override public int getUserId() { return mId; } @@ -90,6 +93,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mId = mId; } + @Override public String getName() { return mName; } @@ -98,6 +102,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mName = mName; } + @Override public String getComment() { return mComment; } @@ -106,6 +111,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mComment = mComment; } + @Override public byte[] getCommentHash() { return mCommentHash != null ? mCommentHash.toByteArray() : null; } @@ -114,6 +120,7 @@ public class User extends IUser.Stub implements Comparable<User> { mCommentHash = commentHash; } + @Override public byte[] getTexture() { return mTexture != null ? mTexture.toByteArray() : null; } @@ -122,6 +129,7 @@ public class User extends IUser.Stub implements Comparable<User> { mTexture = texture; } + @Override public byte[] getTextureHash() { return mTextureHash != null ? mTextureHash.toByteArray() : null; } @@ -130,6 +138,7 @@ public class User extends IUser.Stub implements Comparable<User> { mTextureHash = textureHash; } + @Override public String getHash() { return mHash; } @@ -138,6 +147,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mHash = mHash; } + @Override public boolean isMuted() { return mMuted; } @@ -146,6 +156,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mMuted = mMuted; } + @Override public boolean isDeafened() { return mDeafened; } @@ -154,6 +165,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mDeafened = mDeafened; } + @Override public boolean isSuppressed() { return mSuppressed; } @@ -162,6 +174,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mSuppressed = mSuppressed; } + @Override public boolean isSelfMuted() { return mSelfMuted; } @@ -170,6 +183,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mSelfMuted = mSelfMuted; } + @Override public boolean isSelfDeafened() { return mSelfDeafened; } @@ -178,6 +192,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mSelfDeafened = mSelfDeafened; } + @Override public boolean isPrioritySpeaker() { return mPrioritySpeaker; } @@ -186,6 +201,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mPrioritySpeaker = mPrioritySpeaker; } + @Override public boolean isRecording() { return mRecording; } @@ -194,6 +210,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mRecording = mRecording; } + @Override public boolean isLocalMuted() { return mLocalMuted; } @@ -202,6 +219,7 @@ public class User extends IUser.Stub implements Comparable<User> { this.mLocalMuted = mLocalMuted; } + @Override public boolean isLocalIgnored() { return mLocalIgnored; } @@ -210,6 +228,7 @@ public class User extends IUser.Stub implements Comparable<User> { mLocalIgnored = localIgnored; } + @Override public TalkState getTalkState() { return mTalkState; } diff --git a/src/main/java/com/morlunk/jumble/net/JumbleConnection.java b/src/main/java/com/morlunk/jumble/net/JumbleConnection.java index 2bbbad9..6fd919f 100644 --- a/src/main/java/com/morlunk/jumble/net/JumbleConnection.java +++ b/src/main/java/com/morlunk/jumble/net/JumbleConnection.java @@ -399,30 +399,44 @@ public class JumbleConnection implements JumbleTCP.TCPConnectionListener, Jumble } public int getServerVersion() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mServerVersion; } public String getServerRelease() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mServerRelease; } public String getServerOSName() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mServerOSName; } public String getServerOSVersion() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mServerOSVersion; } public long getTCPLatency() { + if (!isConnected()) + throw new IllegalStateException("Not connected"); return mLastTCPPing; } public long getUDPLatency() { + if (!isConnected()) + throw new IllegalStateException("Not connected"); return mLastUDPPing; } public int getSession() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mSession; } @@ -431,10 +445,14 @@ public class JumbleConnection implements JumbleTCP.TCPConnectionListener, Jumble * @return the input bandwidth in bps, or -1 if not set. */ public int getMaxBandwidth() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mMaxBandwidth; } public JumbleUDPMessageType getCodec() { + if (!isSynchronized()) + throw new IllegalStateException("Not synchronized"); return mCodec; } diff --git a/src/main/java/com/morlunk/jumble/protocol/ModelHandler.java b/src/main/java/com/morlunk/jumble/protocol/ModelHandler.java index 973fb3a..a14b803 100644 --- a/src/main/java/com/morlunk/jumble/protocol/ModelHandler.java +++ b/src/main/java/com/morlunk/jumble/protocol/ModelHandler.java @@ -24,13 +24,13 @@ import android.os.RemoteException; import android.util.Log; import com.morlunk.jumble.Constants; -import com.morlunk.jumble.IJumbleObserver; import com.morlunk.jumble.R; import com.morlunk.jumble.model.Channel; import com.morlunk.jumble.model.Message; import com.morlunk.jumble.model.User; import com.morlunk.jumble.protobuf.Mumble; import com.morlunk.jumble.protocol.JumbleTCPMessageListener; +import com.morlunk.jumble.util.IJumbleObserver; import com.morlunk.jumble.util.JumbleLogger; import com.morlunk.jumble.util.MessageFormatter; @@ -165,15 +165,10 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { channel.addLink(mChannels.get(link)); } - final Channel finalChannel = channel; - try { - if(newChannel) - mObserver.onChannelAdded(finalChannel); - else - mObserver.onChannelStateUpdated(finalChannel); - } catch (RemoteException e) { - e.printStackTrace(); - } + if(newChannel) + mObserver.onChannelAdded(channel); + else + mObserver.onChannelStateUpdated(channel); } @Override @@ -185,11 +180,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { if(parent != null) { parent.removeSubchannel(channel); } - try { - mObserver.onChannelRemoved(channel); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onChannelRemoved(channel); } } @@ -204,11 +195,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { channel.setPermissions(msg.getPermissions()); if(msg.getChannelId() == 0) // If we're provided permissions for the root channel, we'll apply these as our server permissions. mPermissions = channel.getPermissions(); - try { - mObserver.onChannelPermissionsUpdated(channel); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onChannelPermissionsUpdated(channel); } } @@ -343,11 +330,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { user.setChannel(channel); if(!newUser) { - try { - mObserver.onUserJoinedChannel(finalUser, channel, old); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onUserJoinedChannel(finalUser, channel, old); } Channel sessionChannel = self != null ? self.getChannel() : null; @@ -402,16 +385,10 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { if(msg.hasComment()) user.setComment(msg.getComment()); - final boolean finalNewUser = newUser; - - try { - if(finalNewUser) - mObserver.onUserConnected(finalUser); - else - mObserver.onUserStateUpdated(finalUser); - } catch (RemoteException e) { - e.printStackTrace(); - } + if (newUser) + mObserver.onUserConnected(user); + else + mObserver.onUserStateUpdated(user); } @Override @@ -428,12 +405,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { mLogger.logInfo(mContext.getString(R.string.chat_notify_disconnected, MessageFormatter.highlightString(user.getName()))); user.setChannel(null); - - try { - mObserver.onUserRemoved(user, reason); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onUserRemoved(user, reason); } @Override @@ -466,11 +438,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { else reason = mContext.getString(R.string.perm_denied); } - try { - mObserver.onPermissionDenied(reason); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onPermissionDenied(reason); } @Override @@ -490,11 +458,7 @@ public class ModelHandler extends JumbleTCPMessageListener.Stub { String actorName = sender != null ? sender.getName() : mContext.getString(R.string.server); Message message = new Message(msg.getActor(), actorName, channels, trees, users, msg.getMessage()); - try { - mObserver.onMessageLogged(message); - } catch (RemoteException e) { - e.printStackTrace(); - } + mObserver.onMessageLogged(message); } @Override diff --git a/src/main/java/com/morlunk/jumble/util/IJumbleObserver.java b/src/main/java/com/morlunk/jumble/util/IJumbleObserver.java new file mode 100644 index 0000000..d167da3 --- /dev/null +++ b/src/main/java/com/morlunk/jumble/util/IJumbleObserver.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 Andrew Comminos <andrew@comminos.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +package com.morlunk.jumble.util; + +import com.morlunk.jumble.model.IChannel; +import com.morlunk.jumble.model.IMessage; +import com.morlunk.jumble.model.IUser; + +import java.security.cert.X509Certificate; + +/** + * Created by andrew on 18/10/15. + */ +public interface IJumbleObserver { + void onConnected(); + + void onConnecting(); + + void onDisconnected(JumbleException e); + + void onTLSHandshakeFailed(X509Certificate[] chain); + + void onChannelAdded(IChannel channel); + + void onChannelStateUpdated(IChannel channel); + + void onChannelRemoved(IChannel channel); + + void onChannelPermissionsUpdated(IChannel channel); + + void onUserConnected(IUser user); + + void onUserStateUpdated(IUser user); + + void onUserTalkStateUpdated(IUser user); + + void onUserJoinedChannel(IUser user, IChannel newChannel, IChannel oldChannel); + + void onUserRemoved(IUser user, String reason); + + void onPermissionDenied(String reason); + + void onMessageLogged(IMessage message); + + void onLogInfo(String message); + + void onLogWarning(String message); + + void onLogError(String message); +} diff --git a/src/main/java/com/morlunk/jumble/util/JumbleCallbacks.java b/src/main/java/com/morlunk/jumble/util/JumbleCallbacks.java index 1f67a16..5fc1107 100644 --- a/src/main/java/com/morlunk/jumble/util/JumbleCallbacks.java +++ b/src/main/java/com/morlunk/jumble/util/JumbleCallbacks.java @@ -17,214 +17,158 @@ package com.morlunk.jumble.util; -import android.os.RemoteCallbackList; -import android.os.RemoteException; - -import com.morlunk.jumble.IJumbleObserver; import com.morlunk.jumble.model.IChannel; import com.morlunk.jumble.model.IMessage; import com.morlunk.jumble.model.IUser; +import org.spongycastle.jcajce.provider.asymmetric.X509; + +import java.security.cert.X509Certificate; +import java.util.HashSet; +import java.util.Set; + /** * A composite wrapper around Jumble observers to easily broadcast to each observer. * Created by andrew on 12/07/14. */ -public class JumbleCallbacks extends JumbleObserver.Stub { - private final RemoteCallbackList<IJumbleObserver> mCallbacks; +public class JumbleCallbacks implements IJumbleObserver { + private final Set<IJumbleObserver> mCallbacks; public JumbleCallbacks() { - mCallbacks = new RemoteCallbackList<IJumbleObserver>(); + mCallbacks = new HashSet<>(); } public void registerObserver(IJumbleObserver observer) { - mCallbacks.register(observer); + mCallbacks.add(observer); } public void unregisterObserver(IJumbleObserver observer) { - mCallbacks.unregister(observer); - } - - public void kill() { - mCallbacks.kill(); + mCallbacks.remove(observer); } @Override - public void onConnected() throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onConnected(); + public void onConnected() { + for (IJumbleObserver observer : mCallbacks) { + observer.onConnected(); } - mCallbacks.finishBroadcast(); } @Override - public void onConnecting() throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onConnecting(); + public void onConnecting() { + for (IJumbleObserver observer : mCallbacks) { + observer.onConnecting(); } - mCallbacks.finishBroadcast(); } @Override - public void onDisconnected(JumbleException e) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onDisconnected(e); + public void onDisconnected(JumbleException e) { + for (IJumbleObserver observer : mCallbacks) { + observer.onDisconnected(e); } - mCallbacks.finishBroadcast(); } @Override - public void onTLSHandshakeFailed(ParcelableByteArray cert) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onTLSHandshakeFailed(cert); + public void onTLSHandshakeFailed(X509Certificate[] chain) { + for (IJumbleObserver observer : mCallbacks) { + observer.onTLSHandshakeFailed(chain); } - mCallbacks.finishBroadcast(); } @Override - public void onChannelAdded(IChannel channel) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onChannelAdded(channel); + public void onChannelAdded(IChannel channel) { + for (IJumbleObserver observer : mCallbacks) { + observer.onChannelAdded(channel); } - mCallbacks.finishBroadcast(); } @Override - public void onChannelStateUpdated(IChannel channel) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onChannelStateUpdated(channel); + public void onChannelStateUpdated(IChannel channel) { + for (IJumbleObserver observer : mCallbacks) { + observer.onChannelStateUpdated(channel); } - mCallbacks.finishBroadcast(); } @Override - public void onChannelRemoved(IChannel channel) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onChannelRemoved(channel); + public void onChannelRemoved(IChannel channel) { + for (IJumbleObserver observer : mCallbacks) { + observer.onChannelRemoved(channel); } - mCallbacks.finishBroadcast(); } @Override - public void onChannelPermissionsUpdated(IChannel channel) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onChannelPermissionsUpdated(channel); + public void onChannelPermissionsUpdated(IChannel channel) { + for (IJumbleObserver observer : mCallbacks) { + observer.onChannelPermissionsUpdated(channel); } - mCallbacks.finishBroadcast(); } @Override - public void onUserConnected(IUser user) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onUserConnected(user); + public void onUserConnected(IUser user) { + for (IJumbleObserver observer : mCallbacks) { + observer.onUserConnected(user); } - mCallbacks.finishBroadcast(); } @Override - public void onUserStateUpdated(IUser user) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onUserStateUpdated(user); + public void onUserStateUpdated(IUser user) { + for (IJumbleObserver observer : mCallbacks) { + observer.onUserStateUpdated(user); } - mCallbacks.finishBroadcast(); } @Override - public void onUserTalkStateUpdated(IUser user) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onUserTalkStateUpdated(user); + public void onUserTalkStateUpdated(IUser user) { + for (IJumbleObserver observer : mCallbacks) { + observer.onUserTalkStateUpdated(user); } - mCallbacks.finishBroadcast(); } @Override - public void onUserJoinedChannel(IUser user, IChannel newChannel, IChannel oldChannel) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onUserJoinedChannel(user, newChannel, oldChannel); + public void onUserJoinedChannel(IUser user, IChannel newChannel, IChannel oldChannel) { + for (IJumbleObserver observer : mCallbacks) { + observer.onUserJoinedChannel(user, newChannel, oldChannel); } - mCallbacks.finishBroadcast(); } @Override - public void onUserRemoved(IUser user, String reason) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onUserRemoved(user, reason); + public void onUserRemoved(IUser user, String reason) { + for (IJumbleObserver observer : mCallbacks) { + observer.onUserRemoved(user, reason); } - mCallbacks.finishBroadcast(); } @Override - public void onPermissionDenied(String reason) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onPermissionDenied(reason); + public void onPermissionDenied(String reason) { + for (IJumbleObserver observer : mCallbacks) { + observer.onPermissionDenied(reason); } - mCallbacks.finishBroadcast(); } @Override - public void onMessageLogged(IMessage message) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onMessageLogged(message); + public void onMessageLogged(IMessage message) { + for (IJumbleObserver observer : mCallbacks) { + observer.onMessageLogged(message); } - mCallbacks.finishBroadcast(); } @Override - public void onLogInfo(String message) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onLogInfo(message); + public void onLogInfo(String message) { + for (IJumbleObserver observer : mCallbacks) { + observer.onLogInfo(message); } - mCallbacks.finishBroadcast(); } @Override - public void onLogWarning(String message) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onLogWarning(message); + public void onLogWarning(String message) { + for (IJumbleObserver observer : mCallbacks) { + observer.onLogWarning(message); } - mCallbacks.finishBroadcast(); } @Override - public void onLogError(String message) throws RemoteException { - int i = mCallbacks.beginBroadcast(); - while(i > 0) { - i--; - mCallbacks.getBroadcastItem(i).onLogError(message); + public void onLogError(String message) { + for (IJumbleObserver observer : mCallbacks) { + observer.onLogError(message); } - mCallbacks.finishBroadcast(); } } diff --git a/src/main/java/com/morlunk/jumble/util/JumbleObserver.java b/src/main/java/com/morlunk/jumble/util/JumbleObserver.java index 3c51383..91f2751 100644 --- a/src/main/java/com/morlunk/jumble/util/JumbleObserver.java +++ b/src/main/java/com/morlunk/jumble/util/JumbleObserver.java @@ -17,109 +17,104 @@ package com.morlunk.jumble.util; -import android.os.RemoteException; - -import com.morlunk.jumble.IJumbleObserver; -import com.morlunk.jumble.model.Channel; import com.morlunk.jumble.model.IChannel; import com.morlunk.jumble.model.IMessage; import com.morlunk.jumble.model.IUser; -import com.morlunk.jumble.model.Message; -import com.morlunk.jumble.model.User; + +import java.security.cert.X509Certificate; /** * Stub class for Jumble service observation. * Created by andrew on 31/07/13. */ -public class JumbleObserver extends IJumbleObserver.Stub { +public class JumbleObserver implements IJumbleObserver { @Override - public void onConnected() throws RemoteException { + public void onConnected() { } @Override - public void onConnecting() throws RemoteException { + public void onConnecting() { } @Override - public void onDisconnected(JumbleException e) throws RemoteException { + public void onDisconnected(JumbleException e) { } @Override - public void onTLSHandshakeFailed(ParcelableByteArray cert) throws RemoteException { + public void onTLSHandshakeFailed(X509Certificate[] chain) { } @Override - public void onChannelAdded(IChannel channel) throws RemoteException { + public void onChannelAdded(IChannel channel) { } @Override - public void onChannelStateUpdated(IChannel channel) throws RemoteException { + public void onChannelStateUpdated(IChannel channel) { } @Override - public void onChannelRemoved(IChannel channel) throws RemoteException { + public void onChannelRemoved(IChannel channel) { } @Override - public void onChannelPermissionsUpdated(IChannel channel) throws RemoteException { + public void onChannelPermissionsUpdated(IChannel channel) { } @Override - public void onUserConnected(IUser user) throws RemoteException { + public void onUserConnected(IUser user) { } @Override - public void onUserStateUpdated(IUser user) throws RemoteException { + public void onUserStateUpdated(IUser user) { } @Override - public void onUserTalkStateUpdated(IUser user) throws RemoteException { - + public void onUserTalkStateUpdated(IUser user) { + } @Override - public void onUserJoinedChannel(IUser user, IChannel newChannel, IChannel oldChannel) throws RemoteException { + public void onUserJoinedChannel(IUser user, IChannel newChannel, IChannel oldChannel) { } @Override - public void onUserRemoved(IUser user, String reason) throws RemoteException { + public void onUserRemoved(IUser user, String reason) { } @Override - public void onPermissionDenied(String reason) throws RemoteException { + public void onPermissionDenied(String reason) { } @Override - public void onMessageLogged(IMessage message) throws RemoteException { + public void onMessageLogged(IMessage message) { } @Override - public void onLogInfo(String message) throws RemoteException { + public void onLogInfo(String message) { } @Override - public void onLogWarning(String message) throws RemoteException { + public void onLogWarning(String message) { } @Override - public void onLogError(String message) throws RemoteException { + public void onLogError(String message) { } - } diff --git a/src/main/java/com/morlunk/jumble/util/ParcelableByteArray.java b/src/main/java/com/morlunk/jumble/util/ParcelableByteArray.java deleted file mode 100644 index 801cb0a..0000000 --- a/src/main/java/com/morlunk/jumble/util/ParcelableByteArray.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 <http://www.gnu.org/licenses/>. - */ - -package com.morlunk.jumble.util; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Created by andrew on 05/04/14. - */ -public class ParcelableByteArray implements Parcelable { - - private byte[] mByteArray; - - public ParcelableByteArray(byte[] array) { - mByteArray = array; - } - - private ParcelableByteArray(Parcel in) { - int length = in.readInt(); - mByteArray = new byte[length]; - in.readByteArray(mByteArray); - } - - public byte[] getBytes() { - return mByteArray; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mByteArray.length); - dest.writeByteArray(mByteArray); - } - - public static final Creator<ParcelableByteArray> CREATOR = new Creator<ParcelableByteArray>() { - - @Override - public ParcelableByteArray createFromParcel(Parcel source) { - return new ParcelableByteArray(source); - } - - @Override - public ParcelableByteArray[] newArray(int size) { - return new ParcelableByteArray[size]; - } - }; -} |