uk.ac.horizon.ubihelper.service.PeerManager.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.horizon.ubihelper.service.PeerManager.java

Source

/**
 * Copyright (c) 2012 The University of Nottingham
 * 
 * This file is part of ubihelper
 *
 *  ubihelper is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  ubihelper 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 Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with ubihelper. If not, see <http://www.gnu.org/licenses/>.
 *  
 *  @author Chris Greenhalgh (cmg@cs.nott.ac.uk), The University of Nottingham
 */
package uk.ac.horizon.ubihelper.service;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.http.impl.cookie.BrowserCompatSpecFactory;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import uk.ac.horizon.ubihelper.R;
import uk.ac.horizon.ubihelper.R.drawable;
import uk.ac.horizon.ubihelper.channel.NamedChannel;
import uk.ac.horizon.ubihelper.dns.DnsClient;
import uk.ac.horizon.ubihelper.dns.DnsProtocol;
import uk.ac.horizon.ubihelper.dns.DnsUtils;
import uk.ac.horizon.ubihelper.dns.DnsProtocol.RR;
import uk.ac.horizon.ubihelper.net.Message;
import uk.ac.horizon.ubihelper.net.OnPeerConnectionListener;
import uk.ac.horizon.ubihelper.net.PeerConnection;
import uk.ac.horizon.ubihelper.net.PeerConnectionScheduler;
import uk.ac.horizon.ubihelper.protocol.ClientInfo;
import uk.ac.horizon.ubihelper.protocol.ClientState;
import uk.ac.horizon.ubihelper.protocol.MessageUtils;
import uk.ac.horizon.ubihelper.protocol.PeerInfo;
import uk.ac.horizon.ubihelper.protocol.ProtocolManager;
import uk.ac.horizon.ubihelper.protocol.ProtocolManager.ClientConnectionListener;
import uk.ac.horizon.ubihelper.service.PeerManager.SearchInfo;
import uk.ac.horizon.ubihelper.ui.PeerRequestInfoActivity;
import uk.ac.horizon.ubihelper.ui.PeerRequestActivity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.telephony.TelephonyManager;
import android.util.Base64;
import android.util.Log;

/** Manages all interactions with Peers
 * 
 * @author cmg
 *
 */
public class PeerManager {
    public static final String TAG = "ubihelper-peermgr";
    private Service service;
    private boolean closed = false;
    /** for initial discovery search */
    private DnsClient searchClient = null;
    private WifiManager wifi = null;
    private DnsClient.OnChange onSearchChangeListener = new OnSearchChangeListener();
    private boolean searchStarted = false;
    private ServerSocketChannel serverSocketChannel;
    private int serverPort;
    private PeerConnectionScheduler selector;
    private MessageDigest messageDigest;
    private MyProtocolManager protocol;
    private SQLiteDatabase database;
    /** partial, id -> PeerInfo */
    private HashMap<String, PeerInfo> peerInfoCache = new HashMap<String, PeerInfo>();
    private java.util.Timer remoteChannelTimer;

    public static class SearchInfo {
        public String name;
        public InetAddress src;

        public String toString() {
            return name;
        }
    }

    public static final String ACTION_SEARCH_STARTED = "uk.ac.horizon.ubihelper.action.SEARCH_STARTED";
    public static final String ACTION_SEARCH_STOPPED = "uk.ac.horizon.ubihelper.action.SEARCH_STOPPED";
    /** Broadcast, has extra NAME & SOURCEIP */
    public static final String ACTION_PEER_DISCOVERED = "uk.ac.horizon.ubihelper.action.PEER_DISCOVERED";
    /** Broadcast, has extra NAME, SOURCEIP, PEER_STATE & (optionally) PORT */
    public static final String ACTION_PEER_REQUEST_STATE_CHANGED = "uk.ac.horizon.ubihelper.action.PEER_REQUEST_STATE_CHANGED";
    public static final String EXTRA_NAME = "uk.ac.horizon.ubihelper.extra.NAME";
    public static final String EXTRA_SOURCEIP = "uk.ac.horizon.ubihelper.extra.SOURCEIP";
    public static final String EXTRA_PORT = "uk.ac.horizon.ubihelper.extra.PORT";
    public static final String EXTRA_PEER_STATE = "uk.ac.horizon.ubihelper.extra.PEER_STATE";
    public static final String EXTRA_DETAIL = "uk.ac.horizon.ubihelper.extra.DETAIL";
    public static final String EXTRA_ID = "uk.ac.horizon.ubihelper.extra.ID";
    public static final String ACTION_PEER_REQUESTS_CHANGED = "uk.ac.horizon.ubihelper.action.PEER_REQUESTS_CHANGED";

    public static final String ACTION_PEERS_CHANGED = "uk.ac.horizon.ubihelper.action.PEERS_CHANGED";
    public static final String ACTION_PEER_STATE_CHANGED = "uk.ac.horizon.ubihelper.action.PEER_STATE_CHANGED";

    private static final long MAX_QUERY_AGE = 15000;

    /** info keys */
    private static final String KEY_IMEI = "imei";
    private static final String KEY_WIFIMAC = "wifimac";
    private static final String KEY_BTMAC = "btmac";
    public static final String KEY_NAME = "name";
    public static final String KEY_PERIOD = "period";
    public static final String KEY_TIMEOUT = "timeout";

