package pro.dbro.ble; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import java.util.HashMap; import pro.dbro.airshare.app.AirShareService; import pro.dbro.airshare.transport.Transport; import pro.dbro.ble.data.ContentProviderStore; import pro.dbro.ble.data.DataStore; import pro.dbro.ble.data.model.DataUtil; import pro.dbro.ble.data.model.Message; import pro.dbro.ble.data.model.Peer; import pro.dbro.ble.protocol.BLEProtocol; import pro.dbro.ble.protocol.MessagePacket; import pro.dbro.ble.protocol.OwnedIdentityPacket; import pro.dbro.ble.protocol.Protocol; import pro.dbro.ble.ui.Notification; import pro.dbro.ble.ui.activities.LogConsumer; import timber.log.Timber; /** * Created by davidbrodsky on 10/13/14. */ public class ChatClient implements AirShareService.Callback, ChatPeerFlow.DataOutlet, ChatPeerFlow.Callback { public interface Callback { /** Client should not invoke remotePeer#close() */ void onAppPeerStatusUpdated(@NonNull Peer remotePeer, @NonNull ConnectionStatus status); } public static final String TAG = "ChatApp"; public static final String AIRSHARE_SERVICE_NAME = "BLEMeshChat"; private Context mContext; private DataStore mDataStore; private Protocol mProtocol; private AirShareService.ServiceBinder mAirShareServiceBinder; private Callback mCallback; private HashMap<pro.dbro.airshare.session.Peer, ChatPeerFlow> mFlows = new HashMap<>(); /** AirShare Peer -> BLEMeshChat Peer id */ private BiMap<pro.dbro.airshare.session.Peer, Integer> mConnectedPeers = HashBiMap.create(); // <editor-fold desc="Public API"> public ChatClient(@NonNull Context context) { mContext = context; mProtocol = new BLEProtocol(); mDataStore = new ContentProviderStore(context); } public void setAirShareServiceBinder(AirShareService.ServiceBinder binder) { mAirShareServiceBinder = binder; mAirShareServiceBinder.setCallback(this); } public void setCallback(Callback callback) { mCallback = callback; } // <editor-fold desc="Identity & Availability"> public void makeAvailable() { if (mDataStore.getPrimaryLocalPeer() == null) { Timber.e("No primary Identity. Cannot make client available"); return; } if (mAirShareServiceBinder == null) { Timber.e("No AirShareBinder set. Cannot make available"); return; } mAirShareServiceBinder.advertiseLocalUser(); mAirShareServiceBinder.scanForOtherUsers(); } public void makeUnavailable() { if (mAirShareServiceBinder == null) { Timber.e("No AirShareBinder set. Cannot make unavailable"); return; } mAirShareServiceBinder.stop(); } public Peer getPrimaryLocalPeer() { return mDataStore.getPrimaryLocalPeer(); } public Peer createPrimaryIdentity(String alias) { // TODO Test if this should be moved to background thread and async call? return mDataStore.createLocalPeerWithAlias(alias, mProtocol); } // </editor-fold desc="Identity & Availability"> // <editor-fold desc="Messages"> public void sendPublicMessageFromPrimaryIdentity(String body) { MessagePacket messagePacket = mProtocol.serializeMessage((OwnedIdentityPacket) getPrimaryLocalPeer().getIdentity(), body); mDataStore.createOrUpdateMessageWithProtocolMessage(messagePacket).close(); // TODO : Send to connected peers. Future peers will get message during flow if (mAirShareServiceBinder != null) { for (pro.dbro.airshare.session.Peer peer : mConnectedPeers.keySet()) { ChatPeerFlow flow = mFlows.get(peer); // If we're actively flowing with a peer, add the message to that flow // else, send immediately if (flow != null && !flow.isComplete()) flow.queueMessage(messagePacket); else mAirShareServiceBinder.send(messagePacket.rawPacket, peer); } } } // </editor-fold desc="Messages"> public DataStore getDataStore() { return mDataStore; } // </editor-fold desc="Public API"> // <editor-fold desc="Private API"> @Override public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow, @NonNull Peer remotePeer, @NonNull ConnectionStatus status) { Timber.d("%s %s", remotePeer.getAlias(), status == ConnectionStatus.CONNECTED ? "connected" : "disconnected"); if (mCallback != null) mCallback.onAppPeerStatusUpdated(remotePeer, status); if (!mAirShareServiceBinder.isActivityReceivingMessages()) Notification.displayPeerAvailableNotification(mContext, remotePeer, status == ConnectionStatus.CONNECTED); switch (status) { case CONNECTED: mConnectedPeers.put(flow.getRemoteAirSharePeer(), remotePeer.getId()); break; case DISCONNECTED: mConnectedPeers.remove(flow.getRemoteAirSharePeer()); break; } } @Override public void onMessageSent(@NonNull ChatPeerFlow flow, @NonNull Message message, @NonNull Peer recipient) { Timber.d("Sent message: '%s'", message.getBody()); // TODO : Might be unnecessary message.close(); } @Override public void onMessageReceived(@NonNull ChatPeerFlow flow, @NonNull Message message, Peer sender) { Timber.d("Received message: '%s' with sig '%s' ", message.getBody(), DataUtil.bytesToHex(message.getSignature()).substring(0, 3)); // We don't check that mAirShareServiceBinder is not null because this callback is provoked // by the binder callbacks // Send message notification if it's a new message and no Activity is reported active if (!mAirShareServiceBinder.isActivityReceivingMessages()) { Notification.displayMessageNotification(mContext, message, sender); message.close(); } } @Override public void onDataRecevied(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer sender, @Nullable Exception exception) { ChatPeerFlow flow = mFlows.get(sender); if (flow == null) { Timber.w("No flow for %s", sender.getAlias()); return; } try { flow.onDataReceived(data); } catch (ChatPeerFlow.UnexpectedDataException e) { Timber.e(e, "Error processing received data"); } } @Override public void onDataSent(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer recipient, @Nullable Exception exception) { ChatPeerFlow flow = mFlows.get(recipient); if (flow == null) { Timber.w("No flow for %s", recipient.getAlias()); return; } try { flow.onDataSent(data); } catch (ChatPeerFlow.UnexpectedDataException e) { Timber.e(e, "Error processing sent data"); } } @Override public void onPeerStatusUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, @NonNull Transport.ConnectionStatus newStatus, boolean peerIsHost) { if (newStatus == Transport.ConnectionStatus.CONNECTED) { mConnectedPeers.put(peer, null); // We will add the BLEMeshChat peer id after identity is received Timber.d("Beginning flow with %s as %s", peer.getAlias(), peerIsHost ? "host" : "client"); mFlows.put(peer, new ChatPeerFlow(mDataStore, mProtocol, this, peer, peerIsHost, this)); } else if (newStatus == Transport.ConnectionStatus.DISCONNECTED) { if (!mConnectedPeers.containsKey(peer) || mConnectedPeers.get(peer) == null) { if (mConnectedPeers.containsKey(peer)) mConnectedPeers.remove(peer); Timber.w("Cannot report peer %s disconnected, no connection record", peer.getAlias()); return; } int blePeerId = mConnectedPeers.get(peer); Peer remotePeer = mDataStore.getPeerById(blePeerId); onAppPeerStatusUpdated(mFlows.get(peer), remotePeer, ConnectionStatus.DISCONNECTED); } } @Override public void onPeerTransportUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, int newTransportCode, @Nullable Exception exception) { // unused. The networking demands of this app appear to works fine over BLE } @Override public void sendData(pro.dbro.airshare.session.Peer peer, byte[] data) { if(mAirShareServiceBinder == null) { Timber.e("AirShare Service binder is null! Cannot send data"); return; } mAirShareServiceBinder.send(data, peer); } // </editor-fold desc="Private API"> }