package kc.spark.pixels.android.smartconfig;
import static org.solemnsilence.util.Py.set;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import kc.spark.pixels.android.app.AppConfig;
import kc.spark.pixels.android.app.DeviceState;
import kc.spark.pixels.android.cloud.ApiFacade;
import kc.spark.pixels.android.util.Strings;
import org.solemnsilence.util.EZ;
import org.solemnsilence.util.TLog;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.content.LocalBroadcastManager;
import com.integrity_project.smartconfiglib.FirstTimeConfig;
import com.integrity_project.smartconfiglib.FirstTimeConfigListener;
import com.integrity_project.smartconfiglib.FirstTimeConfigListener.FtcEvent;
/**
* Service for handling SmartConfig operations. Can be easily started and
* stopped via the static convenience methods startSmartConfig() and
* stopSmartConfig()
*
*/
public class SmartConfigService extends Service implements FirstTimeConfigListener {
private static final TLog log = new TLog(SmartConfigService.class);
public static final String EXTRA_SSID = "EXTRA_SSID";
public static final String EXTRA_WIFI_PASSWORD = "EXTRA_WIFI_PASSWORD";
public static final String EXTRA_GATEWAY_IP = "EXTRA_GATEWAY_IP";
public static final String EXTRA_AES_KEY = "EXTRA_AES_KEY";
public static final String ACTION_START_SMART_CONFIG = "ACTION_START_SMART_CONFIG";
public static final String ACTION_STOP_SMART_CONFIG = "ACTION_STOP_SMART_CONFIG";
public static void startSmartConfig(Context ctx, String ssid, String wifiPassword,
String gatewayIP, String aesKey) {
if (aesKey == null || aesKey.length() != 16) {
aesKey = AppConfig.getSmartConfigDefaultAesKey();
log.i("Using default AES key for SmartConfig");
}
Intent intent = new Intent(ctx, SmartConfigService.class)
.setAction(SmartConfigService.ACTION_START_SMART_CONFIG)
.putExtra(EXTRA_SSID, ssid)
.putExtra(EXTRA_WIFI_PASSWORD, wifiPassword)
.putExtra(EXTRA_GATEWAY_IP, gatewayIP)
.putExtra(EXTRA_AES_KEY, aesKey);
ctx.startService(intent);
}
public static void stopSmartConfig(Context ctx) {
ctx.startService(new Intent(ctx, SmartConfigService.class)
.setAction(SmartConfigService.ACTION_STOP_SMART_CONFIG));
}
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
private LocalBroadcastManager broadcastMgr;
private ApiFacade api;
private FirstTimeConfig firstTimeConfig;
private HelloListener helloListener;
private Future<?> postOnNoHellosReceivedFuture;
private boolean isStarted = false;
private boolean receivedHello = false;
@Override
public void onCreate() {
super.onCreate();
broadcastMgr = LocalBroadcastManager.getInstance(this);
api = ApiFacade.getInstance(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
log.d("onStartCommand() - intent arg was null, intentionally doing nothing until receving an intent with an action attached.");
} else {
if (ACTION_START_SMART_CONFIG.equals(intent.getAction())) {
startSmartConfig(intent);
} else if (ACTION_STOP_SMART_CONFIG.equals(intent.getAction())) {
stopSmartConfig();
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onFirstTimeConfigEvent(FtcEvent ftcEvent, Exception error) {
log.i("onFirstTimeConfigEvent(): " + ftcEvent);
if (error != null) {
log.e("Error during first time config: ", error);
}
}
@Override
public IBinder onBind(Intent intent) {
// this method must be present but doesn't need to do anything.
return null;
}
@Override
public void onDestroy() {
log.d("onDestroy()");
super.onDestroy();
}
private void startSmartConfig(Intent intent) {
log.i("startSmartConfig()");
if (isStarted) {
log.d("Smart config already started, ignoring new request to start it gain.");
return;
}
try {
if (firstTimeConfig != null) {
firstTimeConfig.stopTransmitting();
}
if (helloListener != null) {
helloListener.stopListener();
}
if (postOnNoHellosReceivedFuture != null) {
postOnNoHellosReceivedFuture.cancel(true);
}
postOnNoHellosReceivedFuture = executor.schedule(new Runnable() {
@Override
public void run() {
if (!receivedHello) {
log.i("No Hello messages heard, making API request for the IDs any Cores we should attempt to claim.");
api.requestUnheardCores();
}
}
}, 60, TimeUnit.SECONDS);
receivedHello = false;
isStarted = true;
firstTimeConfig = buildFirstTimeConfig(this, intent);
helloListener = new HelloListener();
helloListener.startListener();
firstTimeConfig.transmitSettings();
} catch (Exception e) {
log.e("Error while transmitting settings: ", e);
}
}
private void stopSmartConfig() {
log.i("stopSmartConfig()");
if (firstTimeConfig != null) {
try {
firstTimeConfig.stopTransmitting();
helloListener.stopListener();
if (postOnNoHellosReceivedFuture != null) {
postOnNoHellosReceivedFuture.cancel(true);
}
} catch (Exception e) {
log.e("Error trying to stop transmitting: ", e);
}
firstTimeConfig = null;
helloListener = null;
postOnNoHellosReceivedFuture = null;
}
isStarted = false;
receivedHello = false;
stopSelf();
}
private void onHelloIdReceived(final String hexId) {
log.i("Core ID received via CoAP 'Hello': " + hexId);
receivedHello = true;
if (SmartConfigState.getClaimedButPossiblyUnnamedDeviceIds().contains(hexId)) {
log.i("Already claimed and named this Core: " + hexId);
return;
}
// See if this is a device we already know about
if (DeviceState.getDeviceById(hexId) != null) {
log.i("Device is alerady claimed by us but not yet offered for rename:" + hexId);
SmartConfigState.addClaimedButPossiblyUnnamedDeviceId(hexId);
broadcastMgr.sendBroadcast(new Intent(ApiFacade.BROADCAST_CORE_CLAIMED));
} else {
int delay = 2000;
log.i("New core found, will attempt to claim in " + delay / 1000 + " seconds.");
// HACK: wait for 2 seconds after receiving HELLO CoAP
EZ.runOnMainThreadDelayed(new Runnable() {
@Override
public void run() {
api.claimCore(hexId);
}
}, delay);
}
}
private FirstTimeConfig buildFirstTimeConfig(FirstTimeConfigListener listener, Intent intent)
throws Exception {
Bundle extras = intent.getExtras();
String ssid = extras.getString(EXTRA_SSID);
String wifiPassword = extras.getString(EXTRA_WIFI_PASSWORD);
String gatewayIP = extras.getString(EXTRA_GATEWAY_IP);
String aesKey = extras.getString(EXTRA_AES_KEY);
byte[] transmissionKey = aesKey.getBytes();
// AES key isn't being redacted below because it's public knowledge.
log.d("FirstTimeConfig params: SSID=" + ssid + ", wifiPassword="
+ Strings.getRedacted(wifiPassword) + ", gatewayIP=" + gatewayIP + ", aesKey="
+ aesKey);
return new FirstTimeConfig(listener, wifiPassword, transmissionKey, gatewayIP, ssid);
}
class HelloListener {
final AtomicBoolean shouldContinue = new AtomicBoolean(true);
Set<String> hexIdsHeard = set();
MulticastSocket socket;
Future<?> future;
void startListener() {
final String addr = AppConfig.getSmartConfigHelloListenAddress();
final int port = AppConfig.getSmartConfigHelloListenPort();
try {
socket = new MulticastSocket(port);
} catch (IOException e1) {
log.d("Error while listening for Hello messages", e1);
return;
}
this.future = executor.submit(new Runnable() {
@Override
public void run() {
try {
socket.joinGroup(InetAddress.getByName(addr));
// I assume this is sufficient
byte[] buffer = new byte[1024];
DatagramPacket dgram = new DatagramPacket(buffer, buffer.length);
log.d("Listening for CoAP Hello messages on " + addr + ":" + port);
while (shouldContinue.get()) {
// blocks until a datagram is received
socket.receive(dgram);
readCoreId(dgram);
dgram.setLength(buffer.length);
}
} catch (UnknownHostException e) {
// only log when we were intending to continue,
// otherwise we always show an exception in the log when
// shutting down the socket
if (shouldContinue.get()) {
log.d("Error while listening for Hello messages", e);
}
} catch (IOException e) {
// (see above)
if (shouldContinue.get()) {
log.d("Error while listening for Hello messages", e);
}
}
}
});
}
void stopListener() {
shouldContinue.set(false);
if (socket != null) {
socket.close();
socket = null;
}
if (future != null) {
future.cancel(true);
future = null;
}
hexIdsHeard.clear();
}
void readCoreId(DatagramPacket dgram) {
log.d("Received " + dgram.getLength() + " byte datagram from " + dgram.getAddress());
if (dgram.getLength() != AppConfig.getSmartConfigHelloMessageLength()) {
log.w("Received datagram with a payload having a length of " + dgram.getLength()
+ ", ignoring.");
return;
}
byte[] idAsBytes = Arrays.copyOfRange(dgram.getData(), 7, 19);
String asString = bytesToHexString(idAsBytes);
if (!hexIdsHeard.contains(asString)) {
hexIdsHeard.add(asString);
onHelloIdReceived(asString);
}
}
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
Formatter formatter = new Formatter(sb);
for (byte b : bytes) {
formatter.format("%02x", b);
}
return sb.toString();
}
}