Bike Box - Android App

Introduction

The complex and multifaceted features of the Bike Box project required some way for users to interact with modules beyond pushbuttons and switches. The creation of a smartphone app would allow the greatest flexibility and modern appeal, and easily integrate with the rest of the project. The app would allow the users to change security settings, set and reset the alarm on the Bike Box, and even know which modules were connected.



BluetoothHandler

By nature of the activity lifecycle for Android apps, though activities may be paused in the background, only one activity is active at a given time. This makes it a challenge to constantly perform Bluetooth-related tasks and ensure that the active activity is always able to receive and send Bluetooth codes. The BluetoothHandler and BluetoothMessenger classes act as the bridge between Bluetooth events and the currently running activity. BluetoothHandler uses intents to receive information about the smartphone’s Bluetooth adapter and its connection status, and uses this information to automatically connect or reconnect to the Bike Box without the user intervening. A BluetoothMessenger is a threaded object contained within BluetoothHandler that sends and receives messages once a connection has been made. To avoid repetition of code, both classes use the BroadcastUtil class for registering individual or multiple intent receivers, displaying toasts, and communicating between activities using the LocalBroadcastManager class.

In its constructor, BluetoothHandler uses BroadcastUtil to add an array of intent filters, including following Bluetooth adapter events

  • Discovery started – the Bluetooth adapter has started looking for devices
  • Discovery finished – the Bluetooth adapter has finished looking for devices
Discovery only lasts for a few seconds before turning off, most likely a constraint made by the operating system. To work around this, if discovery has finished but the Bike Box has not yet connected, discovery turns back on. BluetoothHandler also adds intent filters for three Bluetooth device events: device found, device connected, and device disconnected. It reads the name of the device through an extra, and if the name matches that of the Bike Box, if performs the appropriate action. Otherwise, it ignores the intent. If the intent indicates that the Bike Box has been found, the BluetoothHandler opens a socket and attempts to connect. If the Bike Box connected, a BluetoothMessenger thread is created and passed a reference to both the activity and the socket created earlier, so that it can begin listening for Bluetooth messages. An intent is broadcasted to notify any interested activities that the connection occurred. If the Bike Box disconnected, the thread is interrupted (causing it to terminate), discovery restarts so that the app can begin looking for the Bike Box, and once again the event is broadcasted in an intent.

BluetoothMessenger

When the device connects, the BluetoothMessenger first creates an intent receiver that listens for BluetoothHandler.QUEUE_CODE intents sent from activities and queues them to be sent out in the order received. The code is sent as a string in an extra and passed into a Code object. To decrease latency from the microcontroller processing unnecessary codes, when a code is queued, any duplicate codes with its key are removed. Note that a code from the queue is not sent out until BluetoothMessenger receives a code_processed Bluetooth code from the microcontroller, so that it knows that the previous code has already been handled. This prevents messages from being sent too quickly. From here, the thread begins.

The thread first enqueues a hello Bluetooth code that will let the microcontroller know that we are listening for messages, and then opens input and output streams. One issue faced with Bluetooth communication has been inconsistent transmission of characters. Sending a code as one string from the microcontroller (in Arduino language, using the Serial.print() function) does not ensure that the socket’s input stream will receive them together. For this reason, a forward slash must be appended to codes sent from the microcontroller to the app, to programmatically detect the end of the code. Character bytes from the input stream are concatenated until the forward slash is read.

When the BluetoothMessenger finally receives a full code to process, it passes it to a Code object, which parses it into a key/value pair (a value of -1 indicates no value). If it is a code_processed code, then the next code in the queue gets sent out through the output stream. Otherwise, based on the key, it calls a method that will decide an appropriate response. These methods are responsible for the toasts that appear from physical actions, such as switches being toggled or modules being plugged in or unplugged. The entire process continues until the BluetoothHandler interrupts the main loop or the app terminates

 
package bikebox.bikebox;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import android.bluetooth.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import java.io.*;
import android.util.*;

