com.ubergeek42.WeechatAndroid.service.RelayServiceBackbone.java Source code

Java tutorial

Introduction

Here is the source code for com.ubergeek42.WeechatAndroid.service.RelayServiceBackbone.java

Source

/*******************************************************************************
 * 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.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;

import com.ubergeek42.WeechatAndroid.BuildConfig;
import com.ubergeek42.WeechatAndroid.R;
import com.ubergeek42.WeechatAndroid.WeechatActivity;
import com.ubergeek42.WeechatAndroid.WeechatPreferencesActivity;
import com.ubergeek42.weechat.relay.RelayConnection;
import com.ubergeek42.weechat.relay.RelayConnectionHandler;
import com.ubergeek42.weechat.relay.RelayMessageHandler;
import com.ubergeek42.weechat.relay.connection.IConnection;
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.StunnelConnection;
import com.ubergeek42.weechat.relay.connection.WebSocketConnection;
import com.ubergeek42.weechat.relay.messagehandler.UpgradeHandler;
import com.ubergeek42.weechat.relay.messagehandler.UpgradeObserver;
import com.ubergeek42.weechat.relay.protocol.RelayObject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public abstract class RelayServiceBackbone extends Service
        implements RelayConnectionHandler, OnSharedPreferenceChangeListener, UpgradeObserver {

    private static Logger logger = LoggerFactory.getLogger("RelayServiceBackbone");
    final private static boolean DEBUG = false;
    final private static boolean DEBUG_CONNECTION = false;
    final private static boolean DEBUG_NOTIFICATIONS = false;

    private static final int NOTIFICATION_ID = 42;
    private static final int NOTIFICATION_HIGHLIGHT_ID = 43;
    private NotificationManager notificationManger;
    private Thread upgrading;

    final static private String PREF_AUTO_CONNECT = "autoconnect";

    final static private String PREF_HOST = "host";
    final static private String PREF_PASSWORD = "password";
    final static private String PREF_PORT = "port";

    final static private String PREF_PING_ENABLED = "ping_enabled";
    final static private String PREF_PING_IDLE = "ping_idle";
    final static private String PREF_PING_TIMEOUT = "ping_timeout";

    final static private String PREF_STUNNEL_CERT = "stunnel_cert";
    final static private String PREF_STUNNEL_PASS = "stunnel_pass";
    final static private String PREF_SSH_HOST = "ssh_host";
    final static private String PREF_SSH_PORT = "ssh_port";
    final static private String PREF_SSH_USER = "ssh_user";
    final static private String PREF_SSH_PASS = "ssh_pass";
    final static private String PREF_SSH_KEYFILE = "ssh_keyfile";

    final static private String PREF_CONNECTION_TYPE = "connection_type";
    final static private String PREF_TYPE_SSH = "ssh";
    final static private String PREF_TYPE_STUNNEL = "stunnel";
    final static private String PREF_TYPE_SSL = "ssl";
    final static private String PREF_TYPE_WEBSOCKET = "websocket";
    final static private String PREF_TYPE_WEBSOCKET_SSL = "websocket-ssl";
    final static private String PREF_TYPE_PLAIN = "plain";

    final static private String PREF_NOTIFICATION_ENABLE = "notification_enable";
    final static private String PREF_NOTIFICATION_SOUND = "notification_sound";
    final static private String PREF_NOTIFICATION_LIGHT = "notification_light";
    final static private String PREF_NOTIFICATION_VIBRATE = "notification_vibrate";
    final static private String PREF_NOTIFICATION_TICKER = "notification_ticker";

    final static private String PREF_MUST_STAY_DISCONNECTED = "wow!";

    private String host;
    private int port;
    private String pass;

    RelayConnection connection;
    HashSet<RelayConnectionHandler> connectionHandlers = new HashSet<RelayConnectionHandler>();

    SharedPreferences prefs;
    SSLHandler certmanager;
    X509Certificate untrustedCert;
    private AlarmManager alarmMgr;

    int hot_count = 0;
    volatile long lastMessageReceivedAt = 0;

    /** mainly used to tell the user if we are REconnected */
    private volatile boolean disconnected;
    private boolean already_had_intent;
    private volatile boolean network_unavailable;

    /** handler that resides on a separate thread. useful for connection/etc */
    Handler thandler;

    // for some reason, this java can't have binary literals...
    public final static int DISCONNECTED = Integer.parseInt("00001", 2);
    public final static int CONNECTING = Integer.parseInt("00010", 2);
    public final static int CONNECTED = Integer.parseInt("00100", 2);
    public final static int AUTHENTICATED = Integer.parseInt("01000", 2);
    public final static int BUFFERS_LISTED = Integer.parseInt("10000", 2);
    int connection_status = DISCONNECTED;

    private BroadcastReceiver connectivityActionReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            NetworkInfo networkInfo = connectivityManager().getActiveNetworkInfo();
            if (networkInfo != null && networkInfo.isConnected()) {
                if (network_unavailable && mustAutoConnect())
                    startThreadedConnectLoop(true);
                network_unavailable = false;
            }
        }
    };

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////////////////////// status & life cycle
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** check status of connection
     ** @param status one of DISCONNECTED, CONNECTING, CONNECTED, AUTHENTICATED, BUFFERS_LISTED
     ** @return true if connection corresponds to one of these */
    public boolean isConnection(int status) {
        return (connection_status & status) != 0;
    }

    @Override
    public void onCreate() {
        if (DEBUG)
            logger.debug("onCreate()");
        super.onCreate();

        prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
        prefs.registerOnSharedPreferenceChangeListener(this);
        notificationManger = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        // prepare handler that will run on a separate thread
        HandlerThread handler_thread = new HandlerThread("doge");
        handler_thread.start();
        thandler = new Handler(handler_thread.getLooper());

        startForeground(NOTIFICATION_ID, buildNotification(null, "Tap to connect", null));

        disconnected = false;
        already_had_intent = false;
        network_unavailable = false;

        // Prepare for dealing with SSL certs
        certmanager = new SSLHandler(new File(getDir("sslDir", Context.MODE_PRIVATE), "keystore.jks"));

        registerReceiver(connectivityActionReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        alarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    }

    @Override
    public void onDestroy() {
        if (DEBUG)
            logger.debug("onDestroy()");
        prefs.edit().remove(PREF_MUST_STAY_DISCONNECTED).commit(); // forget current connection status
        notificationManger.cancelAll();
        unregisterReceiver(connectivityActionReceiver);
        super.onDestroy();
    }

    /** this method is called:
     **     * whenever app calls startService() (that means on each screen rotate)
     **     * when service is recreated by system after OOM kill. in this case,
     **       the intent is 'null' (and we can say we are 're'connecting.
     ** we are using this method because it's the only way to know if we are returning from OOM kill.
     ** but we want to only run this ONCE after onCreate*/
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (DEBUG_CONNECTION)
            logger.debug("onStartCommand({}, {}, {}); had intent? {}",
                    new Object[] { intent, flags, startId, already_had_intent });
        if (!already_had_intent) {
            if (mustAutoConnect())
                startThreadedConnectLoop(intent == null);
            already_had_intent = true;
        }
        return START_STICKY;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////////////////////// notifications!
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** build notification without displaying it
     *
     * @param tickerText text that flashes a bit, can be null
     * @param content text that appears under title
     * @param intent intent that's executed on notification click, can be null
     * @return built notification */

    @TargetApi(16)
    private Notification buildNotification(@Nullable String tickerText, @NonNull String content,
            @Nullable PendingIntent intent) {
        if (DEBUG_NOTIFICATIONS)
            logger.debug("buildNotification({}, {}, {})", new Object[] { tickerText, content, intent });
        PendingIntent contentIntent;
        contentIntent = (intent != null) ? intent
                : PendingIntent.getActivity(this, 0, new Intent(this, WeechatActivity.class),
                        PendingIntent.FLAG_CANCEL_CURRENT);

        int icon;
        if (!isConnection(AUTHENTICATED)) {
            if (isConnection(CONNECTING))
                icon = R.drawable.ic_connecting;
            else
                icon = R.drawable.ic_disconnected;
        } else if (hot_count == 0)
            icon = R.drawable.ic_connected;
        else
            icon = R.drawable.ic_hot;

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setContentIntent(contentIntent).setSmallIcon(icon)
                .setContentTitle("WeechatAndroid v" + BuildConfig.VERSION_NAME).setContentText(content)
                .setWhen(System.currentTimeMillis());

        if (prefs.getBoolean(PREF_NOTIFICATION_TICKER, true)) {
            builder.setTicker(tickerText);
        }

        Notification notification = builder.build();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            notification.priority = Notification.PRIORITY_MIN;
        }
        notification.flags |= Notification.FLAG_ONGOING_EVENT;
        return notification;
    }

    /** display default notification */
    void displayDefaultNotification() {
        showNotification(null, getString(R.string.notification_connected_to) + host);
    }

    /** display custom notification with specific intent */
    private void showNotification(@Nullable String tickerText, @NonNull String content,
            @NonNull PendingIntent intent) {
        notificationManger.notify(NOTIFICATION_ID, buildNotification(tickerText, content, intent));
    }

    /** display custom notification with default intent */
    private void showNotification(@Nullable String tickerText, @NonNull String content) {
        notificationManger.notify(NOTIFICATION_ID, buildNotification(tickerText, content, null));
    }

    private static final int BUFFER = 0, LINE = 1;

    /** display notification with a hot message
     ** clicking on it will open the buffer & scroll up to the hot line, if needed
     ** mind that SOMETIMES hot_count will be larger than hot_list, because
     ** it's filled from hotlist data and hot_list only contains lines that
     ** arrived in real time. so we add (message not available) if there are NO lines to display
     ** and add "..." if there are some lines to display, but not all */
    public void changeHotNotification(boolean new_highlight) {
        if (DEBUG_NOTIFICATIONS)
            logger.warn("changeHotNotification({})", new_highlight);
        final int hot_count = BufferList.getHotCount();
        final List<String[]> hot_list = BufferList.hot_list;

        if (hot_count == 0) {
            notificationManger.cancel(NOTIFICATION_HIGHLIGHT_ID);
        } else {
            // find our target buffer. if ALL items point to the same buffer, use it,
            // otherwise, go to buffer list ( "")
            if (!prefs.getBoolean(PREF_NOTIFICATION_ENABLE, true)) {
                return;
            }
            Set<String> set = new HashSet<String>();
            for (String[] h : hot_list)
                set.add(h[BUFFER]);
            String target_buffer = (hot_count == hot_list.size() && set.size() == 1) ? hot_list.get(0)[BUFFER] : "";
            if (DEBUG_NOTIFICATIONS)
                logger.warn("...target='{}', hot_count={}, set.size()={}",
                        new Object[] { target_buffer, hot_count, set.size() });

            // prepare intent
            Intent i = new Intent(this, WeechatActivity.class).putExtra("full_name", target_buffer);
            PendingIntent contentIntent = PendingIntent.getActivity(this, 1, i, PendingIntent.FLAG_UPDATE_CURRENT);

            // prepare notification
            // make the ticker the LAST message
            String message = hot_list.size() == 0 ? getString(R.string.hot_message_not_available)
                    : hot_list.get(hot_list.size() - 1)[LINE];
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                    .setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_hot)
                    .setContentTitle(getResources().getQuantityString(R.plurals.hot_messages, hot_count, hot_count))
                    .setContentText(message);

            // display several lines only if we have at least one visible line and
            // 2 or more lines total. that is, either display full list of lines or
            // one ore more visible lines and "..."
            if (hot_list.size() > 0 && hot_count > 1) {
                NotificationCompat.InboxStyle inbox = new NotificationCompat.InboxStyle().setSummaryText(host);

                for (String[] buffer_to_line : hot_list)
                    inbox.addLine(buffer_to_line[LINE]);
                if (hot_list.size() < hot_count)
                    inbox.addLine("");

                builder.setContentInfo(String.valueOf(hot_count));
                builder.setStyle(inbox);
            }

            if (new_highlight) {
                builder.setTicker(message);
                builder.setSound(Uri.parse(prefs.getString(PREF_NOTIFICATION_SOUND, "")));
                if (!prefs.getBoolean(PREF_NOTIFICATION_VIBRATE, true)) {
                    builder.setVibrate(new long[] { 0, 0, 0, 0 });
                }
                if (prefs.getBoolean(PREF_NOTIFICATION_LIGHT, false)) {
                    builder.setLights(Color.WHITE, 100, 100);
                }
            }

            notificationManger.notify(NOTIFICATION_HIGHLIGHT_ID, builder.build());
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////////////////////// connect/disconnect
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** only auto-connect if auto-connect is on ON in the prefs and
     ** the user did not disconnect by tapping disconnect in menu */
    private boolean mustAutoConnect() {
        return prefs.getBoolean(PREF_AUTO_CONNECT, false) && !prefs.getBoolean(PREF_MUST_STAY_DISCONNECTED, false);
    }

    private static final boolean CONNECTION_IMPOSSIBLE = false;
    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 };

    public void startThreadedConnectLoop(final boolean reconnecting) {
        if (DEBUG_CONNECTION)
            logger.debug("startThreadedConnectLoop()");
        if (connection != null && connection.isConnected()) {
            logger.error("startThreadedConnectLoop() run while connected!!");
            return;
        }
        prefs.edit().putBoolean(PREF_MUST_STAY_DISCONNECTED, false).commit();
        thandler.removeCallbacksAndMessages(null);
        thandler.post(new Runnable() {
            int reconnects = 0;
            int ticker = reconnecting ? R.string.notification_reconnecting : R.string.notification_connecting;
            int content = reconnecting ? R.string.notification_reconnecting_details
                    : R.string.notification_connecting_details;
            int content_now = reconnecting ? R.string.notification_reconnecting_details_now
                    : R.string.notification_connecting_details_now;

            Runnable connectRunner = new Runnable() {
                @Override
                public void run() {
                    if (DEBUG_CONNECTION)
                        logger.debug("...run()");
                    if (connection != null && connection.isConnected())
                        return;
                    if (DEBUG_CONNECTION)
                        logger.debug("...not connected; connecting now");
                    connection_status = CONNECTING;
                    showNotification(String.format(getString(ticker), prefs.getString("host", null)),
                            String.format(getString(content_now)));
                    if (connect() != CONNECTION_IMPOSSIBLE)
                        thandler.postDelayed(notifyRunner, WAIT_BEFORE_WAIT_MESSAGE_DELAY * 1000);
                }
            };

            Runnable notifyRunner = new Runnable() {
                @Override
                public void run() {
                    if (DEBUG_CONNECTION)
                        logger.debug("...run()");
                    if (connection != null && connection.isConnected())
                        return;
                    long delay = DELAYS[reconnects < DELAYS.length ? reconnects : DELAYS.length - 1];
                    if (DEBUG_CONNECTION)
                        logger.debug("...waiting {} seconds", delay);
                    showNotification(String.format(getString(ticker), host),
                            String.format(getString(content), delay));
                    reconnects++;
                    thandler.postDelayed(connectRunner, delay * 1000);
                }
            };

            @Override
            public void run() {
                connectRunner.run();
            }
        });
    }

    // do the actual shutdown on a separate thread (to avoid NetworkOnMainThreadException on Android 3.0+)
    // remember that we are down lest we are reconnected when application
    // kills the service and restores it back later
    public void startThreadedDisconnect(boolean mustStayDisconnected) {
        if (DEBUG_CONNECTION)
            logger.debug("startThreadedDisconnect()");
        prefs.edit().putBoolean(PREF_MUST_STAY_DISCONNECTED, mustStayDisconnected).commit();
        thandler.removeCallbacksAndMessages(null);
        thandler.post(new Runnable() {
            @Override
            public void run() {
                if (connection != null)
                    connection.disconnect();
            }
        });
    }

    public void startThreadedDisconnect() {
        startThreadedDisconnect(true);
    }

    @TargetApi(19)
    void schedulePing(long triggerAt, Bundle extras) {
        Intent intent = new Intent(PingActionReceiver.PING_ACTION);
        intent.putExtras(extras);

        PendingIntent alarmIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, alarmIntent);
        } else {
            alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, alarmIntent);
        }
    }

    void schedulePing(long triggerAt) {
        schedulePing(triggerAt, new Bundle());
    }

    long pingIdleTime() {
        return Integer.parseInt(prefs.getString(PREF_PING_IDLE, "300")) * 1000;
    }

    long pingTimeout() {
        return Integer.parseInt(prefs.getString(PREF_PING_TIMEOUT, "30")) * 1000;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** try connect once
     ** @return true if connection attempt has been made and false if connection is not possible */
    private boolean connect() {
        if (DEBUG_CONNECTION)
            logger.debug("connect()");

        // only connect if we aren't already connected
        if ((connection != null) && (connection.isConnected()))
            return false;

        if (connection != null)
            connection.disconnect();

        // load the preferences
        host = prefs.getString(PREF_HOST, null);
        pass = prefs.getString(PREF_PASSWORD, null);
        port = Integer.parseInt(prefs.getString(PREF_PORT, "8001"));

        // if no host defined, signal user to edit their preferences
        if (host == null || pass == null) {
            Intent i = new Intent(this, WeechatPreferencesActivity.class);
            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
            showNotification(getString(R.string.notification_update_settings_details),
                    getString(R.string.notification_update_settings), contentIntent);
            return false;
        }

        NetworkInfo networkInfo = connectivityManager().getActiveNetworkInfo();
        if (networkInfo == null || !networkInfo.isConnected()) {
            Intent i = new Intent(this, WeechatActivity.class);
            PendingIntent contentIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
            showNotification(getString(R.string.notification_network_unavailable_details),
                    getString(R.string.notification_network_unavailable), contentIntent);
            network_unavailable = true;
            return false;
        }

        IConnection conn;
        String connType = prefs.getString(PREF_CONNECTION_TYPE, PREF_TYPE_PLAIN);
        if (connType.equals(PREF_TYPE_SSH)) {
            SSHConnection tmp = new SSHConnection(host, port);
            tmp.setSSHHost(prefs.getString(PREF_SSH_HOST, ""));
            tmp.setSSHPort(prefs.getString(PREF_SSH_PORT, "22"));
            tmp.setSSHUsername(prefs.getString(PREF_SSH_USER, ""));
            tmp.setSSHKeyFile(prefs.getString(PREF_SSH_KEYFILE, ""));
            tmp.setSSHPassword(prefs.getString(PREF_SSH_PASS, ""));
            conn = tmp;
        } else if (connType.equals(PREF_TYPE_STUNNEL)) {
            StunnelConnection tmp = new StunnelConnection(host, port);
            tmp.setStunnelCert(prefs.getString(PREF_STUNNEL_CERT, ""));
            tmp.setStunnelKey(prefs.getString(PREF_STUNNEL_PASS, ""));
            conn = tmp;
        } else if (connType.equals(PREF_TYPE_SSL)) {
            SSLConnection tmp = new SSLConnection(host, port);
            tmp.setSSLKeystore(certmanager.sslKeystore);
            conn = tmp;
        } else if (connType.equals(PREF_TYPE_WEBSOCKET)) {
            WebSocketConnection tmp = new WebSocketConnection(host, port, false);
            conn = tmp;
        } else if (connType.equals(PREF_TYPE_WEBSOCKET_SSL)) {
            WebSocketConnection tmp = new WebSocketConnection(host, port, true);
            tmp.setSSLKeystore(certmanager.sslKeystore);
            conn = tmp;
        } else {
            conn = new PlainConnection(host, port);
        }
        conn.addConnectionHandler(this);
        connection = new RelayConnection(conn, pass);
        connection.connect();
        return true;
    }

    private ConnectivityManager connectivityManager() {
        return (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////////////////////// callbacks
    ////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public void onConnecting() {
        if (DEBUG)
            logger.debug("onConnecting()");
        connection_status = CONNECTING;
        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onConnecting();
    }

    @Override
    public void onConnect() {
        if (DEBUG)
            logger.debug("onConnect()");
        connection_status = CONNECTED;

        if (prefs.getBoolean(PREF_PING_ENABLED, true)) {
            long triggerAt = SystemClock.elapsedRealtime() + pingTimeout();
            schedulePing(triggerAt);
        }

        connection.addHandler("*", lastMessageHandler);

        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onConnect();
    }

    @Override
    public void onAuthenticated() {
        if (DEBUG)
            logger.debug("onAuthenticated()");
        connection_status = CONNECTED | AUTHENTICATED;

        if (disconnected) {
            showNotification(getString(R.string.notification_reconnected_to) + host,
                    getString(R.string.notification_connected_to) + host);
        } else {
            String tmp = getString(R.string.notification_connected_to) + host;
            showNotification(tmp, tmp);
        }
        disconnected = false;

        startHandlingBoneEvents();

        // handle weechat upgrading & buffer listed event
        UpgradeHandler uh = new UpgradeHandler(this);
        connection.addHandler("_upgrade", uh);
        connection.addHandler("_upgrade_ended", uh);

        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onAuthenticated();
    }

    @Override
    public void onAuthenticationFailed() {
        if (DEBUG)
            logger.debug("onAuthenticateFailed()");

        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onAuthenticationFailed();
    }

    abstract void startHandlingBoneEvents();

    @Override
    public void onBuffersListed() {
        if (DEBUG)
            logger.debug("onBuffersListed()");
        connection_status = CONNECTED | AUTHENTICATED | BUFFERS_LISTED;
        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onBuffersListed();
    }

    @Override
    public void onDisconnect() {
        if (DEBUG)
            logger.debug("onDisconnect()");
        connection_status = DISCONNECTED;

        // Only do the disconnect handler once
        if (disconnected)
            return;
        disconnected = true;

        Intent intent = new Intent(PingActionReceiver.PING_ACTION);
        PendingIntent alarmIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_NO_CREATE);
        alarmMgr.cancel(alarmIntent);

        // automatically attempt reconnection if enabled (and if we aren't shutting down)
        if (mustAutoConnect()) {
            startThreadedConnectLoop(true);
        } else {
            String tmp = getString(R.string.notification_disconnected);
            showNotification(tmp, tmp);
        }

        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onDisconnect();
    }

    @Override
    public void onError(String error, Object extraData) {
        if (DEBUG)
            logger.error("onError(..., ...)", error, extraData);
        for (RelayConnectionHandler rch : connectionHandlers)
            rch.onError(error, extraData);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // Load/refresh preferences
        if (key.equals(PREF_HOST)) {
            host = prefs.getString(key, null);
        } else if (key.equals(PREF_PASSWORD)) {
            pass = prefs.getString(key, null);
        } else if (key.equals(PREF_PORT)) {
            port = Integer.parseInt(prefs.getString(key, "8001"));
        }
    }

    @Override
    public void onUpgrade() {
        // Don't do this twice
        if (upgrading != null && upgrading.isAlive())
            return;

        // Basically just reconnect on upgrade
        upgrading = new Thread(new Runnable() {
            @Override
            public void run() {
                showNotification(getString(R.string.notification_upgrading),
                        getString(R.string.notification_upgrading_details));
                startThreadedDisconnect();
                SystemClock.sleep(5000);
                startThreadedConnectLoop(true);
            }
        });
        upgrading.start();
    }

    private RelayMessageHandler lastMessageHandler = new RelayMessageHandler() {
        @Override
        public void handleMessage(RelayObject obj, String id) {
            lastMessageReceivedAt = SystemClock.elapsedRealtime();
        }
    };
}