MainActivity.java 21.4 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
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<Connection> mConnectionReactor = new Reactor<Connection>() {
        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<Action> mLedReactor = new Reactor<Action>() {
        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<CountAction> mCountReactor = new Reactor<CountAction>() {
        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<DelayAction> mDelayReactor = new Reactor<DelayAction>() {
        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();
        }
    }
}