/** * 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); } } }
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"); }