public class BluetoothMessenger extends Thread {
 public static final String
 HEADLIGHT_CONNECTED = "headlight-connected",
  HEADLIGHT_DISCONNECTED = "headlight-disconnected",
  BLINKERS_CONNECTED = "blinkers-connected",
  BLINKERS_DISCONNECTED = "blinkers-disconnected",
  SECURITY_CONNECTED = "security-connected",
  SECURITY_DISCONNECTED = "security-disconnected",
  CONSOLE_UPDATED = "console-update",
  BLINKER_TOGGLED_OFF = "blinker-toggled-off",
  BLINKER_TOGGLED_RIGHT = "blinker-toggled-right",
  BLINKER_TOGGLED_LEFT = "blinker-toggled-left",
  HEADLIGHT_TOGGLED_OFF = "headlight-toggled-off",
  HEADLIGHT_TOGGLED_ON = "headlight-toggled-on",
  ALARM_STATE_CHANGED = "alarm-state-changed",
  VALID_PIN = "valid-pin",
  INVALID_PIN = "invaoid-pin";

 public static final String QUEUE_CODE = "queue-code";

 public String inMessage = "";
 public static ArrayList < Code > codeQueue = new ArrayList < > ();
 public BluetoothSocket socket;
 public AppCompatActivity activity;

 public BluetoothMessenger(AppCompatActivity a, BluetoothSocket btSocket) {
  activity = a;
  socket = btSocket;

  BroadcastUtil.registerLocalReceiver(activity, QUEUE_CODE, new BroadcastReceiver() {
   @Override public void onReceive(Context context, Intent intent) {
    Code code = new Code(intent.getStringExtra("code"));
    queueCode(code);
   }
  });

  start();
 }

 public void run() {
  try {
   queueCode(new Code("hello"));

   while (!isInterrupted()) {
    InputStream inputStream = socket.getInputStream();

    while (inputStream.available() > 0) {
     final char currentChar = (char) inputStream.read();

     if (currentChar == '/') {
      final String code = inMessage;
      inMessage = "";

      activity.runOnUiThread(new Runnable() {
       @Override public void run() {
        handleCodeString(code);
       }
      });
     } else
      inMessage += currentChar;
    }
   }
  } catch (IOException e) {
   Log.e("Socket I/O exception", e.getStackTrace().toString());
  }
 }


 public void sendMessage(String message) {
  try {
   socket.getOutputStream().write(message.getBytes());
   BroadcastUtil.updateConsole(activity, "Code sent via bluetooth (" + message.getBytes().length + " bytes): " + message);
  } catch (IOException e) {
   Log.e("Socket I/O exception", e.getStackTrace().toString());
  }
 }


 public void handleCodeString(String codeString) {
  BroadcastUtil.updateConsole(activity, "Code recieved via bluetooth (" + codeString.getBytes().length + " bytes): " + codeString);

  Code code = new Code(codeString);

  if (codeQueue.size() > 0 && code.getKey().equals(Key.CODE_PROCESSED)) {
   sendMessage(codeQueue.get(0).toString());
   codeQueue.remove(0);
  }

  Integer value = code.getValue();

  switch (code.getKey()) {
   case Key.MODULE_CONNECTED:
    handleModuleConnect(value);
    break;
   case Key.MODULE_DISCONNECTED:
    handleModuleDisconnect(value);
    break;
   case Key.HEADLIGHT_STATE:
    handleHeadlightState(value);
    break;
   case Key.BLINKER_STATE:
    handleBlinkerState(value);
    break;
   case Key.ALARM_STATE:
    handleAlarmState(value);
    break;
   case Key.VERIFY_RESPONSE:
    handleVerifyResponse(value);
    break;
   case Key.SET_PIN_COMPLETE:
    handleSetPinComplete();
    break;
   case Key.RESET_PIN_COMPLETE:
    handleResetPinComplete();
  }
 }

 public void handleAlarmState(int value) {
  switch (value) {
   case 0:
    toast("Alarm disarmed.");
    break;
   case 1:
    toast("Calibrating accelerometer. Hold tight...");
    break;
   case 2:
    toast("Calibration complete. Alarm armed.");
    break;
   case 3:
    toast("Alarm triggered. Potential theft in progress...");
  }

  BroadcastUtil.broadcast(activity, ALARM_STATE_CHANGED, "state", value);
 }

 public void handleHeadlightState(int value) {
  if (value == 0) {
   broadcast(HEADLIGHT_TOGGLED_OFF);
   toast("Headlight switch deactivated - app control restored");
  } else {
   broadcast(HEADLIGHT_TOGGLED_ON);
   toast("Headlight switch activated - app control disabled");
  }
 }

 public void handleBlinkerState(int value) {
  switch (value) {
   case 0:
    broadcast(BLINKER_TOGGLED_OFF);
    toast("Blinker toggled off - app control restored");
    break;
   case 1:
    broadcast(BLINKER_TOGGLED_RIGHT);
    toast("Blinker toggled right - app control disabled");
    break;
   case 2:
    broadcast(BLINKER_TOGGLED_LEFT);
    toast("Blinker toggled left - app control disabled");
  }
 }

