package com.emmoco.example.blinker; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.InputType; import android.view.View; import android.widget.*; import com.emmoco.android.*; /** * This example application uses the Emmoco Framework to connect to a target running the Blinker schema * It allows the users to control the target by setting the blink counter, the blink delay and start the LED blinker * application running on the target. * * This application will receive indicators from the target when the LED changes state (on or off) and the app will * update an on-screen LED to match the state of the target's LED * * */ public class MainActivity extends Activity { // The connection states track the app's connection to the target device public enum ConnectionState { CONNECTED, DISCONNECTED }; private final static String TAG = "Blinker"; private String mCurrentDevice; private ResourceSchema mServiceSchema; private TargetConnection mTargetConnection; private ResourceValue mCmdResourceValue; private ResourceValue mLedStateResourceValue; private ResourceValue mCountResourceValue; private ResourceValue mDelayResourceValue; private ImageView mLedImage; private ImageView mIndicator; private TextView mCountTextView; private TextView mDelayTextView; private TextView mIndTextView; private Button mConnectButton; private ToggleButton mOnOffButton; private Button mPlusButton; private Button mMinusButton; private Button mSetDevice; private ConnectionState mConnectionState = ConnectionState.DISCONNECTED; private long mScaledStep; private String mDelayFormat; private SeekBar mSlider; private StickyProgressDialog mProgress; private int mLocalCount; /** * Standard Android onCreate method. This will setup all the layout and save references to the UI elements * that will be modified during the course of the program * * @param savedInstanceState */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Setup and retrieve view references setContentView(R.layout.main_activity); mLedImage = (ImageView) findViewById(R.id.led_image); mIndicator = (ImageView) findViewById(R.id.connected_indicator); mCountTextView = (TextView) findViewById(R.id.repeat_count); mIndTextView = (TextView) findViewById(R.id.indicator_count); mConnectButton = (Button) findViewById(R.id.connect_button); mOnOffButton = (ToggleButton) findViewById(R.id.on_off_button); mPlusButton = (Button) findViewById(R.id.plus_button); mMinusButton = (Button) findViewById(R.id.minus_button); mSetDevice = (Button) findViewById(R.id.setdevice_button); // Create a ProgressDialog spinner to use during connection mProgress = new StickyProgressDialog(this); mProgress.setProgressStyle(ProgressDialog.STYLE_SPINNER); mProgress.setButton(getResources().getString(R.string.label_cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Cancel the connect attempt by closing it mTargetConnection.closeSync(); mProgress.setMessage(getResources().getString(R.string.notice_cancelling)); mProgress.show(); return; } }); // Setup Emmoco specific items initializeEmmoco(); // set the slider for entering the delay value setup_slider(); // Update the UI elements to show that the we are not connected to the target updateUiState(false); } /** * Standard Android onDestroy. Here we will make sure that we are disconnected from the target on Activity * destruction. */ @Override protected void onDestroy() { super.onDestroy(); if (mTargetConnection != null) mTargetConnection.closeSync(); } /** * Do all the work loading the schema and setting up the references to the Emmoco Target's Resources */ private void initializeEmmoco() { // load the schema that was downloaded from em-hub. We include the schema as part of the app resources mServiceSchema = Framework.current().createSchemaFromRaw(this, R.raw.blinker); // get the device to connect to from the users preferences SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mCurrentDevice = prefs.getString(SetDeviceActivity.PREF_DEVICE_NAME, TargetConnection.MOCK_BLINKER_PRE); // initialize the Emmoco the resources mCmdResourceValue = mServiceSchema.newValue("cmd"); mLedStateResourceValue = mServiceSchema.newValue("ledState"); mCountResourceValue = mServiceSchema.newValue("count"); mDelayResourceValue = mServiceSchema.newValue("delay"); } /** * Update the UI elements based on whether we are connected or disconnected from the target * * If we are connected, we enable all the UI controls. * If we are disconnected, we disable all the UI controls that require talking to the target to operate * * @param connected */ private void updateUiState(boolean connected){ // indicator light mIndicator.setEnabled(connected); // delay slider mSlider.setEnabled(connected); // on/off toggle mOnOffButton.setEnabled(connected); if (mConnectionState == ConnectionState.DISCONNECTED){ // Change text on connect button mConnectButton.setText(R.string.label_connect); }else{ mConnectButton.setText(R.string.label_disconnect); } // Repeat section buttons mMinusButton.setEnabled(connected); mPlusButton.setEnabled(connected); mCountTextView.setEnabled(connected); // The device button is only active when not connected mSetDevice.setEnabled(!connected); // When connect or disconnect happens, the toggle button is not checked mOnOffButton.setChecked(false); // Clear the local counter mIndTextView.setText(""); } /** * Emmoco Reactor used to handle the connection requests to the target * * Reactors are listeners that are called when the target device state changes, read/writes complete, or the target * sends an indicator. * * Reactors are called from the context of the UI thread. * * This reactor will be called back during the connection and disconnection phases of the example. * * This will set the UI elements to certain values based on connection and disconnection states * */ enum Connection { CONNECT_DONE, DISCONNECT_DONE }; private Reactor mConnectionReactor = new Reactor() { public void exec(Connection action, int status, Bundle data) { Toast msg; int notice; switch (action) { case CONNECT_DONE: if (status == TargetConnection.STATUS_OK) { // // Connection completed successfully // mConnectionState = ConnectionState.CONNECTED; notice = R.string.notice_connected; // bind to catch if the far end disconnects mTargetConnection.bindHangupEvent(mConnectionReactor.event(Connection.DISCONNECT_DONE)); // read and display the current value of count mTargetConnection.read(mCountResourceValue, mCountReactor.event(CountAction.READ_COMPLETE)); // read and display the current value of delay mTargetConnection.read(mDelayResourceValue, mDelayReactor.event(DelayAction.READ_COMPLETE)); // Now update all the UI elements updateUiState(true); } else if (status == TargetConnection.STATUS_UUID_MISMATCH) { mConnectionState = ConnectionState.DISCONNECTED; notice = R.string.notice_uuid_mismatch; } else if (status == TargetConnection.STATUS_CONNECTION_TIMEOUT) { mConnectionState = ConnectionState.DISCONNECTED; notice = R.string.notice_timeout; } else if (status == TargetConnection.STATUS_CONNECTION_CANCELLED) { mConnectionState = ConnectionState.DISCONNECTED; notice = R.string.notice_cancelled; } else { // Connection attempt failed. Display a message notice = R.string.notice_failure; mConnectionState = ConnectionState.DISCONNECTED; } // Show a quick toast with the status of the connection msg = Toast.makeText(getApplicationContext(), notice, Toast.LENGTH_LONG); msg.show(); // Dismiss the dialog only when connect result comes back mProgress.dismissManually(); break; case DISCONNECT_DONE: // no need to check the status, disconnect always completes by leaving us // in the disconnected state mConnectionState = ConnectionState.DISCONNECTED; msg = Toast.makeText(getApplicationContext(), R.string.notice_disconnected, Toast.LENGTH_SHORT); msg.show(); updateUiState(false); break; } } }; /** * Reactor used to handle the writes and indicators for the blinker schema. This reactor will be called back * after each write (where we do not need to do anything at all) and the firing of the indicator. When the * indicator is fired, it signals when the LED changes, so we can use that indicator to trigger our UI LED and * countdown value to change */ enum Action { WRITE_CMD_COMPLETE, INDICATOR }; private Reactor mLedReactor = new Reactor() { public void exec(Action action, int status, Bundle data) { switch (action) { case WRITE_CMD_COMPLETE: // nothing to do break; case INDICATOR: // update the virtual led display based on the value that gets updated when the indicator is fired. // We registered this value after connection occurred with bindIndicatorValue() if (mLedStateResourceValue.toString().equals("LED_OFF")) { mLedImage.setSelected(false); if (mLocalCount == 0){ // Reached the end, we disable some UI elements mIndTextView.setText(""); mOnOffButton.setChecked(false); } } else { mLedImage.setSelected(true); mLocalCount--; mIndTextView.setText(String.valueOf(mLocalCount)); } break; } } }; /** * Reactor used for count value. When the count value is read back from the target, the UI value is updated with * the new value that is read back */ enum CountAction { READ_COMPLETE, WRITE_COMPLETE }; private Reactor mCountReactor = new Reactor() { public void exec(CountAction action, int status, Bundle data) { Long count; switch (action) { case READ_COMPLETE: count = new Long(mCountResourceValue.toLong()); mCountTextView.setText(count.toString()); break; case WRITE_COMPLETE: mTargetConnection.read(mCountResourceValue,mCountReactor.event(CountAction.READ_COMPLETE)); break; } } }; /** * Reactor used for delay resource. When the delay resource is read or written, the seek bar value is moved (on * read) and the display value is updated */ enum DelayAction { READ_COMPLETE, WRITE_COMPLETE }; private Reactor mDelayReactor = new Reactor() { public void exec(DelayAction action, int status, Bundle data) { double delay; long delayOrd; switch (action) { case READ_COMPLETE: delay = mDelayResourceValue.toDouble(); delayOrd = mDelayResourceValue.toLong(); mDelayTextView.setText(String.format(mDelayFormat, delay)); // set the progress bar position based on the ordinal value. We use the ordinal value // to determine where to position the seek on the 0 to 100 scale of the seekbar. int progress; if (delayOrd == mDelayResourceValue.numMaxOrdinal()) { progress = 100; } else { progress = (int) (delayOrd * mScaledStep); } mSlider.setProgress(progress); break; case WRITE_COMPLETE: // Nothing to do break; } } }; /** * The set-device button has been pressed, start the sub-activity which will change the device preference */ final private int DEVICE_SELECT = 1; public void onSetDeviceClick(View v) { Intent i = new Intent(this, SetDeviceActivity.class); startActivityForResult(i, DEVICE_SELECT); } /** * Process the result from the set-device sub activity * * @param requestCode * @param resultCode * @param data */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode){ case DEVICE_SELECT: // If the device value string has changed, update our local copy SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String device = prefs.getString(SetDeviceActivity.PREF_DEVICE_NAME, TargetConnection.MOCK_BLINKER_PRE); // If device has changed we need to disconnect if connected, then the user can reconnect // manually to the new device if (!mCurrentDevice.contentEquals(device)){ mCurrentDevice = device; if ((mTargetConnection != null) &&(mTargetConnection.isConnected())){ onConnectToggleClick(null); } } break; } } /** * Handler function for when the on/off button is press. In this handler, depending on the current state, * the command is sent to start or stop the blinker on the target * * @param v unused */ public void onToggleOnOff(View v) { String command; ToggleButton b = (ToggleButton)v; // Determine which command to send if (b.isChecked()){ // We have been turned on command = "START_CMD"; // Set up the local count value so we can count down mLocalCount = mCountResourceValue.toInt(); ((TextView)findViewById(R.id.indicator_count)).setText(String.valueOf(mLocalCount)); }else{ // We have been turned off command = "STOP_CMD"; } // Send command mCmdResourceValue.assignEnum(command); mTargetConnection.write(mCmdResourceValue, mLedReactor.event(Action.WRITE_CMD_COMPLETE)); } /** * Handler for the connect toggle button * * Normally in an app the user doesn't worry about connecting or disconnecting, we * simply connect when the main activity becomes visible and disconnect * when another activity comes to the front, but this handler is used to * give the user the ability to toggle the connect state by touching the * connect/disconnect button * * @param v unused */ public void onConnectToggleClick(View v) { // toggle connect or disconnect switch (mConnectionState) { case DISCONNECTED: // Show Connection spinner mProgress.setMessage(getResources().getString(R.string.notice_connecting)); mProgress.setCancelable(false); mProgress.show(); // configure the generic mock device if it is being used with some default values if (mCurrentDevice.equals(TargetConnection.MOCK_PRE)) { MockTargetDevice.reset(); MockTargetDevice.current().setTickRate(500); MockTargetDevice.current().setAccessDelay(2); } // connect to the device using the schema mTargetConnection = new TargetConnection(mCurrentDevice, getApplicationContext()); mTargetConnection.bindSchema(mServiceSchema); mTargetConnection.bindIndicatorValue("ledState", mLedReactor.event(Action.INDICATOR),mLedStateResourceValue); mTargetConnection.open(mConnectionReactor.event(Connection.CONNECT_DONE)); break; case CONNECTED: mTargetConnection.close(mConnectionReactor.event(Connection.DISCONNECT_DONE)); break; } } /** * handler for the minus Button * the minus button decrements the count resource and writes it to blinker * * @param v */ public void onMinusButtonClick(View v) { long count = Long.parseLong(mCountTextView.getText().toString()); if (count > 0) { count--; } mCountResourceValue.assignInt(count); mTargetConnection.write(mCountResourceValue, mCountReactor.event(CountAction.WRITE_COMPLETE)); } /** * Handler for the plus Button * The plus button increments the count resource and writes it to blinker * * @param v */ public void onPlusButtonClick(View v) { long count = Long.parseLong(mCountTextView.getText().toString()); if (count < mCountResourceValue.intMax()){ count++; } mCountResourceValue.assignInt(count); mTargetConnection.write(mCountResourceValue, mCountReactor.event(CountAction.WRITE_COMPLETE)); } /** * Sets up the seekbar to be used as the input for changing the target's delay values. This must be called * after the schema and resource values are set up, We need to do some math here to split the 0 to 100 range of the * seekbar into the number of distinct values we can set the delay resource, and map those parts of the seekbar * into values that are set. * */ private void setup_slider() { // calculate scaled step mScaledStep = 100 / mDelayResourceValue.numMaxOrdinal(); mDelayFormat = "%.1f"; mDelayTextView = (TextView) findViewById(R.id.delay); mDelayTextView.setRawInputType(InputType.TYPE_CLASS_NUMBER); mSlider = (SeekBar) findViewById(R.id.slider); // initializeEmmoco slider listener mSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { public void onStopTrackingTouch(SeekBar seekBar) { // they took their finger off the bar, so write to the device Double delay = Double.parseDouble(mDelayTextView.getText().toString()); mDelayResourceValue.assignNum(delay); mTargetConnection.write(mDelayResourceValue, mDelayReactor.event(DelayAction.WRITE_COMPLETE)); } public void onStartTrackingTouch(SeekBar seekBar) { } public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { long ord = progress / mScaledStep; mDelayResourceValue.assignNum(ord); mDelayTextView.setText(String.format(mDelayFormat, mDelayResourceValue.toDouble())); } }); } /** * Android UI tweak. * Normally a progress dialog disappears when you hit a button (like cancel), but we want our dialog to stick around * until our background task has completed the cancellation phase. The easiest way is to override the dismiss() so * the dialog does not automatically close */ private class StickyProgressDialog extends ProgressDialog { private StickyProgressDialog(Context context) { super(context); } @Override public void dismiss() { // do nothing } public void dismissManually() { super.dismiss(); } } }