package info.guardianproject.gilga.service; import info.guardianproject.gilga.GilgaApp; import info.guardianproject.gilga.GilgaMeshActivity; import info.guardianproject.gilga.R; import info.guardianproject.gilga.model.Device; import info.guardianproject.gilga.model.DirectMessage; import info.guardianproject.gilga.model.Status; import info.guardianproject.gilga.radio.WifiController; import info.guardianproject.gilga.uplink.IRCUplink; import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.StringTokenizer; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; import android.net.wifi.p2p.WifiP2pDevice; import android.net.wifi.p2p.WifiP2pManager; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.util.Log; import android.widget.Toast; public class GilgaService extends Service { public final static String TAG = "GilgaService"; public final static String ACTION_NEW_MESSAGE = "action_new_message"; public final static String MATCH_DIRECT_MESSAGE = "(?i)^(d |dm |pm ).*$"; // Local Bluetooth adapter private BluetoothAdapter mBluetoothAdapter = null; private final static int BLUETOOTH_DISCOVERY_RETRY_TIMEOUT = 12000; //Local Device Address private String mLocalShortBluetoothAddress = ""; private String mLocalAddressHeader = ""; private WifiController mWifiController; public static Hashtable<String,Device> mDeviceMap = new Hashtable<String,Device>(); boolean mRepeaterMode = false; //by default RT trusted messages boolean mRepeatToIRC = false; //need to add more options here private IRCUplink mIRCRepeater = null; private final static String DEFAULT_IRC_CHANNEL = "#gilgamesh"; private Status mLastStatus = null; // String buffer for outgoing messages private StringBuffer mOutStringBuffer; private static Hashtable<String,Status> mMessageLog = null; //uses hash to ensure we don't display dup messages private ArrayList<DirectMessage> mQueuedDirectMessage = new ArrayList<DirectMessage>(); private DirectMessageSession mDirectChatSession; @Override public IBinder onBind(Intent arg0) { // TODO Auto-generated method stub return null; } @Override public void onCreate() { super.onCreate(); init(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { if (intent.hasExtra("status")) { String status = intent.getStringExtra("status"); if (status.matches(MATCH_DIRECT_MESSAGE)) { sendDirectMessage (status); } else { if (mLastStatus != null) mLastStatus.active = false; mLastStatus = new Status(); mLastStatus.from = getString(R.string.me_); mLastStatus.ts = new java.util.Date().getTime(); mLastStatus.trusted = false; mLastStatus.body = status; mLastStatus.reach = mDeviceMap.size(); mLastStatus.active = true; if (intent.hasExtra("type")) mLastStatus.type = intent.getIntExtra("type", Status.TYPE_GENERAL); GilgaApp.mStatusAdapter.add(mLastStatus); updateStatus (status); } } if (intent.hasExtra("repeat")) { mRepeaterMode = intent.getBooleanExtra("repeat", false); if (mRepeatToIRC) { if (mRepeaterMode) mIRCRepeater = new IRCUplink(mLocalShortBluetoothAddress,DEFAULT_IRC_CHANNEL); else if (mIRCRepeater != null) mIRCRepeater.shutdown(); } } } startListening(); startForegroundNotify(); return (START_STICKY); } @Override public void onDestroy() { super.onDestroy(); stopForeground(true); // Stop the Bluetooth chat services if (mDirectChatSession != null) mDirectChatSession.stop(); // Make sure we're not doing discovery anymore if (mBluetoothAdapter != null && mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); mBluetoothAdapter = null; } mWifiController.stopWifi(); this.unregisterReceiver(mReceiver); } private void startForegroundNotify () { String message = getString(R.string.app_name) + getString(R.string._is_running); if (mRepeaterMode) message += " | " + getString(R.string.repeater_enabled); Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_notify) .setContentTitle(getString(R.string.app_name)) .setContentText(message); if (mRepeaterMode) builder.setTicker(getString(R.string.repeater_enabled)); Intent resultIntent = new Intent(this, GilgaMeshActivity.class); // Because clicking the notification opens a new ("special") activity, there's // no need to create an artificial back stack. PendingIntent resultPendingIntent = PendingIntent.getActivity( this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT ); builder.setContentIntent(resultPendingIntent); // Sets an ID for the notification int mNotificationId = 002; startForeground(mNotificationId,builder.getNotification()); } public static Hashtable<String,Status> getMessageLog () { return mMessageLog; } private void init () { mMessageLog = new Hashtable<String,Status>(); // Get local Bluetooth adapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mLocalShortBluetoothAddress = mapToNickname(mBluetoothAdapter.getAddress()); mLocalAddressHeader = mLocalShortBluetoothAddress.substring(0,5); // mChatService = new BluetoothChatService(this, mHandler); mWifiController = new WifiController(); mWifiController.init(this); IntentFilter filter = new IntentFilter(); // Register for broadcasts when a device is discovered filter.addAction(BluetoothDevice.ACTION_FOUND); // Register for broadcasts when discovery has finished filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); // Indicates a change in the Wi-Fi P2P status. filter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); // Indicates a change in the list of available peers. filter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); // Indicates the state of Wi-Fi P2P connectivity has changed. filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); // Indicates this device's details have changed. filter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); registerReceiver(mReceiver, filter); // Initialize the buffer for outgoing messages mOutStringBuffer = new StringBuffer(""); mHandler.postDelayed(mBluetoothChecker, BLUETOOTH_DISCOVERY_RETRY_TIMEOUT); } private Runnable mBluetoothChecker = new Runnable () { public void run () { if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { try { if (!mBluetoothAdapter.isDiscovering()) mBluetoothAdapter.startDiscovery(); } catch (Exception e){} } mHandler.postDelayed(mBluetoothChecker, BLUETOOTH_DISCOVERY_RETRY_TIMEOUT); } }; public boolean processInboundMessage (String name, String address, boolean trusted) { String messageBuffer = name; StringTokenizer st = new StringTokenizer(messageBuffer,"\n"); boolean isNewDevice = false; while (st.hasMoreTokens()) { String message = st.nextToken(); if (message.startsWith("#")|| message.startsWith("!")|| message.startsWith("@")|| message.startsWith(".")|| message.startsWith(" ")) { message = message.trim(); Status status = new Status(); status.from = address; status.body = message; status.trusted = trusted; status.ts = new java.util.Date().getTime(); if (isNewMessage(status)) //have we seen this message before { isNewDevice = true; if (message.startsWith("!")) { status.type = Status.TYPE_ALERT; String alertMsg = '@' + mapToNickname(status.from) + ": " + status.body; sendNotitication(getString(R.string.alert),alertMsg); } GilgaApp.mStatusAdapter.add(status); if (mRepeaterMode && (!message.contains('@' + mLocalAddressHeader)) ) //don't RT my own tweet { String rtMessage = "RPT @" + mapToNickname(status.from) + ": " + status.body; updateStatus(rtMessage); //retweet! try { mIRCRepeater.sendMessage(rtMessage); } catch (IOException e) { Log.e(TAG,"error repeating to IRC",e); } Status statusMe = new Status(); statusMe.from = getString(R.string.me_); statusMe.ts = status.ts; statusMe.trusted = trusted; statusMe.body = rtMessage; statusMe.type = Status.TYPE_REPEAT; GilgaApp.mStatusAdapter.add(statusMe); } } } } return isNewDevice; } private boolean isNewMessage (Status msg) { if (msg.body.indexOf(':')!=-1) { String messageBody = msg.body.substring(msg.body.lastIndexOf(':')+1).trim(); String hash = MD5(messageBody); if (mMessageLog.containsKey(hash)) return false; else { mMessageLog.put(hash, msg); return true; } } else { String hash = MD5(msg.body); if (mMessageLog.containsKey(hash)) return false; else { mMessageLog.put(hash, msg); return true; } } } private void startBroadcasting() { // if(D) Log.d(TAG, "ensure discoverable"); if (mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600); discoverableIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(discoverableIntent); } if (!mBluetoothAdapter.isDiscovering()) mBluetoothAdapter.startDiscovery(); if (mDirectChatSession == null) { mDirectChatSession = new DirectMessageSession(this, mHandler); mDirectChatSession.start(); } else { // Only if the state is STATE_NONE, do we know that we haven't started already if (mDirectChatSession.getState() == DirectMessageSession.STATE_NONE) { // Start the Bluetooth chat services mDirectChatSession.start(); } } } private void startListening () { if (!mBluetoothAdapter.isDiscovering()) mBluetoothAdapter.startDiscovery(); mWifiController.startWifiDiscovery (); } private void sendDirectMessage (String message) { StringTokenizer st = new StringTokenizer(message," "); String cmd = st.nextToken(); String address = st.nextToken(); if (address.equals(mLocalShortBluetoothAddress) || address.equals(mBluetoothAdapter.getAddress())) { //can't send DM's to yourself Toast.makeText(this, R.string.you_can_t_send_private_messages_to_yourself, Toast.LENGTH_SHORT).show(); return; } try { final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); final boolean isSecure = device.getBondState()==BluetoothDevice.BOND_BONDED; StringBuffer dMessage = new StringBuffer(); while (st.hasMoreTokens()) dMessage.append(st.nextToken()).append(" "); DirectMessage dm = new DirectMessage(); dm.to = address; dm.body = dMessage.toString().trim(); dm.ts = new java.util.Date().getTime(); dm.delivered = false; mQueuedDirectMessage.add(dm); dm.trusted = isSecure; GilgaApp.mStatusAdapter.add(dm); if (mDirectChatSession == null) { mDirectChatSession = new DirectMessageSession(this, mHandler); mDirectChatSession.start(); } else { mDirectChatSession.disconnect(); } mHandler.postAtTime(new Runnable () { public void run () { mDirectChatSession.connect(device, isSecure); } }, 2000); } catch (IllegalArgumentException iae) { Toast.makeText(this, getString(R.string.error_sending_message_) + iae.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } } /** * Sends a message. * @param message A string of text to send. */ private void updateStatus(String message) { // Check that there's actually something to send if (message.length() > 0) { mOutStringBuffer.append(' ' + message + '\n'); if (mOutStringBuffer.toString().getBytes().length > 248) { mOutStringBuffer.setLength(0); mOutStringBuffer.append(' ' + message + '\n'); } mBluetoothAdapter.setName(mOutStringBuffer.toString()); mWifiController.updateWifiStatus(message); startBroadcasting() ; } } public static String mapToNickname (String hexAddressIn) { String shortAddress = new String(hexAddressIn); if (shortAddress.length() > 6) { //remove : and get last 6 characters shortAddress = shortAddress.replace(":", ""); shortAddress = shortAddress.substring(shortAddress.length()-6,shortAddress.length()); } return shortAddress.toUpperCase(); } public String MD5(String md5) { try { java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); byte[] array = md.digest(md5.getBytes()); StringBuffer sb = new StringBuffer(); for (int i = 0; i < array.length; ++i) { sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1,3)); } return sb.toString(); } catch (java.security.NoSuchAlgorithmException e) { } return null; } // The Handler that gets information back from the BluetoothChatService private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_STATE_CHANGE: //if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); switch (msg.arg1) { case DirectMessageSession.STATE_CONNECTED: //once connected, send message, then wait for response String address = msg.getData().getString("address"); ArrayList<DirectMessage> listSent = new ArrayList<DirectMessage>(); if (address != null) { synchronized (mQueuedDirectMessage) { Iterator<DirectMessage> itDm = mQueuedDirectMessage.iterator(); while (itDm.hasNext()) { DirectMessage dm = itDm.next(); if (dm.to.equals(address)) { String dmText = dm.body + '\n'; mDirectChatSession.write(dmText.getBytes()); dm.delivered = true; GilgaApp.mStatusAdapter.notifyDataSetChanged(); listSent.add(dm); } } } } mQueuedDirectMessage.removeAll(listSent); break; case DirectMessageSession.STATE_CONNECTING: // setStatus(R.string.title_connecting); break; case DirectMessageSession.STATE_LISTEN: case DirectMessageSession.STATE_NONE: // setStatus(getString(R.string.broadcast_mode_public_) + " | " + getString(R.string.you_are_) + mLocalAddress); break; } break; case MESSAGE_WRITE: //we just add it directly, but we should mark as delivered here /** byte[] writeBuf = (byte[]) msg.obj; // construct a string from the buffer String writeMessage = new String(writeBuf); Status status = new Status(); status.from = getString(R.string.me_); status.body = writeMessage; status.trusted = true; status.type = Status.TYPE_DIRECT; status.ts = new java.util.Date().getTime(); **/ // GilgaApp.mStatusAdapter.add(status); break; case MESSAGE_READ: byte[] readBuf = (byte[]) msg.obj; // construct a string from the valid bytes in the buffer String readMessage = new String(readBuf, 0, msg.arg1); String addr = msg.getData().getString("address"); StringTokenizer st = new StringTokenizer (readMessage,"\n"); while (st.hasMoreTokens()) { DirectMessage dm = new DirectMessage(); dm.from = addr; dm.body = st.nextToken(); dm.trusted = true; dm.ts = new java.util.Date().getTime(); sendNotitication(getString(R.string._pm_from_) + addr, dm.body); GilgaApp.mStatusAdapter.add(dm); } break; case MESSAGE_DEVICE_NAME: // save the connected device's name // mConnectedDeviceName = mapToNickname(msg.getData().getString(DEVICE_NAME)); // Toast.makeText(getApplicationContext(), R.string.connected_to_ // + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); break; case MESSAGE_TOAST: // Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST), // Toast.LENGTH_SHORT).show(); break; } } }; public void sendNotitication (String title, String message) { Notification.Builder builder = new Notification.Builder(this) .setSmallIcon(R.drawable.ic_notify) .setContentTitle(title) .setContentText(message); //Vibration builder.setVibrate(new long[] { 500, 1000, 500 }); builder.setAutoCancel(true); //LED builder.setLights(Color.BLUE, 3000, 3000); Intent resultIntent = new Intent(this, GilgaMeshActivity.class); // Because clicking the notification opens a new ("special") activity, there's // no need to create an artificial back stack. PendingIntent resultPendingIntent = PendingIntent.getActivity( this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT ); builder.setContentIntent(resultPendingIntent); // Sets an ID for the notification int mNotificationId = 001; // Gets an instance of the NotificationManager service NotificationManager mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // Builds the notification and issues it. mNotifyMgr.notify(mNotificationId, builder.getNotification()); } // The BroadcastReceiver that listens for discovered devices and // changes the title when discovery is finished private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // When discovery finds a device if (BluetoothDevice.ACTION_FOUND.equals(action)) { // Get the BluetoothDevice object from the Intent BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device.getName() != null) { String address = device.getAddress(); boolean isNewStatusOrDevice = processInboundMessage(device.getName(),address,device.getBondState() == BluetoothDevice.BOND_BONDED); if (isNewStatusOrDevice) //this is a gilgamesh device { Device d = new Device(device); mDeviceMap.put(device.getAddress(), d); int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI,Short.MIN_VALUE); d.mSignalInfo = rssi + context.getString(R.string.dbm); //if we have a last status, increase the number of devices reached if (mLastStatus != null) mLastStatus.reach = mDeviceMap.size(); //set to current size } else { Device d = mDeviceMap.get(device.getAddress()); if (d != null) { int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI,Short.MIN_VALUE); d.mSignalInfo = rssi + context.getString(R.string.dbm); } } if (mQueuedDirectMessage.size() > 0 && mDirectChatSession != null && (mDirectChatSession.getState() != DirectMessageSession.STATE_CONNECTED || mDirectChatSession.getState() != DirectMessageSession.STATE_CONNECTING)) { //try to do resend now if address matches if (address != null) { synchronized (mQueuedDirectMessage) { Iterator<DirectMessage> itDm = mQueuedDirectMessage.iterator(); while (itDm.hasNext()) { DirectMessage dm = itDm.next(); if (dm.to.equals(address)) { boolean isSecure = device.getBondState()==BluetoothDevice.BOND_BONDED; mDirectChatSession.connect(device, isSecure); break; } } } } } } // When discovery is finished, change the Activity title } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { mHandler.postDelayed(new Runnable () { public void run () { if (mBluetoothAdapter != null) mBluetoothAdapter.startDiscovery(); } }, BLUETOOTH_DISCOVERY_RETRY_TIMEOUT); } else if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { // Determine if Wifi P2P mode is enabled or not, alert // the Activity. int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1); if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { mWifiController.setEnabled(true); } } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { mWifiController.requestPeers(); } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { mWifiController.getNetworkInfo (); } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) { WifiP2pDevice device = (WifiP2pDevice) intent.getParcelableExtra( WifiP2pManager.EXTRA_WIFI_P2P_DEVICE); mDeviceMap.put(device.deviceAddress, new Device(device)); //if we have a last status, increase the number of devices reached if (mLastStatus != null) mLastStatus.reach = mDeviceMap.size(); //set to current size boolean trusted = false; //not sure how to do this with wifi if (!mapToNickname(device.deviceAddress).startsWith(mLocalAddressHeader)) //not me processInboundMessage(device.deviceName,device.deviceAddress,trusted); } } }; // Message types sent from the BluetoothChatService Handler public static final int MESSAGE_STATE_CHANGE = 1; public static final int MESSAGE_READ = 2; public static final int MESSAGE_WRITE = 3; public static final int MESSAGE_DEVICE_NAME = 4; public static final int MESSAGE_TOAST = 5; }