/**
   * Method to handle the incoming activity data. There are two kind of messages we currently know:
   * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data
   * starts, etc.) - the second one is 20 bytes long and contains the actual activity data
   *
   * <p>The first message type is parsed by this method, for every other length of the value param,
   * bufferActivityData is called.
   *
   * @param value
   * @see #bufferActivityData(byte[])
   */
  private void handleActivityNotif(byte[] value) {
    if (value.length == activityMetadataLength) {
      handleActivityMetadata(value);
    } else {
      bufferActivityData(value);
    }
    LOG.debug(
        "activity data: length: "
            + value.length
            + ", remaining bytes: "
            + activityStruct.activityDataRemainingBytes);

    GB.updateTransferNotification(
        getContext().getString(R.string.busy_task_fetch_activity_data),
        true,
        (int)
            (((float)
                    (activityStruct.activityDataUntilNextHeader
                        - activityStruct.activityDataRemainingBytes))
                / activityStruct.activityDataUntilNextHeader
                * 100),
        getContext());

    if (activityStruct.isBlockFinished()) {
      sendAckDataTransfer(
          activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
      GB.updateTransferNotification("", false, 100, getContext());
    }
  }
 private void checkBtAvailability() {
   if (mBtAdapter == null) {
     GB.toast(
         mContext.getString(R.string.bluetooth_is_not_supported_), Toast.LENGTH_SHORT, GB.WARN);
   } else if (!mBtAdapter.isEnabled()) {
     GB.toast(mContext.getString(R.string.bluetooth_is_disabled_), Toast.LENGTH_SHORT, GB.WARN);
   }
 }
  /**
   * Method to store temporarily the activity data values got from the Mi Band.
   *
   * <p>Since we expect chunks of 20 bytes each, we do not store the received bytes it the length is
   * different.
   *
   * @param value
   */
  private void bufferActivityData(byte[] value) {
    /*
            if (scheduledTask != null) {
                scheduledTask.cancel(true);
            }
    */
    if (activityStruct.hasRoomFor(value)) {
      if (activityStruct.isValidData(value)) {
        activityStruct.buffer(value);

        /*                scheduledTask = scheduleTaskExecutor.schedule(new Runnable() {
                            @Override
                            public void run() {
                                GB.toast(getContext(), "chiederei " + activityStruct.activityDataTimestampToAck + "   "+ activityStruct.activityDataUntilNextHeader, Toast.LENGTH_LONG, GB.ERROR);
                                //sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
                                LOG.debug("runnable called");
                            }
                        }, 10l, TimeUnit.SECONDS);
        */
        if (activityStruct.isBufferFull()) {
          flushActivityDataHolder();
        }
      } else {
        // the length of the chunk is not what we expect. We need to make sense of this data
        LOG.warn(
            "GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: "
                + value.length
                + ", EXPECTED LENGTH: "
                + activityStruct.activityDataRemainingBytes);
        getSupport().logMessageContent(value);
      }
    } else {
      GB.toast(
          getContext(),
          "error buffering activity data: remaining bytes: "
              + activityStruct.activityDataRemainingBytes
              + ", received: "
              + value.length,
          Toast.LENGTH_LONG,
          GB.ERROR);
      try {
        TransactionBuilder builder = performInitialized("send stop sync data");
        builder.write(
            getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT),
            new byte[] {MiBandService.COMMAND_STOP_SYNC_DATA});
        builder.queue(getQueue());
        GB.updateTransferNotification("Data transfer failed", false, 0, getContext());
        handleActivityFetchFinish();

      } catch (IOException e) {
        LOG.error("error stopping activity sync", e);
      }
    }
  }