 public void handleModuleConnect(int value) {

  switch (value) {
   case 0:
    broadcast(HEADLIGHT_CONNECTED);
    toast("Headlight connected");
    break;
   case 1:
    broadcast(BLINKERS_CONNECTED);
    toast("Blinkers connected");
    break;
   case 2:
    broadcast(SECURITY_CONNECTED);
    toast("Security module connected");
  }
 }

 public void handleModuleDisconnect(int value) {
  switch (value) {
   case 0:
    broadcast(HEADLIGHT_DISCONNECTED);
    toast("Headlight disconnected");
    break;
   case 1:
    broadcast(BLINKERS_DISCONNECTED);
    toast("Blinkers disconnected");
    break;
   case 2:
    broadcast(SECURITY_DISCONNECTED);
    toast("Security module disconnected");
  }
 }


 public void handleVerifyResponse(int value) {
  if (value == 0)
   BroadcastUtil.broadcast(activity, INVALID_PIN);
  else
   BroadcastUtil.broadcast(activity, VALID_PIN);
 }


 public void handleSetPinComplete() {
  toast("Pin successfully reset");
 }

 public void handleResetPinComplete() {
  toast("Bike Box successfully reset.");
 }

 public void broadcast(String message) {
  LocalBroadcastManager.getInstance(activity).sendBroadcast(new Intent(message));
 }


 public void cancel() throws IOException {
  if (socket != null) socket.close();
 }

 public void queueCode(Code code) {
  if (codeQueue.isEmpty())
   sendMessage(code.toString());
  else {
   for (int i = 0; i < codeQueue.size(); i++) {
    if (codeQueue.get(i).getKey().equals(code.getKey()))
     codeQueue.remove(i);
   }

   codeQueue.add(code);
  }
 }

 public void toast(String message) {
  Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
 }
}


Home Activity

The Home activity launches when the application first loads and serves as the bridge between all other activities, containing a button for each

  • Headlight Activity
  • Blinkers
  • Security
  • Developer Console
In its constructor, the Home activity creates a BluetoothHandler and registers all appropriate receivers, most of which listen for intents that indicate whether specific modules are plugged in. The button for a module-specific activity will appear when the user plugs in the module and disappear when the user unplugs the module, preventing the potential confusion of accessing settings for a module the user does not own. If the Bike Box disconnects, the buttons for all modules disappear, and when discovery mode begins, the animation of a magnifying glass teetering side to side appears at the bottom of the column of buttons. It disappears when discovery ends.

When the user pushes a button, the associated activity launches and the Home activity remains paused in the background, allowing them to return by using the back key on their smartphone. The only exception to this behavior is the Security button, which prompts the user for a PIN before opening the Security activity. The Help button does not actually run an activity, instead opening a webpage that displays the user manual.

package bikebox.bikebox;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.*;
import android.content.*;
import android.view.*;
import android.os.*;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;

public class Home extends AppCompatActivity {
    public final static int PIN_LENGTH=4;
    public final static int PIN_MIN=1000;
    public final static int PIN_MAX=9999;

    public static final String[] ACTIONS={
            BluetoothMessenger.HEADLIGHT_CONNECTED,
            BluetoothMessenger.HEADLIGHT_DISCONNECTED,
            BluetoothMessenger.BLINKERS_CONNECTED,
            BluetoothMessenger.BLINKERS_DISCONNECTED,
            BluetoothMessenger.SECURITY_CONNECTED,
            BluetoothMessenger.SECURITY_DISCONNECTED,
            BluetoothMessenger.INVALID_PIN,
            BluetoothMessenger.VALID_PIN,
            BluetoothHandler.BIKE_BOX_CONNECTED,
            BluetoothHandler.BIKE_BOX_DISCONNECTED};

    public Button developerConsoleButton,headlightButton,blinkerButton,securityButton,pinButton;
    public EditText pinInput;
    public ImageView imageMag;
    public Animation animation;

    public final String[] DISCOVERY_ACTIONS={
            BluetoothAdapter.ACTION_DISCOVERY_STARTED,
            BluetoothAdapter.ACTION_DISCOVERY_FINISHED,
            BluetoothDevice.ACTION_ACL_CONNECTED,
            BluetoothDevice.ACTION_ACL_DISCONNECTED
    };

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        setTitle("Bike Box | Home");
        new BluetoothHandler(this);
        registerIntentReceivers();