    public PeerManager(Service service) {
        this.service = service;
        // Note: meant to open database on another thread?!
        database = new PeersOpenHelper(service).getWritableDatabase();

        protocol = new MyProtocolManager();
        peerConnectionListener = new OnPeerConnectionListener(protocol);
        remoteChannelTimer = new Timer();

        wifi = (WifiManager) service.getSystemService(Service.WIFI_SERVICE);

        try {
            messageDigest = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            Log.e(TAG, "Could not get MessageDigest: " + e);
        }
        try {
            serverSocketChannel = ServerSocketChannel.open();
            ServerSocket ss = serverSocketChannel.socket();
            ss.bind(new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0));
            serverPort = ss.getLocalPort();
            serverSocketChannel.configureBlocking(false);
        } catch (IOException e) {
            Log.w(TAG, "Error opening ServerSocketChannel: " + e.getMessage());
        }
        try {
            selector = new PeerConnectionScheduler(serverSocketChannel);
            selector.setListener(selectorListener);
            selector.start();
        } catch (IOException e) {
            Log.w(TAG, "Error starting Selector: " + e.getMessage());
        }
    }

    public synchronized int getServerPort() {
        return serverPort;
    }

    public synchronized void close() {
        closed = true;
        closeInternal();
    }

    public SQLiteDatabase getDatabase() {
        return database;
    }

    private synchronized void closeInternal() {
        closeSearchInternal();
        if (serverSocketChannel != null) {
            try {
                serverSocketChannel.close();
            } catch (IOException e) {
            }
            serverSocketChannel = null;
        }
        if (selector != null) {
            selector.close();
            selector = null;
        }
        if (database != null) {
            database.close();
            database = null;
        }
        if (remoteChannelTimer != null) {
            remoteChannelTimer.cancel();
            remoteChannelTimer = null;
        }
    }

    private synchronized void closeSearchInternal() {
        if (searchClient != null) {
            searchClient.close();
            searchClient = null;
        }
        if (searchStarted) {
            Intent i = new Intent(ACTION_SEARCH_STOPPED);
            service.sendBroadcast(i);
            searchStarted = false;
        }
    }

    public synchronized boolean isSearchActive() {
        return (searchClient != null && !searchClient.getDone());
    }

    public synchronized LinkedList<SearchInfo> getSearchAnswers() {
        LinkedList<SearchInfo> sis = new LinkedList<SearchInfo>();
        if (searchClient == null)
            return sis;
        LinkedList<DnsProtocol.RR> as = searchClient.getAnswers();
        for (DnsProtocol.RR a : as) {
            SearchInfo si = getSearchInfo(a);
            if (si != null)
                sis.add(si);
        }
        return sis;
    }

    /** return OK if started; false if couldn't, e.g. no wifi active */
    public synchronized boolean startSearch() {
        closeSearchInternal();
        DnsProtocol.Query query = new DnsProtocol.Query();
        query.name = DnsUtils.getServiceDiscoveryName();
        query.rclass = DnsProtocol.CLASS_IN;
        query.type = DnsProtocol.TYPE_PTR;
        searchClient = new DnsClient(query, true);
        searchClient.setOnChange(onSearchChangeListener);
        NetworkInterface ni = getNetworkInterface();
        if (ni == null) {
            Log.w(TAG, "Could not start Dns search - no NetworkInterface");
            return false;
        }
        searchClient.setNetworkInterface(ni);
        searchClient.start();
        searchStarted = true;
        Intent i = new Intent(ACTION_SEARCH_STARTED);
        service.sendBroadcast(i);
        return true;
    }

    private NetworkInterface getNetworkInterface() {
        if (!wifi.isWifiEnabled()) {
            Log.d(TAG, "wifi not enabled");
            return null;
        }
        switch (wifi.getWifiState()) {
        case WifiManager.WIFI_STATE_ENABLED:
            // OK
            break;
        case WifiManager.WIFI_STATE_ENABLING:
            Log.d(TAG, "Wifi enabling");
            return null;
        case WifiManager.WIFI_STATE_DISABLING:
        case WifiManager.WIFI_STATE_DISABLED:
            Log.d(TAG, "wifi disabled/disabling");
            return null;
        default:
            Log.d(TAG, "Wifi state unknown");
            return null;
        }
        // has address?
        WifiInfo info = wifi.getConnectionInfo();
        int ip = info.getIpAddress();
        if (ip == 0) {
            Log.d(TAG, "waiting for an IP address");
            return null;
        }

        NetworkInterface ni = DnsUtils.getNetworkInterface(ip);
        return ni;
    }

    private class OnSearchChangeListener implements DnsClient.OnChange {
        public void onAnswer(final RR rr) {
            SearchInfo si = getSearchInfo(rr);
            if (si != null) {
                Intent i = new Intent(ACTION_PEER_DISCOVERED);
                i.putExtra(EXTRA_NAME, si.name);
                i.putExtra(EXTRA_SOURCEIP, si.src.getHostAddress());
                service.sendBroadcast(i);
            }
        }

        public void onComplete(String error) {
            closeSearchInternal();
        }
    }

    private static SearchInfo getSearchInfo(DnsProtocol.RR rr) {
        if (rr.type == DnsProtocol.TYPE_PTR) {
            try {
                SearchInfo pi = new SearchInfo();
                String ns[] = DnsProtocol.ptrFromData(rr.rdata);
                if (ns != null && ns.length > 0)
                    pi.name = ns[0];
                pi.src = rr.src;
                return pi;
            } catch (IOException e) {
                Log.w(TAG, "Error decoding PTR record: " + e.getMessage());
            }
        }
        return null;
    }

    /** current queries - by name */
    private HashMap<String, ArrayList<DnsClient>> dnsClients = new HashMap<String, ArrayList<DnsClient>>();

    private DnsClient getDnsClient(String name, int type, InetAddress dest) {
        ArrayList<DnsClient> dcs = dnsClients.get(name);
        for (int i = 0; dcs != null && i < dcs.size(); i++) {
            DnsClient dc = dcs.get(i);
            if (dc.getAge() > MAX_QUERY_AGE) {
                dcs.remove(i);
                i--;
                continue;
            }
            if (type == dc.getQuery().type && (dest == null || dest.equals(dc.getDestination())))
                return dc;
        }
        return null;
    }

    /** peer state */
    public static enum PeerRequestState {
        STATE_SEARCHED_ADD, STATE_SRV_DISCOVERY, STATE_SRV_DISCOVERY_FAILED, STATE_SRV_FOUND, STATE_CONNECTING, STATE_CONNECTED, STATE_CONNECTING_FAILED, STATE_NEGOTIATE_PROTOCOL, STATE_PEER_REQ, STATE_PEER_DONE, STATE_PEERED, STATE_PEERED_UNTRUSTED
    }

    /** peer info */
    public static class PeerRequestInfo implements Cloneable {
        public int _id = -1;
        public PeerRequestState state;
        // Search
        public String instanceName;
        public InetAddress src;
        // SRV
        public int port = 0;
        // connect
        PeerConnection pc;
        // peer negotiation
        String pin;
        String pinnonce;
        String pindigest;
        String id;
        String secret1, secret2;
        // done
        public JSONObject peerInfo;

        // debug
        public String detail;
        public boolean manual;

        PeerRequestInfo(String instanceName, InetAddress src) {
            this.instanceName = instanceName;
            this.src = src;
            state = PeerRequestState.STATE_SEARCHED_ADD;
        }

        PeerRequestInfo(PeerRequestInfo pi) {
            id = pi.id;
            state = pi.state;
            instanceName = pi.instanceName;
            src = pi.src;
            port = pi.port;
            detail = pi.detail;
        }

        public PeerRequestInfo(ClientInfo ci) {
            id = ci.id;
            instanceName = ci.name;
            pc = ci.pc;
            port = ci.port;
        }
    }

    private void broadcastPeerState(PeerRequestInfo pi) {
        Intent i = new Intent(ACTION_PEER_REQUEST_STATE_CHANGED);
        i.putExtra(EXTRA_PEER_STATE, pi.state.ordinal());
        i.putExtra(EXTRA_NAME, pi.instanceName);
        if (pi.detail != null)
            i.putExtra(EXTRA_DETAIL, pi.detail);
        i.putExtra(EXTRA_SOURCEIP, pi.src.getHostAddress());
        if (pi.port != 0)
            i.putExtra(EXTRA_PORT, pi.port);
        // for pass over to PeerInfo
        if (pi.id != null)
            i.putExtra(EXTRA_ID, pi.id);
        //Log.d(TAG,"sendBroadcast (peer state)...");
        service.sendBroadcast(i);
        //Log.d(TAG,"sendBroadcast done");
    }

    private ArrayList<PeerRequestInfo> peerRequests = new ArrayList<PeerRequestInfo>();

    /** public API - list peers */
    public synchronized List<PeerRequestInfo> getPeerRequests() {
        ArrayList<PeerRequestInfo> rpeers = new ArrayList<PeerRequestInfo>();
        for (PeerRequestInfo pi : peerRequests) {
            rpeers.add(new PeerRequestInfo(pi));
        }
        return rpeers;
    }

    /** public API - get info on peer */
    public synchronized PeerRequestInfo getPeer(Intent i) {
        String sourceip = i.getExtras().getString(EXTRA_SOURCEIP);
        String name = i.getExtras().getString(EXTRA_NAME);
        for (PeerRequestInfo pi : peerRequests) {
            if (pi.instanceName.equals(name) && pi.src.getHostAddress().equals(sourceip))
                return pi;
        }
        return null;
    }

    /** public API - matches */
    public static synchronized boolean matches(PeerRequestInfo pi, Intent i) {
        String sourceip = i.getExtras().getString(EXTRA_SOURCEIP);
        String name = i.getExtras().getString(EXTRA_NAME);
        if (pi.instanceName.equals(name) && pi.src.getHostAddress().equals(sourceip))
            return true;
        return false;
    }

    /** public API - matches */
    public static synchronized boolean matches(PeerRequestInfo pi, SearchInfo si) {
        if (pi.instanceName.equals(si.name) && pi.src.equals(si.src))
            return true;
        return false;
    }

    /** public API - start adding a discovered peer */
    public synchronized void addPeer(SearchInfo peerInfo) {
        // already in progress?
        for (PeerRequestInfo pi : peerRequests) {
            if (matches(pi, peerInfo)) {
                // kick?
                return;
            }
        }
        PeerRequestInfo pi = new PeerRequestInfo(peerInfo.name, peerInfo.src);
        peerRequests.add(pi);
        Intent i = new Intent(ACTION_PEER_REQUESTS_CHANGED);
        service.sendBroadcast(i);
        updatePeer(pi);
    }

    private synchronized void updatePeer(PeerRequestInfo pi) {
        switch (pi.state) {
        case STATE_SEARCHED_ADD:
            startSrvDiscovery(pi);
            break;
        case STATE_SRV_DISCOVERY_FAILED:
        case STATE_CONNECTING_FAILED:
            broadcastPeerState(pi);
            removePeer(pi);
            break;
        case STATE_SRV_FOUND:
            connectPeer(pi);
            break;
        case STATE_CONNECTED:
            negotiateProtocol(pi);
            break;
        }
    }

    private OnPeerConnectionListener peerConnectionListener;

    private class OnPeerConnectionListener extends ClientConnectionListener {
        public OnPeerConnectionListener(ProtocolManager pm) {
            super(pm);
        }

        public void onRecvMessage(PeerConnection pc) {
            if (pc.attachment() instanceof PeerRequestInfo) {
                PeerRequestInfo pi = (PeerRequestInfo) pc.attachment();
                Log.d(TAG, "onMessage PeerInfo " + pi);
                checkMessages(pi);
            } else
                super.onRecvMessage(pc);
        }

        public void onFail(PeerConnection pc, boolean sendFailed, boolean recvFailed, boolean connectFailed) {
            if (pc.attachment() instanceof PeerRequestInfo) {
                PeerRequestInfo pi = (PeerRequestInfo) pc.attachment();
                Log.d(TAG, "onFail PeerInfo " + pi);
                pi.state = PeerRequestState.STATE_CONNECTING_FAILED;
                pi.detail = null;
                updatePeer(pi);
            } else
                super.onFail(pc, sendFailed, recvFailed, connectFailed);
        }

        public void onConnected(PeerConnection pc) {
            if (pc.attachment() instanceof PeerRequestInfo) {
                PeerRequestInfo pi = (PeerRequestInfo) pc.attachment();
                if (pi.state == PeerRequestState.STATE_CONNECTING) {
                    Log.d(TAG, "onConnected -> connected PeerInfo " + pi);
                    pi.state = PeerRequestState.STATE_CONNECTED;
                    pi.detail = null;
                    broadcastPeerState(pi);
                    updatePeer(pi);
                } else
                    Log.d(TAG, "onConnected " + pi.state + " PeerInfo " + pi);
            } else
                super.onConnected(pc);
        }
    };

    private synchronized void connectPeer(PeerRequestInfo pi) {
        if (pi.src == null) {
            pi.state = PeerRequestState.STATE_CONNECTING_FAILED;
            pi.detail = "IP unknown";
            updatePeer(pi);
            return;
        }
        pi.state = PeerRequestState.STATE_CONNECTING;
        pi.detail = "connectPeer()";
        try {
            Log.d(TAG, "Connect to " + pi.src.getHostAddress() + ":" + pi.port);
            pi.pc = selector.connect(new InetSocketAddress(pi.src, pi.port), peerConnectionListener, pi);
            //Log.d(TAG,"Connect done="+done);
            //pi.detail = "4 (done="+done+")";
            boolean done = pi.pc.isConnected();
            if (done) {
                //Toast.makeText(service, "Connected!", Toast.LENGTH_SHORT).show();
                pi.state = PeerRequestState.STATE_CONNECTED;
                pi.detail = "Connected immediately";
            } else {
                //Toast.makeText(service, "waiting...", Toast.LENGTH_SHORT).show();
                pi.detail = "Waiting for connect";
                //Log.d(TAG,"Register (connect)...");
                //Log.d(TAG,"registered");
            }
            //Log.d(TAG,"Broadcast...");
            broadcastPeerState(pi);
            //Log.d(TAG,"Broadcast done");
            if (done) {
                updatePeer(pi);
            }
            return;
        } catch (Exception e) {
            // Not just IOExceptions?!
            Log.w(TAG, "Problem connecting to peer " + pi.src.getHostAddress() + ":" + pi.port + ": "
                    + e.getMessage());
            //Toast.makeText(service, "Error connecting: "+e, Toast.LENGTH_LONG).show();
            pi.state = PeerRequestState.STATE_CONNECTING_FAILED;
            pi.detail = e.getMessage();
            updatePeer(pi);
        }
    }

    private class MyProtocolManager extends ProtocolManager {

        @Override
        public void removeClient(ClientInfo ci) {
            hideClientNotification(ci);
            super.removeClient(ci);
            clients.remove(ci);
        }

        @Override
        protected byte[] base64Decode(String str) {
            return Base64.decode(str, Base64.DEFAULT);
        }

        @Override
        protected String base64Encode(byte[] bs) {
            return Base64.encodeToString(bs, Base64.DEFAULT);
        }

        /** prompt for PIN, e.g. from user. return false (default) if cannot prompt. */
        @Override
        protected boolean clientPromptForPin(ClientInfo ci) {
            // Notification?
            // create taskbar notification
            int icon = R.drawable.peer_request_notification_icon;
            CharSequence tickerText = "Peer request from " + ci.name;
            long when = System.currentTimeMillis();

            Notification notification = new Notification(icon, tickerText, when);

            Context context = service;
            CharSequence contentTitle = "Peer request from " + ci.name;
            CharSequence contentText = "Accept or reject peer request from " + ci.name;
            Intent notificationIntent = new Intent(service, PeerRequestActivity.class);
            notificationIntent.putExtra(EXTRA_ID, ci.id);
            notificationIntent.putExtra(EXTRA_NAME, ci.name);
            PendingIntent contentIntent = PendingIntent.getActivity(service, 0, notificationIntent, 0);

            notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
            peerRequestNotificationId++;
            ci.notificationId = peerRequestNotificationId;
            NotificationManager mNotificationManager = (NotificationManager) service
                    .getSystemService(Service.NOTIFICATION_SERVICE);
            //service.(ci.notificationId, notification);   
            mNotificationManager.notify(ci.notificationId, notification);

            return true;
        }

        /** called when successfully peered */
        @Override
        protected boolean clientHandlePeered(ClientInfo ci, PeerInfo pi) {
            clients.remove(ci);
            addPeer(pi);
            //         peerCache.put(pi.id, pi);
            //         pi.pc.attach(pi);
            //
            //         Intent i = new Intent(ACTION_PEER_REQUESTS_CHANGED);
            //         service.sendBroadcast(i);

            // don't handle more messages as a client
            return false;
        }

        @Override
        protected void peerHandleFailed(PeerInfo pi) {
            // TODO Auto-generated method stub
            Log.d(TAG, "Peer failed: " + pi.id);
            // TODO fast response?
        }

        @Override
        protected JSONObject getInfo() {
            return PeerManager.this.getInfo();
        }

        @Override
        protected String getName() {
            return service.getDeviceName();
        }

        @Override
        protected int getPort() {
            return serverPort;
        }

        @Override
        protected String getId() {
            return getDeviceId();
        }

    }

    protected synchronized void failPeer(PeerRequestInfo pi, String detail) {
        pi.state = PeerRequestState.STATE_CONNECTING_FAILED;
        pi.detail = detail;
        broadcastPeerState(pi);
        removePeer(pi);
    }

    private synchronized void addPeer(PeerInfo pi) {
        // in database?
        if (pi.id == null) {
            Log.e(TAG, "addPeer with null id");
            return;
        }
        database.beginTransaction();
        try {
            PeerInfo pi2 = PeersOpenHelper.getPeerInfo(database, pi.id);
            if (pi2 != null) {
                // update!
                pi._id = pi2._id;
                pi.enabled = pi2.enabled;
                Log.d(TAG, "Updating peer " + pi.id + " on addPeer to " + pi);
                PeersOpenHelper.updatePeerInfo(database, pi);
            } else {
                // add
                PeersOpenHelper.addPeerInfo(database, pi);
                Log.d(TAG, "Added Peer " + pi.id + " as " + pi._id);
            }
            database.setTransactionSuccessful();
            // add to peers
            peerInfoCache.put(pi.id, pi);
            // broadcast etc.!
            Intent i = new Intent(ACTION_PEERS_CHANGED);
            service.sendBroadcast(i);
        } finally {
            database.endTransaction();
        }
    }

    protected synchronized void checkMessages(PeerRequestInfo pi) {
        Message m = null;
        while ((m = pi.pc.getMessage()) != null) {
            switch (pi.state) {
            case STATE_NEGOTIATE_PROTOCOL: {
                boolean ok = MessageUtils.checkNegotiateProtocolResponse(m);
                if (!ok) {
                    failPeer(pi, "Incompatible protocol: " + m.body);
                    return;
                }
                pi.state = PeerRequestState.STATE_PEER_REQ;
                pi.detail = null;
                broadcastPeerState(pi);
                sendPeerRequest(pi);
                break;
            }
            case STATE_PEER_REQ: {
                // should get pin back (but in future could get something else, e.g.already known)
                boolean ok = handlePeerReqResponse(pi, m);
                if (!ok)
                    return;
                break;
            }
            case STATE_PEER_DONE: {
                boolean ok = handlePeerDoneResponse(pi, m);
                if (!ok)
                    return;
                break;
            }
            default:
                Log.w(TAG, "Don't know what to do with message in state " + pi.state);
                return;
            }
        }

    }

    private synchronized void startSrvDiscovery(final PeerRequestInfo pi) {
        // 1: do SRV discovery to get IP/Port
        String name = DnsUtils.getServiceDiscoveryName();
        DnsClient dc = getDnsClient(name, DnsProtocol.TYPE_SRV, pi.src);
        if (dc == null) {
            // start again
            DnsProtocol.Query query = new DnsProtocol.Query();
            query.name = DnsUtils.getServiceDiscoveryName();
            query.type = DnsProtocol.TYPE_SRV;
            query.rclass = DnsProtocol.CLASS_IN;
            final DnsClient fdc = new DnsClient(query, false);
            NetworkInterface ni = getNetworkInterface();
            if (ni != null)
                fdc.setNetworkInterface(ni);
            fdc.setDestination(pi.src);
            fdc.setOnChange(new DnsClient.OnChange() {
                public void onAnswer(RR rr) {
                }

                public void onComplete(String error) {
                    service.postTask(new Runnable() {
                        public void run() {
                            srvDiscoveryComplete(fdc, pi.src);
                        }
                    });
                }
            });
            ArrayList<DnsClient> dcs = dnsClients.get(name);
            if (dcs == null) {
                dcs = new ArrayList<DnsClient>();
                dnsClients.put(name, dcs);
            }
            dcs.add(fdc);
            pi.state = PeerRequestState.STATE_SRV_DISCOVERY;
            broadcastPeerState(pi);
            fdc.start();
            return;
        } else {
            pi.state = PeerRequestState.STATE_SRV_DISCOVERY;
            // handle result? (not too old, but may have finished, or not)
            boolean done = dc.getDone();
            if (done) {
                srvDiscoveryComplete(dc, pi.src);
            } else
                broadcastPeerState(pi);
            // otherwise will be done...
            return;
        }
    }

    // 2: initiate connection
    // 3: generate and send peer request
    // 4: wait for and handle response       
    private synchronized void srvDiscoveryComplete(DnsClient dc, InetAddress src) {
        // copy peers in case of delete
        ArrayList<PeerRequestInfo> peers2 = new ArrayList<PeerRequestInfo>();
        peers2.addAll(peerRequests);
        for (int i = 0; i < peers2.size(); i++) {
            PeerRequestInfo pi = peers2.get(i);
            if (pi.state == PeerRequestState.STATE_SRV_DISCOVERY && pi.src.equals(src)) {
                LinkedList<DnsProtocol.RR> as = dc.getAnswers();
                if (as.size() == 0) {
                    pi.state = PeerRequestState.STATE_SRV_DISCOVERY_FAILED;
                    updatePeer(pi);
                } else {
                    try {
                        DnsProtocol.SrvData srv = DnsProtocol.srvFromData(as.get(0).rdata);
                        pi.state = PeerRequestState.STATE_SRV_FOUND;
                        pi.port = srv.port;
                        if (!srv.target.equals(src.getHostAddress())) {
                            Log.w(TAG, "SRV returned different IP: " + srv.target + " vs " + src.getHostAddress());
                        }
                        updatePeer(pi);
                    } catch (IOException e) {
                        Log.w(TAG, "Error parsing SRV data: " + e.getMessage());
                        pi.state = PeerRequestState.STATE_SRV_DISCOVERY_FAILED;
                        updatePeer(pi);
                    }
                }
            }
        }
    }

    private synchronized void removePeer(PeerRequestInfo pi) {
        if (peerRequests.remove(pi)) {
            Intent i = new Intent(ACTION_PEER_REQUESTS_CHANGED);
            service.sendBroadcast(i);
        }
        if (pi.pc != null) {
            pi.pc.close();
            pi.pc = null;
        }
    }

    private synchronized void negotiateProtocol(PeerRequestInfo pi) {
        // Start negotiation on PeerConnection...
        // protocol hello
        String protocol = Message.getHelloBody();
        Message m = new Message(Message.Type.HELLO, null, null, protocol);
        pi.pc.sendMessage(m);
        pi.state = PeerRequestState.STATE_NEGOTIATE_PROTOCOL;
        broadcastPeerState(pi);
        return;
    }

    private synchronized void sendPeerRequest(PeerRequestInfo pi) {

        // create peer request
        JSONObject msg = new JSONObject();
        try {
            msg.put(MessageUtils.KEY_TYPE, MessageUtils.MSG_INIT_PEER_REQ);
            // pass key
            byte pbuf[] = new byte[4];
            protocol.getRandom(pbuf);
            StringBuilder pb = new StringBuilder();
            for (int i = 0; i < pbuf.length; i++)
                pb.append((char) ('0' + ((pbuf[i] & 0xff) % 10)));
            pi.pin = pb.toString();
            // pin nonce and digest
            byte nbuf[] = new byte[8];
            protocol.getRandom(nbuf);
            pi.pinnonce = Base64.encodeToString(nbuf, Base64.DEFAULT);
            messageDigest.reset();
            messageDigest.update(nbuf);
            messageDigest.update(pi.pin.getBytes("UTF-8"));
            byte dbuf[] = messageDigest.digest();
            pi.pindigest = Base64.encodeToString(dbuf, Base64.DEFAULT);
            msg.put(MessageUtils.KEY_PINDIGEST, pi.pindigest);

            msg.put(MessageUtils.KEY_ID, getDeviceId());
            msg.put(MessageUtils.KEY_NAME, service.getDeviceName());
            msg.put(MessageUtils.KEY_PORT, this.serverPort);
            Message m = new Message(Message.Type.MANAGEMENT, null, null, msg.toString());
            pi.pc.sendMessage(m);

            pi.state = PeerRequestState.STATE_PEER_REQ;
            pi.detail = "Pin is " + pi.pin;
            broadcastPeerState(pi);
        } catch (JSONException e) {
            // shouldn't happen!
            Log.e(TAG, "JSON error (shoulnd't be): " + e);
        } catch (UnsupportedEncodingException e) {
            // shouldn't happen!
            Log.e(TAG, "Unsupported encoding (shoulnd't be): " + e);
        }
    }

    /** called from checkMessage(pi) to handle message in state STATE_PEER_REQ */
    private boolean handlePeerReqResponse(PeerRequestInfo pi, Message m) {
        if (m.type != Message.Type.MANAGEMENT) {
            failPeer(pi, "Received init_peer_req response of type " + m.type);
            return false;
        }
        String pinGuess = null;
        try {
            JSONObject msg = new JSONObject(m.body);
            String type = msg.getString(MessageUtils.KEY_TYPE);
            if (!MessageUtils.MSG_RESP_PEER_PIN.equals(type)) {
                if (MessageUtils.MSG_RESP_PEER_NOPIN.equals(type)) {
                    return handlePeerNopin(pi, msg);
                }
                failPeer(pi, "Received init_peer_req response " + type);
                // TODO resp_peer_known
                return false;
            }
            // id, name, port, pin
            pinGuess = msg.getString(MessageUtils.KEY_PIN);
            pi.id = msg.getString(MessageUtils.KEY_ID);
            // TODO known IDs?
            int port = msg.getInt(MessageUtils.KEY_PORT);
            if (port != pi.port)
                Log.w(TAG, "resp_peer_pin has different port: " + port + " vs " + pi.port);
            String name = msg.getString(MessageUtils.KEY_NAME);
            if (pi.instanceName == null)
                pi.instanceName = name;
            else if (!name.equals(pi.instanceName))
                Log.w(TAG, "resp_peer_pin has different name: " + name + " vs " + pi.instanceName);
        } catch (JSONException e) {
            failPeer(pi, "Error in resp_peer_pin message: " + e);
            return false;
        }
        Log.i(TAG, "Received resp_peer_pin in state peer_req with pin=" + pinGuess);
        // pin?
        if (!pi.pin.equals(pinGuess)) {
            failPeer(pi, "Incorrect pin: " + pinGuess);
            return false;
        }
        // done
        try {
            JSONObject resp = new JSONObject();
            resp.put(MessageUtils.KEY_TYPE, MessageUtils.MSG_INIT_PEER_DONE);
            byte sbuf[] = new byte[8];
            protocol.getRandom(sbuf);
            pi.secret1 = Base64.encodeToString(sbuf, Base64.DEFAULT);
            resp.put(MessageUtils.KEY_SECRET, pi.secret1);
            resp.put(MessageUtils.KEY_PINNONCE, pi.pinnonce);
            JSONObject info = getInfo();
            if (info != null)
                resp.put(MessageUtils.KEY_INFO, info);

            Message r = new Message(Message.Type.MANAGEMENT, null, null, resp.toString());
            pi.pc.sendMessage(r);

            pi.state = PeerRequestState.STATE_PEER_DONE;
            pi.detail = null;

            broadcastPeerState(pi);

            return true;

        } catch (JSONException e) {
            // shouldn't happen!
            Log.e(TAG, "JSON error (shoulnd't be): " + e);
        }
        return false;
    }

    /** nopin response from peer in response to pin request */
    private synchronized boolean handlePeerNopin(PeerRequestInfo pi, JSONObject msg) {
        try {
            pi.id = msg.getString(MessageUtils.KEY_ID);
            // TODO known IDs?
            int port = msg.getInt(MessageUtils.KEY_PORT);
            if (port != pi.port)
                Log.w(TAG, "resp_peer_nopin has different port: " + port + " vs " + pi.port);
            String name = msg.getString(MessageUtils.KEY_NAME);
            if (pi.instanceName == null)
                pi.instanceName = name;
            else if (!name.equals(pi.instanceName)) {
                Log.w(TAG, "resp_peer_nopin has different name: " + name + " vs " + pi.instanceName);
                pi.instanceName = name;
            }
            pi.secret2 = msg.getString(MessageUtils.KEY_SECRET);
            pi.peerInfo = msg.getJSONObject(MessageUtils.KEY_INFO);
        } catch (JSONException e) {
            failPeer(pi, "Error in resp_peer_pin message: " + e);
            return false;
        }
        Log.i(TAG, "Received resp_peer_nopin in state peer_req");

        pi.state = PeerRequestState.STATE_PEERED_UNTRUSTED;
        pi.detail = "no pin";

        peerRequests.remove(pi);
        Intent bi = new Intent(ACTION_PEER_REQUESTS_CHANGED);
        service.sendBroadcast(bi);
        broadcastPeerState(pi);

        addPeer(pi);

        return true;
    }

    private void addPeer(PeerRequestInfo preq) {
        // convert to PeerInfo
        PeerInfo pi = new PeerInfo();
        pi.createdTimestamp = System.currentTimeMillis();
        pi.info = preq.peerInfo;
        if (pi.info != null) {
            try {
                if (pi.info.has(KEY_BTMAC))
                    pi.btmac = pi.info.getString(KEY_BTMAC);
                if (pi.info.has(KEY_WIFIMAC))
                    pi.wifimac = pi.info.getString(KEY_WIFIMAC);
                if (pi.info.has(KEY_IMEI))
                    pi.imei = pi.info.getString(KEY_IMEI);
            } catch (JSONException e) {
                Log.e(TAG, "Unexpected JSON error unpacking peerInfo: " + e);
            }
        }
        pi.secret = protocol.combineSecrets(preq.secret1, preq.secret2);
        pi.ip = preq.src.getHostAddress();
        pi.ipTimestamp = System.currentTimeMillis();
        pi.port = preq.port;
        pi.portTimestamp = pi.ipTimestamp;
        pi.name = preq.instanceName;
        pi.id = preq.id;
        pi.trusted = preq.state == PeerRequestState.STATE_PEERED;
        pi.nickname = pi.name;
        pi.manual = preq.manual;

        pi.pc = preq.pc;
        if (pi.pc != null)
            pi.pc.attach(pi);

        Log.i(TAG, "Converted PeerRequestInfo to PeerInfo");

        addPeer(pi);
    }

    /** get info to pass to peer */
    private synchronized JSONObject getInfo() {
        try {
            JSONObject info = new JSONObject();
            // has address?
            WifiInfo wifiinfo = wifi.getConnectionInfo();
            String wifimac = wifiinfo.getMacAddress();
            if (wifimac != null)
                info.put(KEY_WIFIMAC, wifimac);
            // note: cannot get BluetoothAdapter from comms thread if not a looper
            String btmac = service.getBtMac();
            if (btmac != null)
                info.put(KEY_BTMAC, btmac);
            String imei = service.getImei();
            if (imei != null)
                info.put(KEY_IMEI, imei);
            return info;
        } catch (JSONException e) {
            // shouldn't happen!
            Log.e(TAG, "JSON error (shoulnd't be): " + e);
        }
        return null;
    }

    private static int peerRequestNotificationId = 2;

    /** called on receipt of message after init_peer_done in peer */
    private synchronized boolean handlePeerDoneResponse(PeerRequestInfo pi, Message m) {
        if (m.type != Message.Type.MANAGEMENT) {
            failPeer(pi, "Received init_peer_done response of type " + m.type);
            return false;
        }
        try {
            JSONObject msg = new JSONObject(m.body);
            String type = msg.getString(MessageUtils.KEY_TYPE);
            if (!MessageUtils.MSG_RESP_PEER_DONE.equals(type)) {
                failPeer(pi, "Received init_peer_done response " + type);
                return false;
            }
            pi.secret2 = msg.getString(MessageUtils.KEY_SECRET);
            pi.peerInfo = msg.getJSONObject(MessageUtils.KEY_INFO);
        } catch (JSONException e) {
            failPeer(pi, "Error in resp_peer_done message: " + e);
            return false;
        }
        // all done
        pi.state = PeerRequestState.STATE_PEERED;
        pi.detail = null;

        peerRequests.remove(pi);
        Intent bi = new Intent(ACTION_PEER_REQUESTS_CHANGED);
        service.sendBroadcast(bi);
        broadcastPeerState(pi);

        addPeer(pi);

        return true;
    }

    private String getDeviceId() {
        return service.getDeviceId();
    }

    /** clients */
    private ArrayList<ClientInfo> clients = new ArrayList<ClientInfo>();

    private PeerConnectionScheduler.Listener selectorListener = new PeerConnectionScheduler.Listener() {

        public void onAccept(PeerConnectionScheduler pcs, PeerConnection newPeerConnection) {
            Log.d(TAG, "onAccept new peer");
            // TODO Auto-generated method stub
            ClientInfo ci = new ClientInfo(newPeerConnection);
            newPeerConnection.setOnPeerConnectionListener(peerConnectionListener);
            newPeerConnection.attach(ci);
            clients.add(ci);
            Log.d(TAG,
                    "Accepted new connection from "
                            + newPeerConnection.getSocketChannel().socket().getInetAddress().getHostAddress() + ":"
                            + newPeerConnection.getSocketChannel().socket().getPort());
        }
    };

    /** public API - peer request accept */
    public synchronized void acceptPeerRequest(Intent triggerIntent, String pin) {
        // TODO
        ClientInfo ci = getClientInfo(triggerIntent);
        if (ci == null) {
            Log.w(TAG, "Could not find ClientInfo for reject with id "
                    + triggerIntent.getExtras().getString(EXTRA_ID));
            return;
        }
        ci.pin = pin;
        // notification?
        hideClientNotification(ci);
        // next step
        Message m = MessageUtils.getRespPeerPin(getDeviceId(), serverPort, service.getDeviceName(), pin);
        ci.pc.sendMessage(m);
        ci.state = ClientState.STATE_PEER_PIN;
    }

    /** public API - peer request reject */
    public synchronized void rejectPeerRequest(Intent triggerIntent) {
        // TODO
        ClientInfo ci = getClientInfo(triggerIntent);
        if (ci == null) {
            Log.w(TAG, "Could not find ClientInfo for reject with id "
                    + triggerIntent.getExtras().getString(EXTRA_ID));
            return;
        }
        // notification?
        hideClientNotification(ci);
        // reject
        protocol.removeClient(ci);
    }

    private synchronized ClientInfo getClientInfo(Intent triggerIntent) {
        String id = triggerIntent.getExtras().getString(EXTRA_ID);
        for (ClientInfo ci : clients) {
            if (ci.id.equals(id))
                return ci;
        }
        return null;
    }

    private synchronized void hideClientNotification(ClientInfo ci) {
        if (ci.notificationId != 0) {
            NotificationManager nm = (NotificationManager) service.getSystemService(Service.NOTIFICATION_SERVICE);
            nm.cancel(ci.notificationId);
            ci.notificationId = 0;
        }
    }

    /** public API - start peer add for specified host/port. Return PeerReq8estInfoActivity intent */
    public synchronized Intent addPeerRequest(InetAddress host, int port) {
        Intent i = new Intent(service, PeerRequestInfoActivity.class);
        i.putExtra(EXTRA_SOURCEIP, host.getHostAddress());
        i.putExtra(EXTRA_PORT, port);
        String name = "Unknown (manually added)";
        i.putExtra(EXTRA_NAME, name);

        PeerRequestInfo pi = new PeerRequestInfo(name, host);
        pi.port = port;
        pi.state = PeerRequestState.STATE_SRV_FOUND;
        pi.manual = true;

        peerRequests.add(pi);

        Intent bi = new Intent(ACTION_PEER_REQUESTS_CHANGED);
        service.sendBroadcast(bi);
        updatePeer(pi);

        return i;
    }

    public synchronized List<PeerInfo> getPeers() {
        return PeersOpenHelper.getPeerInfos(database);
    }

    public synchronized List<PeerInfo> getEnabledPeers() {
        return PeersOpenHelper.getPeerInfos(database, PeersOpenHelper.KEY_ENABLED + " = 1", null);
    }

    public synchronized PeerInfo getPeer(String id) {
        PeerInfo pi = peerInfoCache.get(id);
        if (pi == null) {
            pi = PeersOpenHelper.getPeerInfo(database, id);
            if (pi != null)
                peerInfoCache.put(id, pi);
        }
        return pi;
    }

    public synchronized void setPeerEnabled(PeerInfo peerInfo, boolean isChecked) {
        peerInfo.enabled = isChecked;
        PeerInfo pi = getPeer(peerInfo.id);
        pi.enabled = peerInfo.enabled;
        PeersOpenHelper.updatePeerInfo(database, pi);
        //NO peerInfoCache.put(pi.id, pi);

        Log.d(TAG, "setPeerEnabled id=" + peerInfo.id + " -> " + isChecked);

        Intent i = new Intent(ACTION_PEER_STATE_CHANGED);
        i.putExtra(EXTRA_ID, pi.id);
        broadcastPeerState(pi);
    }

    private void broadcastPeerState(PeerInfo pi) {
        Intent i = new Intent(ACTION_PEER_STATE_CHANGED);
        i.putExtra(EXTRA_ID, pi.id);
        i.putExtra(EXTRA_NAME, pi.name);
        //Log.d(TAG,"sendBroadcast (peer state)...");
        service.sendBroadcast(i);
        //Log.d(TAG,"sendBroadcast done");
    }

    private class RemoteChannel extends NamedChannel {
        String peerId;
        String peerChannelName;

        public RemoteChannel(String peerId, String peerChannelName) {
            super("/" + peerId + "/" + peerChannelName);
            this.peerId = peerId;
            this.peerChannelName = peerChannelName;
        }

        @Override
        public synchronized void close() {
            // TODO Auto-generated method stub
            super.close();
        }

        @Override
        public synchronized JSONObject getImmediateValue() {
            // TODO Auto-generated method stub
            return super.getImmediateValue();
        }

        @Override
        protected void handleStart() {
            super.handleStart();
            PeerInfo pi = getPeer(peerId);
            if (pi == null) {
                Log.d(TAG, "No peer " + peerId + " in handleStart for " + peerChannelName);
            } else if (pi.pc == null) {
                Log.d(TAG, "No PeerConnection for " + peerId + " in handleStart for " + peerChannelName);
            } else {
                // send initial get
                JSONObject req = getRequest();
                JSONArray reqs = new JSONArray();
                reqs.put(req);
                Log.d(TAG, "Send initial request for " + name);
                pi.pc.sendMessage(new Message(Message.Type.REQUEST, "GET /ubihelper", null, reqs.toString()));
            }
            checkRemoteRequestTask(peerId);
        }

        @Override
        protected void handleStop() {
            super.handleStop();
            // TODO send end of get?         
            checkRemoteRequestTask(peerId);
        }

        JSONObject getRequest() {
            JSONObject req = new JSONObject();
            try {
                req.put(KEY_NAME, name);
                req.put(KEY_PERIOD, this.period);
                req.put(KEY_TIMEOUT, DEFAULT_REMOTE_REQUEST_PERIOD * 2);
            } catch (JSONException e) {
                // shouldn't
            }
            return req;
        }

        boolean isActive() {
            return active;
        }
    }

    private class RemoteRequestTask extends TimerTask {
        private String peerId;

        RemoteRequestTask(String peerId) {
            this.peerId = peerId;
        }

        @Override
        public void run() {
            JSONArray reqs = new JSONArray();
            synchronized (PeerManager.this) {
                for (RemoteChannel rc : remoteChannels) {
                    if (peerId.equals(rc.peerId) && rc.isActive())
                        reqs.put(rc.getRequest());
                }
            }
            PeerInfo pi = getPeer(peerId);
            if (pi == null) {
                Log.d(TAG, "No peer " + peerId + " in RemoteRequestTask");
                return;
            }
            if (pi.pc == null) {
                Log.d(TAG, "No PeerConnection for " + peerId + " in RemoteRequestTask");
                return;
            }
            // send repeat get
            Log.d(TAG, "Send repeat request to " + peerId + ": " + reqs.toString());
            pi.pc.sendMessage(new Message(Message.Type.REQUEST, "GET /ubihelper", null, reqs.toString()));
        }
    };

    private static final long DEFAULT_REMOTE_REQUEST_PERIOD = 10000;
    private HashMap<String, RemoteRequestTask> remoteRequestTasks = new HashMap<String, RemoteRequestTask>();
    private LinkedList<RemoteChannel> remoteChannels = new LinkedList<RemoteChannel>();

    private synchronized void checkRemoteRequestTask(String peerId) {
        boolean active = false;
        for (RemoteChannel rc : remoteChannels) {
            if (rc.peerId.equals(peerId) && rc.isActive()) {
                active = true;
                break;
            }
        }
        RemoteRequestTask remoteRequestTask = remoteRequestTasks.get(peerId);
        if (active && remoteRequestTask == null) {
            remoteRequestTask = new RemoteRequestTask(peerId);
            remoteRequestTasks.put(peerId, remoteRequestTask);
            remoteChannelTimer.scheduleAtFixedRate(remoteRequestTask, DEFAULT_REMOTE_REQUEST_PERIOD,
                    DEFAULT_REMOTE_REQUEST_PERIOD);
        } else if (!active && remoteRequestTask != null) {
            remoteRequestTask.cancel();
            remoteRequestTasks.remove(peerId);
        }
    }

    public synchronized NamedChannel getRemoteChannel(String peerId, String peerChannelName) {
        RemoteChannel rc = new RemoteChannel(peerId, peerChannelName);
        remoteChannels.add(rc);
        return rc;
    }
}