Example #4
0
 protected void displayError(Throwable error) {
   GB.toast(
       getContext(),
       getContext().getString(R.string.dbaccess_error_executing, error.getMessage()),
       Toast.LENGTH_LONG,
       GB.ERROR,
       error);
 }
 @Override
 public void onCreate(SQLiteDatabase db) {
   try {
     ActivityDBCreationScript script = new ActivityDBCreationScript();
     script.createSchema(db);
   } catch (RuntimeException ex) {
     GB.toast("Error creating database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
   }
 }
  /**
   * empty the local buffer for activity data, arrange the values received in groups of three and
   * store them in the DB
   */
  private void flushActivityDataHolder() {
    if (activityStruct == null) {
      LOG.debug("nothing to flush, struct is already null");
      return;
    }
    LOG.debug("flushing activity data samples: " + activityStruct.activityDataHolderProgress / 3);
    byte category, intensity, steps;

    DBHandler dbHandler = null;
    try {
      dbHandler = GBApplication.acquireDB();
      int minutes = 0;
      try (SQLiteDatabase db =
          dbHandler
              .getWritableDatabase()) { // explicitly keep the db open while looping over the
                                        // samples
        int timestampInSeconds =
            (int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000);
        if ((activityStruct.activityDataHolderProgress % 3) != 0) {
          throw new IllegalStateException(
              "Unexpected data, progress should be mutiple of 3: "
                  + activityStruct.activityDataHolderProgress);
        }
        int numSamples = activityStruct.activityDataHolderProgress / 3;
        ActivitySample[] samples = new ActivitySample[numSamples];
        SampleProvider sampleProvider = new MiBandSampleProvider();
        int s = 0;

        for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) {
          category = activityStruct.activityDataHolder[i];
          intensity = activityStruct.activityDataHolder[i + 1];
          steps = activityStruct.activityDataHolder[i + 2];

          samples[minutes] =
              new GBActivitySample(
                  sampleProvider,
                  timestampInSeconds,
                  (short) (intensity & 0xff),
                  (short) (steps & 0xff),
                  category);

          // next minute
          minutes++;
          timestampInSeconds += 60;
        }
        dbHandler.addGBActivitySamples(samples);
      } finally {
        activityStruct.bufferFlushed(minutes);
      }
    } catch (Exception ex) {
      GB.toast(getContext(), ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
    } finally {
      if (dbHandler != null) {
        dbHandler.release();
      }
    }
  }
  private void handleActivityMetadata(byte[] value) {

    if (value.length != activityMetadataLength) {
      return;
    }

    // byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
    int dataType = value[0];
    // byte 1 to 6 represent a timestamp
    GregorianCalendar timestamp = MiBandDateConverter.rawBytesToCalendar(value, 1);

    // counter of all data held by the band
    int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
    totalDataToRead *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? 3 : 1;

    // counter of this data block
    int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
    dataUntilNextHeader *= (dataType == MiBandService.MODE_REGULAR_DATA_LEN_MINUTE) ? 3 : 1;

    // there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType
    // == 1 (MiBandService.MODE_REGULAR_DATA_LEN_MINUTE)),
    // these chunks are usually 20 bytes long and grouped in blocks
    // after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
    // as we just did

    if (activityStruct.isFirstChunk() && dataUntilNextHeader != 0) {
      GB.toast(
          getContext()
              .getString(
                  R.string.user_feedback_miband_activity_data_transfer,
                  DateTimeUtils.formatDurationHoursMinutes((totalDataToRead / 3), TimeUnit.MINUTES),
                  DateFormat.getDateTimeInstance().format(timestamp.getTime())),
          Toast.LENGTH_LONG,
          GB.INFO);
    }
    LOG.info(
        "total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)");
    LOG.info(
        "data to read until next header: "
            + dataUntilNextHeader
            + " len: "
            + (dataUntilNextHeader / 3)
            + " minute(s)");
    LOG.info(
        "TIMESTAMP: "
            + DateFormat.getDateTimeInstance().format(timestamp.getTime())
            + " magic byte: "
            + dataUntilNextHeader);

    activityStruct.startNewBlock(timestamp, dataUntilNextHeader);
  }
 @Override
 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   try {
     for (int i = oldVersion; i >= newVersion; i--) {
       DBUpdateScript updater = getUpdateScript(db, i);
       if (updater != null) {
         LOG.info("downgrading activity database to version " + (i - 1));
         updater.downgradeSchema(db);
       }
     }
     LOG.info("activity database is now at version " + newVersion);
   } catch (RuntimeException ex) {
     GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
     throw ex; // reject downgrade
   }
 }
    public void startNewBlock(GregorianCalendar timestamp, int dataUntilNextHeader) {
      GB.assertThat(timestamp != null, "Timestamp must not be null");

      if (isFirstChunk()) {
        activityDataTimestampProgress = timestamp;
      } else {
        if (timestamp.getTimeInMillis() >= activityDataTimestampProgress.getTimeInMillis()) {
          activityDataTimestampProgress = timestamp;
        } else {
          // something is fishy here... better not trust the given timestamp and simply
          // (re)use the current one
          // we do accept the timestamp to ack though, so that the bogus data is properly cleared on
          // the band
          LOG.warn(
              "Got bogus timestamp: "
                  + timestamp.getTime()
                  + " that is smaller than the previous timestamp: "
                  + activityDataTimestampProgress.getTime());
        }
      }
      activityDataTimestampToAck = (GregorianCalendar) timestamp.clone();
      activityDataRemainingBytes = activityDataUntilNextHeader = dataUntilNextHeader;
      validate();
    }
 private void validate() {
   GB.assertThat(activityDataRemainingBytes >= 0, "Illegal state, remaining bytes is negative");
 }