        pinInput=(EditText)findViewById(R.id.pin_input);
        pinInput.setVisibility(View.GONE);

        imageMag = (ImageView)findViewById(R.id.image_mag);
        imageMag.setVisibility(View.GONE);

        developerConsoleButton=(Button)findViewById(R.id.developer_console_button);
        developerConsoleButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                startActivity(new Intent(Home.this, DeveloperConsole.class));
            }
        });

        headlightButton = (Button)findViewById(R.id.headlight_button);
        headlightButton.setVisibility(View.GONE);
        headlightButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(Home.this, Headlight.class));
            }
        });

        blinkerButton = (Button)findViewById(R.id.blinkers_button);
        blinkerButton.setVisibility(View.GONE);
        blinkerButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                startActivity(new Intent(Home.this, Blinkers.class));
            }
        });

        pinButton = (Button)findViewById(R.id.verify_button);
        pinButton.setVisibility(View.GONE);
        pinButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                int pin = Integer.parseInt(pinInput.getText().toString());

                if (pin >= PIN_MIN && pin <= PIN_MAX)
                    BroadcastUtil.broadcastCode(Home.this, new Code(Key.VERIFY_PIN, pin));
                else {
                    BroadcastUtil.toast(Home.this, "Pin must be between " + PIN_MIN + " and " + PIN_MIN);
                    showNumericInput();
                }
            }
        });

        securityButton = (Button)findViewById(R.id.security_button);
        securityButton.setVisibility(View.GONE);
        securityButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
            showNumericInput();
            }
        });
    }

    public void showNumericInput() {
        pinInput.setText("");
        pinInput.setVisibility(View.VISIBLE);
        pinButton.setVisibility(View.VISIBLE);
        pinInput.requestFocus();
        pinInput.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN , 0, 0, 0));
        pinInput.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP , 0, 0, 0));
    }

    public void registerIntentReceivers() {
        BroadcastUtil.registerLocalReceiver(this,ACTIONS,new BroadcastReceiver() {
            @Override public void onReceive(Context context, Intent intent) {
                switch(intent.getAction()) {
                    case BluetoothHandler.BIKE_BOX_DISCONNECTED:
                        headlightButton.setVisibility(View.GONE);
                        blinkerButton.setVisibility(View.GONE);
                        securityButton.setVisibility(View.GONE);
                        break;
                    case BluetoothMessenger.BLINKERS_CONNECTED:
                        blinkerButton.setVisibility(View.VISIBLE);
                        break;
                    case BluetoothMessenger.BLINKERS_DISCONNECTED:
                        blinkerButton.setVisibility(View.GONE);
                        break;
                    case BluetoothMessenger.HEADLIGHT_CONNECTED:
                        headlightButton.setVisibility(View.VISIBLE);
                        break;
                    case BluetoothMessenger.HEADLIGHT_DISCONNECTED:
                        headlightButton.setVisibility(View.GONE);
                        break;
                    case BluetoothMessenger.SECURITY_CONNECTED:
                        securityButton.setVisibility(View.VISIBLE);
                        break;
                    case BluetoothMessenger.SECURITY_DISCONNECTED:
                        securityButton.setVisibility(View.GONE);
                        break;
                    case BluetoothMessenger.INVALID_PIN:
                        BroadcastUtil.toast(Home.this, "Incorrect pin. Try again.");
                        pinInput.setText("");
                        showNumericInput();
                        break;
                    case BluetoothMessenger.VALID_PIN:
                        pinInput.setVisibility(View.GONE);
                        pinButton.setVisibility(View.GONE);
                        startActivity(new Intent(Home.this, Security.class));
                }
            }
        });

        BroadcastUtil.registerReceiver(this,DISCOVERY_ACTIONS,new BroadcastReceiver() {
            @Override public void onReceive(Context context, Intent intent) {
                switch(intent.getAction()) {
                    case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
                        magnifyingGlassAnimationVisible(true);
                        break;
                    case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
                        magnifyingGlassAnimationVisible(false);
                }
            }
        });
    }

    public void magnifyingGlassAnimationVisible(boolean bool) {
        if (bool) {
            imageMag.setVisibility(View.VISIBLE);
            animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.rotate);
            animation.setRepeatMode(Animation.REVERSE);
            imageMag.startAnimation(animation);
        }
        else {
            imageMag.setVisibility(View.GONE);
            imageMag.clearAnimation();
        }
    }

    public void onResume() {
        super.onResume();
        pinInput.setVisibility(View.GONE);
        pinButton.setVisibility(View.GONE);
        pinInput.setText("");
    }
}


