To note. This is not my cleanest code. I threw it together as the project progressed. This project will not go into as much detail as the Arduino Firmware Walkthrough, but should give a general idea of what each part of the Android does.
Most of these files were auto-generated by the Android Studio. I am unfamiliar with the format myself (first project using Android Studio and all) but I can point out where my source code is.
To begin navigate to WorkoutAid/Android/app/src/main/java/com/example/mike/workoutaid. You will see four files here and they are all Java Classes. I will detail each Class, but the overview of the classes:
- Used primarily for handling the automation of the app. Timer events, and processing the Bluetooth communication.
- Used to manage the connection to the Bluetooth device and handle the buffer communications to the Bluetooth device.
- NOTE: To use a different Bluetooth device, the MAC address variable must be changed in the source.
- Will add modifications to make this configurable later.
- Processes the on-screen buttons used primarily for testing. Is also the main entry point to the app.
- Android Activity Layout for the Main Activity was auto generated by the layout editor in Android Studio
This is the main entry point into the app and starts all of the Services needed to get this to work. We extend a java base class (ActionBarActivity) and override some of the base classes functions.
The onCreate function is run when a new app is created (or started) or the Activity state became 'started' (due to screen rotation etc.. ). In this function we do some initial house keeping and start the BluetoothSerialService and the BackgroundService. Order is important here. The BluetoothSerialService needs to be started first due to accepting events to provide configuration that could possibly be sent by this Activity or another class (cough cough BackgroundService). After the services are started the GUI thread continues and listens for events (more on this in a bit).
The onDestroy function cleans up when the activity is going to close or some other 'close like' event occurs (possibly screen rotation etc..). This function stops the services in the reverse order in which they were started.
The onCreateOptionsMenu and onOptionsItemSelected functions were auto-generated for the Activity and I didn't do anything with them.
The buttonHandler function is a function that ALL button onClick events call. I did it this way because there is one entry / processing area for all of the on screen buttons. The actions these buttons take are not going to be detailed due to them being used for debugging. If you are interested please dig a bit into the code. It should be a short read.
Testing. Testing. Testing. A lot of the testing and verification I did was behavioral testing.. EG I looked at the Android LogCat output to verify things were happening as I would expect them to. This is not 100% ideal, but worked.
Moving on the buttonEmuHelper is used with some of the debugging buttons to help emulate a Bluetooth message coming from the BluetoothSerialService. Not 100% needed, but allowed me to test button functionality while I didn't have the Trinket Pro device with me.
The setupBluetooth and destroyBluetooth functions are helper functions to get the BluetoothSerialService properly started and destroyed.
This class is very simple, but some research is required to get it to 'work'. The class extends an Android BroadcastReceiver. The Broadcast Receiver (BCR) sits on the device and listens for broadcasts. This BCR specifically listens for com.spotify.music.metadatachanged, com.spotify.music.playbackstatechanged, and com.spotify.music.queuechanged. However the Class using this class must register the receiver to actually listen for these events.
The BrodcastTypes static class just holds the broadcast event information.
The onReceive function actually receives the event and processes it. In this class implementation, I actually generate additional broadcasts to send to the BluetoothSerialService. This made it a bit easier and having to pass bundles between classes.. Just call a static function and BAM. It works. Basically I parse the even data and spawn new events. A bit of math was involved to calculate the track length in a more readable format but it was straightforward.
Behold the class that does most of the lower level leg work. This class is responsible for handling all Bluetooth configuration, connection, and data passing. I have used this class in a few other projects but I had to modify the receive side quite a bit to handle data the way I wanted it to. This class extends an Android Service and is run in the background of the starting activity. Perfect fit for handling all Bluetooth tasks in the background.
This is most likely not the best implementation of something like this, but as I have said a lot.. It works for what I need it for. Please don't worry about all of the public static final strings and the lack of enums.
This class has a few threads that it spawns to handle to socket communication to the RF device in the Android OS. If there are enough comments, I may document this later but will have to pass for now.
The primary code I want to focus on in this class is the data communications found in the ConnectedThread. This thread is spawned once the Bluetooth RF Socket has been connected to. The run function in this thread sets up the buffers to receive messages and sits in an infinite loop waiting for messages. The call mmInStream.read(buffer); is (IIRC) a blocking call hence why its in its own thread. This receive functions basically waits until it finds the terminating sequence (0xA, 0xD, and 0x0 in order) before broadcasting the message out to the system. Once the broadcast is sent, the function clears its buffer and waits for the next message. I had some problems getting this to work and I ended up using a two buffer copy to get it to work. This is due to the fact that if I expect to get a 10 byte message, I may get a byte at a time on the socket. I needed to compound the bytes received in order to properly process. Using the terminating sequence, I was able to send and receive data and string messages in the same command. (Aside and out of scope) Take for instance sending a line update to the OLED. I needed to send the command, the line number, and the 21 char message. Most of the commands are set up this way.. Its a good thing I was able to modify this.
One of the nice features of this service is it broadcasts messages to let other components know that something happened to the Bluetooth connection.. eg connection established or connection was lost. This is useful for automating the connection and keeping a connection to the Bluetooth device.
This one is a bit tricky to talk about because it does quite a lot. I will try to break it down into sections. A handful of the class variables are used to keep state or are static variables to store certain information.
- Managing the Bluetooth Connection
- When the service is started the service registers broadcasts from the Bluetooth Service. Both alerts (connection status) and messages (data). Then an Android Handler is used to trigger the BluetoothSerialService to start a connection. The BluetoothSerialService sends connection state alerts to this BackgroundService and this service makes decisions based on that connection state given a timed event on the connection handler. If the state goes from disconnected to connected, this service sends init data to the Trinket Pro.
- Handling Bluetooth Messages Received (Status from the Trinket Pro)
- Basically I listen for the BluetoothSerialService.BLUETOOTH_SERVICE_MESSAGE broadcast (that I registered above) and determine if I sent the message or received the message. I then process the message (pass it off to either the button processing or other).
- Registering the Spotify Receiver
- This is easy because all of the work is done in the SpotifyBCR.java cass. I basically have an instance of the class and on start register that instance as a BroadcastReceiver and let it do the work.
- Updating Time / Delayed polling for status
- For this I used Java Timers. Declare an instance of the timer (clockTimer and muteStatusTimer in the code), and on service start schedule the timer to fire at X interval. Register a function that that timer calls when it runs and do the processing there.
- Useful for managing volume and mute states. I only mess with(volume up, volume down, and mute) the Audiomanager.STREAM_MUSIC in this service. Grab an instance and go (AudioManager) serviceContext.getSystemService(Context.AUDIO_SERVICE);
- KeyEvents / Runtime Commands
- A Pain in the behind. This is where I spent most of my time and this is where the most improvement can be had. I used Runtime.getRuntime().exec(cmd) to send in a key event. For some reason it liked to hook into other audio players and I saw interesting behavior. This will need to be improved in the future.
- Static Helper Functions
- Each 'public static void' function at the end of the service are used across multiple components to generate intents to send over the Bluetooth connection. These functions handle the formatting of the predefined command structure that is shared (implicitly) between Android and the Trinket Pro.
- Button Processing
- Each time a button event is received (from Bluetooth and processes) I store that state. If that state is not the same as the previous state (YAY XOR) I process the buttons else do nothing. The event also sends a deviceState which is configurable from both the Trinket Pro side and Android side. I did not use this feature, but will be used for future planning to have multiple button maps or different menus to implement additional functionality on the device.