/* This file is part of the Android Clementine Remote.
* Copyright (C) 2013, Andreas Muttscheller <asfa194@gmail.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 de.qspool.clementineremote.backend;
import android.net.TrafficStats;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.util.ArrayList;
import java.util.Date;
import de.qspool.clementineremote.App;
import de.qspool.clementineremote.backend.listener.PlayerConnectionListener;
import de.qspool.clementineremote.backend.pb.ClementineMessage;
import de.qspool.clementineremote.backend.pb.ClementineMessage.ErrorMessage;
import de.qspool.clementineremote.backend.pb.ClementineMessageFactory;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.Message.Builder;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.MsgType;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ReasonDisconnect;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ResponseDisconnect;
/**
* This Thread-Class is used to communicate with Clementine
*/
public class ClementinePlayerConnection extends ClementineSimpleConnection
implements Runnable {
public ClementineConnectionHandler mHandler;
public final static int PROCESS_PROTOC = 874456;
public enum ConnectionStatus {IDLE, CONNECTING, NO_CONNECTION, CONNECTED, LOST_CONNECTION, DISCONNECTED}
private final long KEEP_ALIVE_TIMEOUT = 25000; // 25 Second timeout
private final int MAX_RECONNECTS = 5;
private Handler mUiHandler;
private int mLeftReconnects;
private long mLastKeepAlive;
private ArrayList<PlayerConnectionListener> mListeners
= new ArrayList<>();
private ClementineMessage mRequestConnect;
private long mStartTx;
private long mStartRx;
private long mStartTime;
private Thread mIncomingThread;
/**
* Add a new listener for closed connections
*
* @param listener The listener object
*/
public void addPlayerConnectionListener(PlayerConnectionListener listener) {
mListeners.add(listener);
}
public void run() {
// Start the thread
Looper.prepare();
mHandler = new ClementineConnectionHandler(this);
fireOnConnectionStatusChanged(ConnectionStatus.IDLE);
Looper.loop();
}
/**
* Try to connect to Clementine
*
* @param message The Request Object. Stores the ip to connect to.
*/
@Override
public boolean createConnection(ClementineMessage message) {
fireOnConnectionStatusChanged(ConnectionStatus.CONNECTING);
// Reset the connected flag
mLastKeepAlive = 0;
// Now try to connect and set the input and output streams
boolean connected = super.createConnection(message);
// Check if Clementine dropped the connection.
// Is possible when we connect from a public ip and clementine rejects it
if (connected && !mSocket.isClosed()) {
// Now we are connected
// We can now reconnect MAX_RECONNECTS times when
// we get a keep alive timeout
mLeftReconnects = MAX_RECONNECTS;
// Set the current time to last keep alive
setLastKeepAlive(System.currentTimeMillis());
// Until we get a new connection request from ui,
// don't request the first data a second time
mRequestConnect = ClementineMessageFactory
.buildConnectMessage(message.getIp(), message.getPort(),
message.getMessage().getRequestConnect().getAuthCode(),
false,
message.getMessage().getRequestConnect().getDownloader());
// Save started transmitted bytes
int uid = App.getApp().getApplicationInfo().uid;
mStartTx = TrafficStats.getUidTxBytes(uid);
mStartRx = TrafficStats.getUidRxBytes(uid);
mStartTime = new Date().getTime();
// Create a new thread for reading data from Clementine.
// This is done blocking, so we receive the data directly instead of
// waiting for the handler and still be able to send commands directly.
mIncomingThread = new Thread(new Runnable() {
@Override
public void run() {
while (isConnected() && !mIncomingThread.isInterrupted()) {
checkKeepAlive();
ClementineMessage m = getProtoc(3000);
if (!m.isErrorMessage() || m.getErrorMessage() != ErrorMessage.TIMEOUT) {
Message msg = Message.obtain();
msg.obj = m;
msg.arg1 = PROCESS_PROTOC;
mHandler.sendMessage(msg);
}
}
}
});
mIncomingThread.start();
// Get hostname
if (mSocket.getInetAddress() != null) {
App.Clementine.setHostname(mSocket.getInetAddress().getHostName());
}
fireOnConnectionStatusChanged(ConnectionStatus.CONNECTED);
} else {
sendUiMessage(new ClementineMessage(ErrorMessage.NO_CONNECTION));
fireOnConnectionStatusChanged(ConnectionStatus.NO_CONNECTION);
}
return connected;
}
/**
* Process the received protocol buffer
*
* @param clementineMessage The Message received from Clementine
*/
protected void processProtocolBuffer(ClementineMessage clementineMessage) {
fireOnClementineMessageReceived(clementineMessage);
// Close the connection if we have an old proto verion
if (clementineMessage.isErrorMessage()) {
closeConnection(clementineMessage);
} else if (clementineMessage.getMessageType() == MsgType.DISCONNECT) {
closeConnection(clementineMessage);
} else {
sendUiMessage(clementineMessage);
}
}
/**
* Send a message to the ui thread
*
* @param obj The Message containing data
*/
private void sendUiMessage(Object obj) {
Message msg = Message.obtain();
msg.obj = obj;
// Send the Messages
if (mUiHandler != null) {
mUiHandler.sendMessage(msg);
}
}
/**
* Send a request to clementine
*
* @param message The request as a RequestToThread object
* @return true if data was sent, false if not
*/
@Override
public boolean sendRequest(ClementineMessage message) {
// Send the request to Clementine
boolean ret = super.sendRequest(message);
// If we lost connection, try to reconnect
if (!ret) {
//
if (mRequestConnect != null) {
ret = super.createConnection(mRequestConnect);
}
if (!ret) {
// Failed. Close connection
Builder builder = ClementineMessage.getMessageBuilder(MsgType.DISCONNECT);
ResponseDisconnect.Builder disc = builder.getResponseDisconnectBuilder();
disc.setReasonDisconnect(ReasonDisconnect.Server_Shutdown);
builder.setResponseDisconnect(disc);
closeConnection(new ClementineMessage(builder));
}
}
return ret;
}
/**
* Disconnect from Clementine
*
* @param message The RequestDisconnect Object
*/
@Override
public void disconnect(ClementineMessage message) {
if (isConnected()) {
// Set the Connected flag to false, so the loop in
// checkForData() is interrupted
super.disconnect(message);
// and close the connection
closeConnection(message);
}
}
/**
* Close the socket and the streams
*/
private void closeConnection(ClementineMessage clementineMessage) {
// Disconnect socket
closeSocket();
sendUiMessage(clementineMessage);
try {
mIncomingThread.interrupt();
mIncomingThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Fire the listener
if (clementineMessage.isErrorMessage() &&
(clementineMessage.getErrorMessage() == ErrorMessage.IO_EXCEPTION ||
clementineMessage.getErrorMessage() == ErrorMessage.KEEP_ALIVE_TIMEOUT)) {
fireOnConnectionStatusChanged(ConnectionStatus.LOST_CONNECTION);
}
fireOnConnectionStatusChanged(ConnectionStatus.DISCONNECTED);
// Close thread
Looper.myLooper().quit();
}
/**
* Check the keep alive timeout.
* If we reached the timeout, we can assume, that we lost the connection
*/
private void checkKeepAlive() {
if (mLastKeepAlive > 0
&& (System.currentTimeMillis() - mLastKeepAlive) > KEEP_ALIVE_TIMEOUT) {
// Check if we shall reconnect
while (mLeftReconnects > 0) {
closeSocket();
if (super.createConnection(mRequestConnect)) {
mLeftReconnects = MAX_RECONNECTS;
break;
}
mLeftReconnects--;
}
// We tried, but the server isn't there anymore
if (mLeftReconnects == 0) {
Message msg = Message.obtain();
msg.obj = new ClementineMessage(ErrorMessage.KEEP_ALIVE_TIMEOUT);
msg.arg1 = PROCESS_PROTOC;
mHandler.sendMessage(msg);
}
}
}
/**
* Fire the event to all listeners
*
* @param status The current connection status
*/
private void fireOnConnectionStatusChanged(ConnectionStatus status) {
for (PlayerConnectionListener listener : mListeners) {
listener.onConnectionStatusChanged(status);
}
}
/**
* Fire the event to all listeners
*/
private void fireOnClementineMessageReceived(ClementineMessage msg) {
for (PlayerConnectionListener listener : mListeners) {
listener.onClementineMessageReceived(msg);
}
}
/**
* Set the ui Handler, to which the thread should talk to
*
* @param playerHandler The Handler
*/
public void setUiHandler(Handler playerHandler) {
this.mUiHandler = playerHandler;
}
/**
* Set the last keep alive timestamp
*
* @param lastKeepAlive The time
*/
public void setLastKeepAlive(long lastKeepAlive) {
this.mLastKeepAlive = lastKeepAlive;
}
public long getStartTx() {
return mStartTx;
}
public long getStartRx() {
return mStartRx;
}
public long getStartTime() {
return mStartTime;
}
public ClementineMessage getRequestConnect() {
return mRequestConnect;
}
}