Headlight Activity

The Headlight activity performs two functions, toggling the headlight between on and off, and adjusting the headlight brightness. There is no Bluetooth code to turn the headlight module on and off directly. Rather, all headlight activity is managed with the headlight_brightness Bluetooth code, which sends the desired pulse width modulated output for the microcontroller’s headlight-out pin. The activity sets the headlight brightness to 0% when the headlight on/off toggle is toggled off, or sets it to the percentage indicated by a progress slider when toggled on. To avoid confusion, this slider appears only when the headlight is on. Because pwm duty cycle and brightness are not linearly correlated, the following an exponential function maps the desired brightness percentage to an appropriate pwm value: pwm = floor(e^0.05542*percent)

Bringing the slider all the way down actually sets the brightness to its lowest possible value rather than turning the light off. If the user toggles the physical headlight switch on, control is automatically taken away from the app to avoid conflicts between the physical and virtual controls.

package bikebox.bikebox;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import android.widget.SeekBar.*;

public class Headlight extends AppCompatActivity {
    public final String[] ACTIONS={
            BluetoothMessenger.HEADLIGHT_DISCONNECTED,
            BluetoothMessenger.HEADLIGHT_TOGGLED_OFF,
            BluetoothMessenger.HEADLIGHT_TOGGLED_ON,
            BluetoothHandler.BIKE_BOX_DISCONNECTED};

    public static int brightnessSeekProgress=50;
    public static int state;

    public final double BRIGHTNESS_CONST=0.05542;
    public SeekBar brightnessSeek;
    public ToggleButton headlightToggle;
    public TextView brightnessLabel;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_headlight);

        setTitle("Bike Box | Headlight");
        brightnessLabel=(TextView)findViewById(R.id.brightness_label);
        defineWidgetBehavior();
        updateState(state);
        registerBroadcastReceivers();
    }

    public void defineWidgetBehavior() {
        headlightToggle=(ToggleButton) findViewById(R.id.headlight_toggle);
        headlightToggle.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                if (headlightToggle.isChecked())
                    updateState(1);
                else
                    updateState(0);
            }
        });

        brightnessSeek=(SeekBar) findViewById(R.id.brightness_seek);
        brightnessSeek.setProgress(brightnessSeekProgress);
        brightnessSeek.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override public void onStopTrackingTouch(SeekBar seekBar) {
                updateBrightness();
            }

            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
        });
    }


    public void registerBroadcastReceivers() {
        BroadcastUtil.registerLocalReceiver(this, ACTIONS, new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                switch (intent.getAction()) {
                    case BluetoothMessenger.HEADLIGHT_DISCONNECTED:
                    case BluetoothHandler.BIKE_BOX_DISCONNECTED:
                        Headlight.this.finish();
                        break;
                    case BluetoothMessenger.HEADLIGHT_TOGGLED_OFF:
                        updateState(0);
                        break;
                    case BluetoothMessenger.HEADLIGHT_TOGGLED_ON:
                        updateState(2);
                }
            }
        });
    }

    public void updateState(int newState) {
        state = newState;

        switch(state) {
            case 0: headlightOff(); break;
            case 1: updateBrightness(); break;
            case 2: headlightOnLocked();
        }
    }

    public void headlightOff() {
        headlightToggle.setEnabled(true);
        brightnessSeek.setEnabled(true);

        headlightToggle.setChecked(false);
        brightnessLabel.setVisibility(View.GONE);
        brightnessSeek.setVisibility(View.GONE);
        BroadcastUtil.broadcastCode(Headlight.this,new Code(Key.HEADLIGHT_BRIGHTNESS,0));
    }

    public void updateBrightness() {
        brightnessSeekProgress = brightnessSeek.getProgress();
        brightnessLabel.setText("Brightness: "+brightnessSeekProgress+"%");
        int pwm = calculatePWM(brightnessSeekProgress);
        headlightToggle.setChecked(true);
        brightnessLabel.setVisibility(View.VISIBLE);
        brightnessSeek.setVisibility(View.VISIBLE);
        BroadcastUtil.broadcastCode(Headlight.this, new Code(Key.HEADLIGHT_BRIGHTNESS,pwm));
    }

    public void headlightOnLocked() {
        headlightToggle.setEnabled(false);
        brightnessSeek.setEnabled(false);
        brightnessSeek.setProgress(brightnessSeek.getMax());
        updateBrightness();
    }

    public int calculatePWM(int percentage) {
        return (int)Math.pow(Math.E,BRIGHTNESS_CONST*percentage);
    }
}


