/*******************************************************************************
* Copyright 2012 Keith Johnson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.ubergeek42.WeechatAndroid.service;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.support.annotation.Nullable;
import com.ubergeek42.WeechatAndroid.R;
import com.ubergeek42.WeechatAndroid.relay.BufferList;
import com.ubergeek42.weechat.relay.RelayConnection;
import com.ubergeek42.weechat.relay.RelayMessage;
import com.ubergeek42.weechat.relay.connection.AbstractConnection.StreamClosed;
import com.ubergeek42.weechat.relay.connection.Connection;
import com.ubergeek42.weechat.relay.connection.PlainConnection;
import com.ubergeek42.weechat.relay.connection.SSHConnection;
import com.ubergeek42.weechat.relay.connection.SSLConnection;
import com.ubergeek42.weechat.relay.connection.WebSocketConnection;
import com.ubergeek42.weechat.relay.protocol.RelayObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.channels.UnresolvedAddressException;
import java.util.EnumSet;
import de.greenrobot.event.EventBus;
import static com.ubergeek42.WeechatAndroid.service.Events.*;
import static com.ubergeek42.WeechatAndroid.utils.Constants.*;
public class RelayService extends Service implements Connection.Observer {
private static Logger logger = LoggerFactory.getLogger("RelayService");
final private static boolean DEBUG = true;
final private static boolean DEBUG_CONNECTION = true;
public RelayConnection connection;
private Connectivity connectivity;
private PingActionReceiver ping;
private Handler thandler; // thread "doge" used for connecting/disconnecting
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// status & life cycle
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onCreate() {
if (DEBUG) logger.debug("onCreate()");
super.onCreate();
// prepare handler that will run on a separate thread
HandlerThread handlerThread = new HandlerThread("doge");
handlerThread.start();
thandler = new Handler(handlerThread.getLooper());
connectivity = new Connectivity();
connectivity.register(this);
ping = new PingActionReceiver(this);
EventBus.getDefault().register(this);
}
@Override
public void onDestroy() {
if (DEBUG) logger.debug("onDestroy()");
P.saveStuff();
connectivity.unregister();
super.onDestroy();
EventBus.getDefault().unregister(this);
}
@Nullable @Override public IBinder onBind(Intent intent) {
return null;
}
@SuppressWarnings("unused")
public void onEvent(SendMessageEvent event) {
logger.debug("onEvent({})", event);
connection.sendMessage(event.message);
}
final public static String ACTION_START = "com.ubergeek42.WeechatAndroid.START";
final public static String ACTION_STOP = "com.ubergeek42.WeechatAndroid.STOP";
// this method is called:
// * whenever app calls startService() (that means on each screen rotate)
// * when service is recreated by system after OOM kill. (intent = null)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG_CONNECTION) logger.debug("onStartCommand({}, {}, {})", intent, flags, startId);
if (intent == null || ACTION_START.equals(intent.getAction())) {
if (state.contains(STATE.STOPPED)) {
P.loadConnectionPreferences();
start();
}
} else if (ACTION_STOP.equals(intent.getAction())) {
if (!state.contains(STATE.STOPPED)) {
stop();
}
}
return START_STICKY;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// connect/disconnect
////////////////////////////////////////////////////////////////////////////////////////////////
private static final long WAIT_BEFORE_WAIT_MESSAGE_DELAY = 5;
private static final long DELAYS[] = new long[] {5, 15, 30, 60, 120, 300, 600, 900};
// called by user and when disconnected
private void start() {
if (DEBUG_CONNECTION) logger.debug("start()");
if (!state.contains(STATE.STOPPED)) {
logger.error("start() run while state != STATE.STOPPED");
return;
}
state = EnumSet.of(STATE.STARTED);
EventBus.getDefault().postSticky(new StateChangedEvent(state));
P.setServiceAlive(true);
_start();
}
// called by ↑ and Connectivity
protected void _start() {
if (DEBUG_CONNECTION) logger.debug("_start()");
thandler.removeCallbacksAndMessages(null);
thandler.post(new Runnable() {
int reconnects = 0;
final String ticker = getString(R.string.notification_connecting);
final String content = getString(R.string.notification_connecting_details);
final String contentNow = getString(R.string.notification_connecting_details_now);
Runnable connectRunner = new Runnable() {
@Override public void run() {
if (state.contains(STATE.AUTHENTICATED)) return;
if (DEBUG_CONNECTION) logger.debug("start(): not connected; connecting now");
Notificator.showMain(RelayService.this, String.format(ticker, P.printableHost), contentNow, null);
switch (connect()) {
case LATER: return; // wait for Connectivity
case IMPOSSIBLE: stop(); break; // can't connect due to ?!?!
case POSSIBLE: thandler.postDelayed(notifyRunner, WAIT_BEFORE_WAIT_MESSAGE_DELAY * 1000);
}
}
};
Runnable notifyRunner = new Runnable() {
@Override public void run() {
if (state.contains(STATE.AUTHENTICATED)) return;
long delay = DELAYS[reconnects < DELAYS.length ? reconnects : DELAYS.length - 1];
if (DEBUG_CONNECTION) logger.debug("start(): waiting {} seconds", delay);
Notificator.showMain(RelayService.this, String.format(ticker, P.printableHost), String.format(content, delay), null);
reconnects++;
thandler.postDelayed(connectRunner, delay * 1000);
}
};
@Override public void run() {
connectRunner.run();
}
});
}
// called by user and when there was a fatal exception while trying to connect
protected void stop() {
if (DEBUG_CONNECTION) logger.debug("stop()");
if (state.contains(STATE.STOPPED)) {
logger.error("stop() run while state == STATE.STOPPED");
return;
}
if (state.contains(STATE.AUTHENTICATED)) goodbye();
state = EnumSet.of(STATE.STOPPED);
EventBus.getDefault().postSticky(new StateChangedEvent(state));
interrupt();
stopSelf();
P.setServiceAlive(false);
}
// called by ↑ and PingActionReceiver
// close whatever connection we have in a thread, may result in a call to onStateChanged
protected void interrupt() {
thandler.removeCallbacksAndMessages(null);
thandler.post(new Runnable() {
@Override public void run() {
if (connection != null) connection.disconnect();
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////
private enum TRY {POSSIBLE, LATER, IMPOSSIBLE}
private TRY connect() {
if (DEBUG_CONNECTION) logger.debug("connect()");
if (connection != null)
connection.disconnect();
if (!connectivity.isNetworkAvailable()) {
Notificator.showMain(this, getString(R.string.notification_waiting_network), null);
return TRY.LATER;
}
Connection conn;
try {
switch (P.connectionType) {
case PREF_TYPE_SSH: conn = new SSHConnection(P.host, P.port, P.sshHost, P.sshPort, P.sshUser, P.sshPass, P.sshKey, P.sshKnownHosts); break;
case PREF_TYPE_SSL: conn = new SSLConnection(P.host, P.port, P.sslSocketFactory); break;
case PREF_TYPE_WEBSOCKET: conn = new WebSocketConnection(P.host, P.port, P.wsPath, null); break;
case PREF_TYPE_WEBSOCKET_SSL: conn = new WebSocketConnection(P.host, P.port, P.wsPath, P.sslSocketFactory); break;
default: conn = new PlainConnection(P.host, P.port); break;
}
} catch (Exception e) {
logger.error("connect(): exception while creating connection", e);
onException(e);
return TRY.IMPOSSIBLE;
}
connection = new RelayConnection(conn, P.pass);
connection.setObserver(this);
connection.connect();
return TRY.POSSIBLE;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// callbacks
////////////////////////////////////////////////////////////////////////////////////////////////
public enum STATE {
STOPPED,
STARTED,
AUTHENTICATED,
LISTED,
}
public EnumSet<STATE> state = EnumSet.of(STATE.STOPPED);
@Override public void onStateChanged(Connection.STATE s) {
logger.debug("onStateChanged({})", s);
switch (s) {
case CONNECTING:
case CONNECTED:
return;
case AUTHENTICATED:
state = EnumSet.of(STATE.STARTED, STATE.AUTHENTICATED);
Notificator.showMain(this, getString(R.string.notification_connected_to, P.printableHost), null);
hello();
break;
case BUFFERS_LISTED:
state = EnumSet.of(STATE.STARTED, STATE.AUTHENTICATED, STATE.LISTED);
break;
case DISCONNECTED:
if (state.contains(STATE.STOPPED)) return;
if (!state.contains(STATE.AUTHENTICATED)) return; // continue connecting
state = EnumSet.of(STATE.STOPPED);
goodbye();
if (P.reconnect) start();
else stopSelf();
}
EventBus.getDefault().postSticky(new StateChangedEvent(state));
}
private void hello() {
ping.scheduleFirstPing();
BufferList.launch(this);
SyncAlarmReceiver.start(this);
}
private void goodbye() {
SyncAlarmReceiver.stop(this);
BufferList.stop();
ping.unschedulePing();
//P.saveStuff();
}
////////////////////////////////////////////////////////////////////////////////////////////////
public static class ExceptionWrapper extends Exception {
public ExceptionWrapper(Exception cause, String message) {
super(message);
initCause(cause);
}
}
// ALWAYS followed by onStateChanged(STATE.DISCONNECTED); might be StreamClosed
@Override public void onException(Exception e) {
logger.error("onException({})", e.getClass().getSimpleName());
if (e instanceof StreamClosed && (!state.contains(STATE.AUTHENTICATED)))
e = new ExceptionWrapper(e, getString(R.string.relay_error_server_closed));
else if (e instanceof UnresolvedAddressException)
e = new ExceptionWrapper(e, getString(R.string.relay_error_resolve, P.connectionType.equals(PREF_TYPE_SSH) ? P.sshHost : P.host));
EventBus.getDefault().post(new ExceptionEvent(e));
}
////////////////////////////////////////////////////////////////////////////////////////////////
private final static RelayObject[] NULL = {null};
@Override public void onMessage(RelayMessage message) {
ping.onMessage();
RelayObject[] objects = message.getObjects() == null ? NULL : message.getObjects();
String id = message.getID();
for (RelayObject object : objects)
BufferList.handleMessage(object, id);
}
}