package no.nordicsemi.puckcentral.activities; import android.app.AlertDialog; import android.app.Dialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.radiusnetworks.ibeacon.IBeacon; import org.droidparts.activity.Activity; import org.droidparts.annotation.bus.ReceiveEvents; import org.droidparts.annotation.inject.InjectDependency; import org.droidparts.annotation.inject.InjectView; import org.droidparts.concurrent.task.AsyncTaskResultListener; import org.droidparts.concurrent.task.SimpleAsyncTask; import org.droidparts.util.L; import java.util.ArrayList; import java.util.UUID; import no.nordicsemi.puckcentral.R; import no.nordicsemi.puckcentral.actuators.Actuator; import no.nordicsemi.puckcentral.adapters.PuckAdapter; import no.nordicsemi.puckcentral.bluetooth.gatt.CubeConnectionManager; import no.nordicsemi.puckcentral.bluetooth.gatt.GattManager; import no.nordicsemi.puckcentral.db.ActionManager; import no.nordicsemi.puckcentral.db.PuckManager; import no.nordicsemi.puckcentral.db.RuleManager; import no.nordicsemi.puckcentral.models.Action; import no.nordicsemi.puckcentral.models.Puck; import no.nordicsemi.puckcentral.models.Rule; import no.nordicsemi.puckcentral.services.GattServices; import no.nordicsemi.puckcentral.triggers.Trigger; public class MainActivity extends Activity { @InjectView(id = R.id.lvPucks) ListView mLvPucks; @InjectDependency private ActionManager mActionManager; @InjectDependency private RuleManager mRuleManager; @InjectDependency private PuckManager mPuckManager; @InjectDependency private GattManager mGattManager; @InjectDependency private CubeConnectionManager mCubeConnectionManager; private PuckAdapter mPuckAdapter; @Override public void onPreInject() { super.onPreInject(); setContentView(R.layout.activity_main); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPuckAdapter = new PuckAdapter(this, (new PuckManager(this)).select()); mLvPucks.addHeaderView(new View(this)); mLvPucks.addFooterView(new View(this)); mLvPucks.setAdapter(mPuckAdapter); } @ReceiveEvents(name = Trigger.TRIGGER_ADD_ACTUATOR_FOR_EXISTING_RULE) public void addActuatorForExistingRule(String _, Object rule) { selectActuatorDialog((Rule) rule); } @ReceiveEvents(name = Trigger.TRIGGER_REMOVE_RULE) public void removeRule(String _, final Rule rule) { AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(R.string.rule_remove) .setPositiveButton(getString(R.string.delete), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mRuleManager.delete(rule.id); mPuckAdapter.requeryData(); } }) .setNegativeButton(getString(R.string.abort), null); builder.create().show(); } @ReceiveEvents(name = Trigger.TRIGGER_CLOSEST_PUCK_CHANGED) public void closestPuckChanged(String _, Puck closestPuck) { mPuckAdapter.closestPuckChanged(closestPuck); } @ReceiveEvents(name = Trigger.TRIGGER_CONNECTION_STATE_CHANGED) public void connectionStateChanged(String _, GattManager.ConnectionStateChangedBundle connectionStateChangedBundle) { mPuckAdapter.connectionStateChanged(connectionStateChangedBundle); mCubeConnectionManager.connectionStateChanged(connectionStateChangedBundle); } boolean currentlyAddingZone = false; @ReceiveEvents(name = Trigger.TRIGGER_ZONE_DISCOVERED) public void createDiscoveredZoneModal(String _, final IBeacon iBeacon) { if (currentlyAddingZone) { return; } else if (mPuckManager.forIBeacon(iBeacon) != null) { Toast.makeText(this, getString(R.string.location_puck_already_paired), Toast.LENGTH_SHORT).show(); return; } currentlyAddingZone = true; ArrayList<UUID> defaultServiceUUIDs = new ArrayList<>(); defaultServiceUUIDs.add(GattServices.LOCATION_SERVICE_UUID); final Puck newPuck = new Puck(null, iBeacon.getMinor(), iBeacon.getMajor(), iBeacon.getProximityUuid(), iBeacon.getBluetoothAddress(), defaultServiceUUIDs); final View view = getLayoutInflater().inflate(R.layout.dialog_location_puck_add, null, false); ((TextView) view.findViewById(R.id.tvLocationPuckIdentifier)).setText(newPuck.getFormattedUUID()); AlertDialog.Builder builder = new AlertDialog.Builder(this) .setView(view) .setTitle(getString(R.string.puck_discovered_dialog_title)) .setPositiveButton(getString(R.string.accept), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String locationPuckName = ((TextView) view.findViewById(R.id .etLocationPuckName)).getText().toString(); newPuck.setName(locationPuckName); mPuckAdapter.create(newPuck); new FetchPuckServices(MainActivity.this, null, newPuck).execute(); } }) .setNegativeButton(getString(R.string.reject), null) .setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { currentlyAddingZone = false; } }); AlertDialog dialog = builder.create(); dialog.show(); } /** * Fetches gatt service UUIDs for a given puck, * and adds themthe pucks list of serviceUUIDs. * * If a puck advertises as non-connectable, the onServicesDiscovered callback * will (propably) never be triggered, not updating the puck. * * This approach was chosen as android provides no way to check if a BLE device * is connectable or not (that i could find). */ private class FetchPuckServices extends SimpleAsyncTask<Void> { private Puck mPuck; public FetchPuckServices(Context ctx, AsyncTaskResultListener<Void> resultListener, Puck puck) { super(ctx, resultListener); mPuck = puck; } @Override protected Void onExecute() throws Exception { BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getSystemService(Context .BLUETOOTH_SERVICE)).getAdapter(); BluetoothDevice device = bluetoothAdapter.getRemoteDevice(mPuck.getAddress()); L.e("Starting service discovery"); device.connectGatt(MainActivity.this, true, new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); L.e("Got status " + status + " and state " + newState); // Catch-all for a variety of undocumented error codes. // Documented at https://code.google.com/r/naranjomanuel-opensource-broadcom-ble/source/browse/api/java/src/com/broadcom/bt/le/api/BleConstants.java?r=f535f31ec89eb3076a2b75ddf586f4b3fc44384b if (status != BluetoothGatt.GATT_SUCCESS) { L.e("Ouch! Disconnecting! status: " + status + " newState " + newState); gatt.disconnect(); return; } if (newState == BluetoothProfile.STATE_CONNECTED) { L.e("Connected to service!"); gatt.discoverServices(); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { L.e("Link disconnected"); gatt.close(); } else { L.e("Received something else, "); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); ArrayList<UUID> serviceUUIDs = mPuck.getServiceUUIDs(); for (BluetoothGattService service : gatt.getServices()) { serviceUUIDs.add(service.getUuid()); } mPuck.setServiceUUIDs(serviceUUIDs); mPuckManager.update(mPuck); L.e("Now has services: " + mPuck.getServiceUUIDs()); gatt.disconnect(); } }); return null; } } @ReceiveEvents(name = Trigger.TRIGGER_ADD_RULE_FOR_EXISTING_PUCK) public void addRuleForExistingPuck(String _, Object puck) { Rule rule = new Rule(); rule.setPuck((Puck) puck); selectTriggerDialog(rule); } public void selectTriggerDialog(final Rule rule) { ArrayList<UUID> serviceUUIDs = rule.getPuck().getServiceUUIDs(); final String[] triggers = GattServices.getTriggersForServiceUUIDs(serviceUUIDs); AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(getString(R.string.select_trigger)) .setItems(triggers, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { rule.setTrigger(triggers[which]); selectActuatorDialog(rule); } }) .setNegativeButton(getString(R.string.abort), null); builder.create().show(); } public void selectActuatorDialog(final Rule rule) { final ArrayList<Actuator> actuators = Actuator.getActuators(); String[] actuatorDescriptions = new String[actuators.size()]; for (int i=0; i< actuators.size(); i++) { actuatorDescriptions[i] = actuators.get(i).describeActuator(); } AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(getString(R.string.select_actuator)) .setItems(actuatorDescriptions, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Action action = new Action( actuators.get(which).getId(), null); Dialog actuatorDialog = actuators.get(which) .getActuatorDialog(MainActivity.this, action, rule, new Actuator.ActuatorDialogFinishListener() { @Override public void onActuatorDialogFinish(Action action, Rule rule) { mActionManager.create(action); mRuleManager.createOrExtendExisting(rule); // This looks at the primary key of the rule entry. Therefore, if we start // the process with a new Rule object, even if the trigger and puck match, // they won't extend the rule we actually want to extend. mPuckAdapter.requeryData(); } }); actuatorDialog.show(); } } ) .setNegativeButton(getString(R.string.abort), null); builder.create().show(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); switch (id) { case R.id.action_trigger: final ArrayList<Puck> pucks = mPuckManager.getAll(); if(pucks.size() > 0) { new Thread() { @Override public void run() { Trigger.trigger(pucks.get(0), Trigger.TRIGGER_ENTER_ZONE); } }.start(); } return true; default: return super.onOptionsItemSelected(item); } } }