Blinker Activity

The Blinker activity performs two functions, switching the blinkers between their four possible states (left, right, hazards, and off) and adjusting blinker speed. Selecting any of the four deselects all others. The relationship between the slider progress and any minimum and maximum pulse rate is determined by following linear relationship where rmax is the maximum blink rate, rmin is the minimum blink rate, s is the slider progress, smax is the maximum slider progress (minimum slider progress is 0), and r is the desired pulse rate: r = rmax - s*(rmax - rmin)/smax Like the headlight activity, if the user toggles the physical blinker switch, control is taken from the app until the toggle switch returns to the neutral position.

package bikebox.bikebox;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;
import android.widget.SeekBar.*;

public class Blinkers extends AppCompatActivity {
    public int BLUE=Color.argb(255,0,150,255);
    public final String[] ACTIONS={
            BluetoothMessenger.BLINKERS_DISCONNECTED,
            BluetoothMessenger.BLINKER_TOGGLED_OFF,
            BluetoothMessenger.BLINKER_TOGGLED_LEFT,
            BluetoothMessenger.BLINKER_TOGGLED_RIGHT,
            BluetoothHandler.BIKE_BOX_DISCONNECTED};

    public static int blinkSeekProgress=50;
    public static int state;
    public static int MIN_BLINK_RATE=200;
    public static int MAX_BLINK_RATE=800;

    Button offButton,rightButton,leftButton,hazardsButton;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_blinkers);

        setTitle("Bike Box | Blinkers");

        offButton = (Button) findViewById(R.id.off_button);
        rightButton = (Button) findViewById(R.id.right_button);
        leftButton = (Button) findViewById(R.id.left_button);
        hazardsButton = (Button) findViewById(R.id.hazards_button);

        updateState(state);
        defineWidgetBehavior();
        registerBroadcastReceivers();
    }

    void updateState(int newState) {
        state = newState;

        switch(state) {
            case 0: selectOffButton();     break;
            case 1: disableSelectRight();  break;
            case 2: disableSelectLeft();   break;
            case 3: selectRightButton();   break;
            case 4: selectLeftButton();    break;
            case 5: selectHazardsButton();

        }
    }

    public void defineWidgetBehavior() {
        offButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                BroadcastUtil.broadcastCode(Blinkers.this,new Code(Key.BLINKER_STATE,0));
                updateState(0);
            }
        });


        rightButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                BroadcastUtil.broadcastCode(Blinkers.this,new Code(Key.BLINKER_STATE,1));
                updateState(3);
            }
        });


        leftButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                BroadcastUtil.broadcastCode(Blinkers.this,new Code(Key.BLINKER_STATE,2));
                updateState(4);
            }
        });


        hazardsButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                BroadcastUtil.broadcastCode(Blinkers.this, new Code(Key.BLINKER_STATE,3));
                updateState(5);
            }
        });

        final SeekBar blinkerSeek=(SeekBar)findViewById(R.id.blink_seek);
        blinkerSeek.setProgress(blinkSeekProgress);

        blinkerSeek.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override public void onStopTrackingTouch(SeekBar seekBar) {
                blinkSeekProgress=seekBar.getProgress();
                int maxSeek=seekBar.getMax();
                int blinkRate=MAX_BLINK_RATE-blinkSeekProgress*(MAX_BLINK_RATE-MIN_BLINK_RATE)/maxSeek;
                BroadcastUtil.broadcastCode(Blinkers.this,new Code(Key.BLINKER_DURATION,blinkRate));
            }
            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
        });
    }

    public void registerBroadcastReceivers() {
        BroadcastUtil.registerLocalReceiver(this,ACTIONS,new BroadcastReceiver() {
            @Override public void onReceive(Context context, Intent intent) {
                switch(intent.getAction()) {
                    case BluetoothMessenger.BLINKERS_DISCONNECTED:
                    case BluetoothHandler.BIKE_BOX_DISCONNECTED:
                        Blinkers.this.finish();
                        break;
                    case BluetoothMessenger.BLINKER_TOGGLED_OFF:
                        updateState(0);
                        break;
                    case BluetoothMessenger.BLINKER_TOGGLED_RIGHT:
                        updateState(1);
                        break;
                    case BluetoothMessenger.BLINKER_TOGGLED_LEFT:
                        updateState(2);
                }
            }
        });
    }

    void disableSelectRight() {
        disableBlinkerButtons();
        rightButton.setTextColor(BLUE);
        leftButton.setTextColor(Color.GRAY);
    }

    void disableSelectLeft() {
        disableBlinkerButtons();
        rightButton.setTextColor(Color.GRAY);
        leftButton.setTextColor(BLUE);
    }

    void disableBlinkerButtons() {
        offButton.setTextColor(Color.GRAY);
        hazardsButton.setTextColor(Color.GRAY);

        offButton.setEnabled(false);
        rightButton.setEnabled(false);
        leftButton.setEnabled(false);
        hazardsButton.setEnabled(false);
    }

    void selectOffButton() {
        offButton.setEnabled(true);
        rightButton.setEnabled(true);
        leftButton.setEnabled(true);
        hazardsButton.setEnabled(true);

        offButton.setTextColor(BLUE);
        rightButton.setTextColor(Color.BLACK);
        leftButton.setTextColor(Color.BLACK);
        hazardsButton.setTextColor(Color.BLACK);
    }

    void selectRightButton() {
        offButton.setTextColor(Color.BLACK);
        rightButton.setTextColor(BLUE);
        leftButton.setTextColor(Color.BLACK);
        hazardsButton.setTextColor(Color.BLACK);
    }

    void selectLeftButton() {
        offButton.setTextColor(Color.BLACK);
        rightButton.setTextColor(Color.BLACK);
        leftButton.setTextColor(BLUE);
        hazardsButton.setTextColor(Color.BLACK);
    }

    void selectHazardsButton() {
        offButton.setTextColor(Color.BLACK);
        rightButton.setTextColor(Color.BLACK);
        leftButton.setTextColor(Color.BLACK);
        hazardsButton.setTextColor(BLUE);
    }

    public void toast(String message) {
        Toast.makeText(this,message, Toast.LENGTH_LONG).show();
    }
}


Security Activity

The Security activity performs four functions: arming/disarming the security module, changing the movement sensitivity, setting the alarm duration, and setting the security PIN. This activity is the most complex, performing more functions than others and involving the heaviest amount of communication with the BluetoothMessenger. Because the security module does not have any physical confirmation that requested actions have taken place, such as blinkers pulsing or the headlight changing brightness, the user receives a toast from the BluetoothMessenger after the microcontroller processes any security-related codes.

package bikebox.bikebox;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class Security extends AppCompatActivity {
    public final int ORANGE=Color.argb(255,255,165,0);
    public final int GREEN=Color.argb(255,0,100,0);
    public final int THRESHOLD_HIGH=12, THRESHOLD_LOW=3, THRESHOLD_MEDIUM=7;


    public static int sensitivity=1;
    public static int statusState=0;

    public final String[] ACTIONS={
            BluetoothMessenger.ALARM_STATE_CHANGED};

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_security);
        registerIntentReceivers();

        setTitle("Bike Box | Security");
        updateStatusState(statusState);

        setupPinSet();
        setupSensitivitySeek();
        setupAlarmToggle();
        setupAlarmDuration();
    }

    public void setupAlarmDuration() {
        final Button durationButton = (Button)findViewById(R.id.duration_button);
        final EditText durationEdit=((EditText)findViewById(R.id.alarm_duration_edit));

        durationButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                int duration=Integer.parseInt(durationEdit.getText().toString());
                durationEdit.setText("");

                BroadcastUtil.broadcastCode(Security.this, new Code(Key.ALARM_DURATION, duration));
            }
        });
    }

    public void updateStatusState(int newState) {
        statusState = newState;
        TextView statusLabel = (TextView) findViewById(R.id.status_label);
        ToggleButton armDisarmToggle = (ToggleButton) findViewById(R.id.arm_disarm_toggle);

        switch(statusState) {
            case 0:
                armDisarmToggle.setEnabled(true);
                armDisarmToggle.setChecked(false);
                statusLabel.setText("Status: Ready");
                statusLabel.setTextColor(GREEN);
                break;
            case 1:
                armDisarmToggle.setEnabled(false);
                armDisarmToggle.setChecked(true);
                statusLabel.setText("Status: Pending");
                statusLabel.setTextColor(ORANGE);
                break;
            case 2:
                armDisarmToggle.setEnabled(true);
                armDisarmToggle.setChecked(true);
                statusLabel.setText("Status: Armed");
                statusLabel.setTextColor(Color.RED);
                break;

            case 3:
                armDisarmToggle.setEnabled(true);
                armDisarmToggle.setChecked(true);
                statusLabel.setText("Status: Triggered");
                statusLabel.setTextColor(Color.RED);
        }
    }

    public void registerIntentReceivers() {
        BroadcastUtil.registerLocalReceiver(this,ACTIONS,new BroadcastReceiver() {
            @Override public void onReceive(Context context, Intent intent) {
                switch(intent.getAction()) {
                    case BluetoothMessenger.ALARM_STATE_CHANGED:
                        int s = intent.getIntExtra("state",0);
                        updateStatusState(s);
                        break;
                }
            }
        });
    }

    public void setupPinSet() {
        final Button setPinButton = (Button)findViewById(R.id.set_pin_button);
        final EditText setPinEdit=((EditText)findViewById(R.id.set_pin_edit));

        setPinButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                int pin=Integer.parseInt(setPinEdit.getText().toString());
                setPinEdit.setText("");

                if (pin >= 1000 && pin <= 9999)
                    BroadcastUtil.broadcastCode(Security.this, new Code(Key.SET_PIN, pin));
                else
                    BroadcastUtil.toast(Security.this, "Pin must be between 1000 and 9999.");
            }
        });


    }

    public void setupSensitivitySeek() {
        SeekBar sensitivitySeek = (SeekBar) findViewById(R.id.sensitivity_seek);
        sensitivitySeek.setProgress(sensitivity);
        sensitivitySeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override public void onStopTrackingTouch(SeekBar seekBar) {
                int threshold=0;
                sensitivity = seekBar.getProgress();

                switch (sensitivity) {
                    case 0:
                        threshold=THRESHOLD_HIGH;
                        break;
                    case 1:
                        threshold=THRESHOLD_MEDIUM;
                        break;
                    case 2:
                        threshold=THRESHOLD_LOW;
                }

                BroadcastUtil.broadcastCode(Security.this,new Code(Key.MOVEMENT_THRESHOLD, threshold));
            }

            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
        });
    }

    public void setupAlarmToggle() {
        ToggleButton armDisarmToggle = (ToggleButton) findViewById(R.id.arm_disarm_toggle);

        armDisarmToggle.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
            if (((ToggleButton)v).isChecked())
                BroadcastUtil.broadcastCode(Security.this,new Code(Key.ARM_ALARM,1));
            else
                BroadcastUtil.broadcastCode(Security.this,new Code(Key.ARM_ALARM, 0));
            }
        });
    }
}


Development Console Activity

The Developer Console activity assists developers in debugging the Bike Box application by displaying a textual representation of all app events in real time and allowing the user to send Bluetooth code strings directly instead of using the GUI. Such a feature would most likely not be included in a commercial application, as it can create security risks and is unnecessary for most users. A text area displays each event on a new line. Example events include connections, disconnections, and discovery actions, along with sent and received code strings displayed alongside the total number of bytes transmitted. When the developer presses the “send code” button, the app sends the string in the preceding text edit to the microcontroller over Bluetooth.

package bikebox.bikebox;

import android.support.v7.app.AppCompatActivity;
import android.content.*;
import android.os.Bundle;
import android.view.*;
import android.widget.*;

public class DeveloperConsole extends AppCompatActivity {

    public static String consoleOutput="App loading...";
    public BroadcastReceiver receiver;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_devloper_controls);

        setTitle("Bike Box | Developer Console");

        Button sendButton = (Button)findViewById(R.id.send_button);
        sendButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                EditText codeEditor=((EditText)findViewById(R.id.code_edit));
                String code=codeEditor.getText().toString();
                BroadcastUtil.broadcastCode(DeveloperConsole.this,new Code(code));
                codeEditor.setText("");
            }
        });

        BroadcastUtil.registerLocalReceiver(this, BluetoothMessenger.CONSOLE_UPDATED,new BroadcastReceiver() {
            @Override public void onReceive(Context context, Intent intent) {
                updateConsole();
            }
        });
    }

    public static void addToConsole(String str) {
        consoleOutput = str+"\n"+consoleOutput;
    }

    public void updateConsole() {
        TextView codeEditor=((TextView)findViewById(R.id.console));
        codeEditor.setText(consoleOutput);
    }

    @Override public void onPause() {
        super.onPause();
    }

    @Override public void onResume() {
        super.onResume();
        updateConsole();
    }
}