package com.siliconlabs.bledemo.fragment; import android.app.Dialog; import android.app.Fragment; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import androidx.core.content.ContextCompat; import androidx.core.util.Pair; import android.os.Handler; import android.text.Editable; import android.text.Html; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.siliconlabs.bledemo.R; import com.siliconlabs.bledemo.activity.DeviceServicesActivity; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Bit; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Characteristic; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Descriptor; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Enumeration; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Field; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.Service; import com.siliconlabs.bledemo.bluetoothdatamodel.datatypes.ServiceCharacteristic; import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.Common; import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.Consts; import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.Converters; import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.Engine; import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.Unit; import com.siliconlabs.bledemo.services.BluetoothLeService; import com.siliconlabs.bledemo.utils.StringUtils; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import static java.lang.StrictMath.abs; public class FragmentCharacteristicDetail extends Fragment { //padding public static final int FIELD_CONTAINER_PADDING_TOP = 15; public static final int FIELD_CONTAINER_PADDING_BOTTOM = 15; public static final int FIELD_VALUE_EDIT_TEXT_PADDING_LEFT = 0; public static final int FIELD_VALUE_EDIT_TEXT_PADDING_TOP = 0; public static final int FIELD_VALUE_EDIT_TEXT_PADDING_RIGHT = 0; public static final int FIELD_VALUE_EDIT_TEXT_PADDING_BOTTOM = 0; //margins public static int FIELD_VALUE_EDIT_LEFT_MARGIN = 15; private int EDIT_NOT_CLEAR_ID = 1000; private Context context; final private int REFRESH_INTERVAL = 500; // miliseconds final private String TYPE_FLOAT = "FLOAT"; final private String TYPE_SFLOAT = "SFLOAT"; final private String TYPE_FLOAT_32 = "float32"; final private String TYPE_FLOAT_64 = "float64"; private BluetoothGattCharacteristic mBluetoothCharact; private BluetoothGattService mGattService; private Characteristic mCharact; private BluetoothLeService mBluetoothLeService; private Service mService; private List<BluetoothGattDescriptor> mDescriptors; private Iterator<BluetoothGattDescriptor> iterDescriptor; private BluetoothGattDescriptor lastDescriptor; private boolean readable = false; private boolean writeable = false; private boolean writeableWithoutResponse = false; private boolean notify = false; private boolean notificationsEnabled = false; private boolean indicationsEnabled = false; private boolean isRawValue = false; private boolean parseProblem = false; private int offset = 0; // in bytes private int currRefreshInterval = REFRESH_INTERVAL; // in seconds private byte[] value; private byte[] previousValue; private BluetoothGatt mDevice; private int defaultMargin; private boolean foundField = false; // the following arraylist is used to check if fields in dialog for editable characteristics are empty, then set enabled stat for save btn ArrayList<EditText> editTexts = new ArrayList<>(); private int viewBackgroundColor = 0; public String address; public View fragmentRootView; public ViewGroup viewGroup; private LinearLayout valuesLayout; private EditText hexEdit; private EditText asciiEdit; private EditText decimalEdit; private EditText hex; private EditText ascii; private EditText decimal; Dialog editableFieldsDialog; LinearLayout writableFieldsContainer; Button saveValueBtn; Button clearBtn; ImageView closeIV; HashMap<Field, Boolean> fieldsInRangeMap; HashMap<Field, Boolean> fieldsValidMap; private boolean writeWithResponse = true; private Handler handler; private boolean writeString = false; private String parsingProblemInfo; public boolean displayWriteDialog = false; private final String HEX_ID = "HEX"; private final String ASCII_ID = "ASCII"; private final String DECIMAL_ID = "DECIMAL"; private ArrayList<View> hidableViews = new ArrayList<>(); private ArrayList<EditText> rawValueViews = new ArrayList<>(); private ArrayList<String> rawValueData; private final Runnable postLoadValueViews = new Runnable() { @Override public void run() { loadValueViews(); } }; private final Runnable postDisplayValues = new Runnable() { @Override public void run() { displayValues(); } }; public FragmentCharacteristicDetail() { } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // TODO inflate appropriate layout file viewBackgroundColor = ContextCompat.getColor(getActivity(), R.color.silabs_white); handler = new Handler(); View view = inflater.inflate(R.layout.fragment_characteristic_details, container, false); fragmentRootView = view; viewGroup = container; context = getActivity(); defaultMargin = getResources().getDimensionPixelSize(R.dimen.characteristic_text_left_margin); valuesLayout = view.findViewById(R.id.values_layout); mDevice = ((DeviceServicesActivity) getActivity()).getBluetoothGatt(); mCharact = Engine.getInstance().getCharacteristic(mBluetoothCharact.getUuid()); mService = Engine.getInstance().getService(mBluetoothCharact.getService().getUuid()); mDescriptors = new ArrayList<>(); setProperties(); Log.d("Charac", mBluetoothCharact.getUuid().toString() + " " + mBluetoothCharact.getInstanceId()); mBluetoothCharact.getProperties(); configureWriteable(); updateBall(); if (!isRawValue) { prepareValueData(); } loadValueViews(); if (displayWriteDialog) { showCharacteristicWriteDialog(); } return view; } @Override public void onResume() { super.onResume(); } @Override public void onPause() { super.onPause(); //getActivity().unregisterReceiver(mBluetoothLeReceiver); } @Override public void onDestroy() { super.onDestroy(); mBluetoothLeService = null; } // Builds activity UI based on characteristic content private void loadValueViews() { fieldsInRangeMap = new HashMap<>(); fieldsValidMap = new HashMap<>(); editTexts.clear(); if (!isRawValue) { if (parseProblem || !addNormalValue()) { editTexts.clear(); addInvalidValue(); } } else { addRawValue(); } } // Configures characteristic if it is writeable private void configureWriteable() { if (writeable || writeableWithoutResponse) { initCharacteristicWriteDialog(); } } public void onDescriptorWrite(UUID descriptorUuid) { if (Common.equalsUUID(descriptorUuid, lastDescriptor.getUuid())) { writeNextDescriptor(); } } public void onActionDataWrite(String uuid, final int status) { if (!mBluetoothCharact.getUuid().toString().equals(uuid)) { return; } getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (status == BluetoothGatt.GATT_SUCCESS) { Toast.makeText(getActivity(), getText(R.string.characteristic_write_success), Toast.LENGTH_SHORT).show(); editableFieldsDialog.dismiss(); ((DeviceServicesActivity) getActivity()).refreshCharacteristicExpansion(); } else { Toast.makeText(getActivity(), getText(R.string.characteristic_write_fail), Toast.LENGTH_SHORT).show(); } } }); } public void onActionDataAvailable(String uuidCharacteristic) { if (currRefreshInterval >= REFRESH_INTERVAL) { if (uuidCharacteristic.equals(mBluetoothCharact.getUuid().toString())) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (currRefreshInterval >= REFRESH_INTERVAL) { currRefreshInterval = 0; offset = 0; value = mBluetoothCharact.getValue(); if (indicationsEnabled || notificationsEnabled) { valuesLayout.removeAllViews(); loadValueViews(); } else if (Arrays.equals(value, previousValue)) { // redraw ui elements hideValues(); handler.removeCallbacks(postDisplayValues); handler.postDelayed(postDisplayValues, 150); } else { valuesLayout.removeAllViews(); handler.removeCallbacks(postLoadValueViews); handler.postDelayed(postLoadValueViews, 150); } if (value != null) { previousValue = value.clone(); } } } }); } } } public void setmBluetoothCharact(BluetoothGattCharacteristic mBluetoothCharact) { this.mBluetoothCharact = mBluetoothCharact; } public void setmService(BluetoothGattService service) { mGattService = service; } // Sets property members for characteristics private void setProperties() { if (Common.isSetProperty(Common.PropertyType.READ, mBluetoothCharact.getProperties())) { readable = true; } if (Common.isSetProperty(Common.PropertyType.WRITE, mBluetoothCharact.getProperties())) { writeable = true; } if (Common.isSetProperty(Common.PropertyType.WRITE_NO_RESPONSE, mBluetoothCharact.getProperties())) { writeableWithoutResponse = true; } if (Common.isSetProperty(Common.PropertyType.NOTIFY, mBluetoothCharact.getProperties()) || Common.isSetProperty(Common.PropertyType.INDICATE, mBluetoothCharact.getProperties())) { notify = true; } //Display IEEE characteristic as raw data if (mCharact == null || mCharact.getFields() == null || mCharact.getName().equals("IEEE 11073-20601 Regulatory Certification Data List")) { isRawValue = true; } } private void writeValueToCharacteristic() { EditText hexEdit = editableFieldsDialog.findViewById(R.id.hex_edit); if (writeWithResponse) { mBluetoothCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); } else { mBluetoothCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } if (hexEdit != null) { String hex = hexEdit.getText().toString().replaceAll("\\s+", ""); byte[] newValue = hexToByteArray(hex); try { Log.d("Name", "" + mDevice.getDevice().getName()); Log.d("Address", "" + mDevice.getDevice().getAddress()); Log.d("Service", "" + mBluetoothCharact.getService().getUuid()); Log.d("Charac", "" + mBluetoothCharact.getUuid()); mBluetoothCharact.setValue(newValue); Log.d("hex", "" + Converters.getHexValue(mBluetoothCharact.getValue())); mDevice.writeCharacteristic(mBluetoothCharact); } catch (Exception e) { Log.e("Service", "null" + e); } } else { if (possibleToSave()) { mBluetoothCharact.setValue(value); mDevice.writeCharacteristic(mBluetoothCharact); Log.d("write_val", "Standard Value to write (hex): " + Converters.getHexValue(value)); } } } private void hideValues() { for (View view : hidableViews) { view.setVisibility(View.GONE); } rawValueData = new ArrayList<>(); for (EditText et : rawValueViews) { rawValueData.add(et.getText().toString()); et.setText(""); } } private void displayValues() { for (View view : hidableViews) { view.setVisibility(View.VISIBLE); } int i = 0; for (EditText et : rawValueViews) { et.setText(rawValueData.get(i++)); } } private boolean possibleToSave() { boolean validField = true; for (Map.Entry<Field, Boolean> entry : fieldsValidMap.entrySet()) { validField = entry.getValue(); if (!validField) { break; } } boolean entryInRange = true; for (Map.Entry<Field, Boolean> entry : fieldsInRangeMap.entrySet()) { entryInRange = entry.getValue(); if (!entryInRange) { break; } } if (!validField) { Toast.makeText(context, context.getString(R.string.characteristic_dialog_invalid_input), Toast.LENGTH_SHORT).show(); return false; } else if (!entryInRange) { Toast.makeText(context, context.getString(R.string.characteristic_dialog_invalid_out_of_range), Toast.LENGTH_SHORT).show(); return false; } else { return true; } } // Count time that is used to preventing from very fast refreshing view private void updateBall() { Timer timer = new Timer(); TimerTask updateBall = new TimerTask() { @Override public void run() { currRefreshInterval += REFRESH_INTERVAL; } }; timer.scheduleAtFixedRate(updateBall, 0, REFRESH_INTERVAL); } public boolean getNotificationsEnabled() { return notificationsEnabled; } public void setNotificationsEnabled(boolean enabled) { notificationsEnabled = enabled; } public boolean getIndicationsEnabled() { return indicationsEnabled; } public void setIndicationsEnabled(boolean enabled) { indicationsEnabled = enabled; } public Characteristic getmCharact() { return mCharact; } // Gets all characteristic descriptors private ArrayList<Descriptor> getCharacteristicDescriptors() { if (mService == null || mCharact == null) { return null; } ArrayList<Descriptor> descriptors = new ArrayList<>(); for (ServiceCharacteristic charact : mService.getCharacteristics()) { if (charact.getType().equals(mCharact.getType())) { for (Descriptor descriptor : charact.getDescriptors()) { descriptors.add(Engine.getInstance().getDescriptorByType(descriptor.getType())); } } } return descriptors; } // Checks if given descriptor is available in this characteristic private boolean isDescriptorAvailable(ArrayList<Descriptor> descriptors, BluetoothGattDescriptor blDescriptor) { for (Descriptor descriptor : descriptors) { if (Common.equalsUUID(descriptor.getUuid(), blDescriptor.getUuid())) { return true; } } return false; } // Writes next descriptor in order to enable notification or indication protected void writeNextDescriptor() { if (iterDescriptor.hasNext()) { lastDescriptor = iterDescriptor.next(); if (lastDescriptor.getCharacteristic() == mBluetoothCharact) { lastDescriptor.setValue(Common.isSetProperty(Common.PropertyType.NOTIFY, mBluetoothCharact .getProperties()) ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); //mBluetoothLeService.writeDescriptor(mDevice, lastDescriptor); mDevice.writeDescriptor(lastDescriptor); } } } // Builds activity UI in tree steps: // a) add views based on characteristic content without setting values // b) add problem info view // c) add raw views (hex, ASCII, decimal) with setting values private void addInvalidValue() { valuesLayout.removeAllViews(); addNormalValue(); addProblemInfoView(); addRawValue(); } // Only called when characteristic is standard Bluetooth characteristic // Build activity UI based on characteristic content and also take account // of field requirements private boolean addNormalValue() { if (writableFieldsContainer != null) { writableFieldsContainer.removeAllViews(); } for (int i = 0; i < mCharact.getFields().size(); i++) { try { Field field = mCharact.getFields().get(i); addField(field); } catch (Exception ex) { Log.i("CharacteristicUI", String.valueOf(i)); Log.i("Characteristic value", Converters.getDecimalValue(value)); parsingProblemInfo = prepareParsingProblemInfo(mCharact); parseProblem = true; return false; } } for (EditText et : editTexts) { et.setText(""); } return true; } private String prepareParsingProblemInfo(Characteristic characteristic) { StringBuilder builder = new StringBuilder(); builder.append("An error occurred while parsing this characteristic.").append("\n"); if (value == null) return builder.toString(); int expectedBytes = 0; int readSize = value.length; try { for (int i = 0; i < characteristic.getFields().size(); i++) { Field field = characteristic.getFields().get(i); expectedBytes += Engine.getInstance().getFormat(field.getFormat()); } } catch (NullPointerException ex) { return builder.toString(); } if (expectedBytes != readSize) { int expectedBits = expectedBytes * 8; int readBits = readSize * 8; builder.append("Reason: expected data length is ") .append(expectedBits) .append("-bit (") .append(expectedBytes); if (expectedBytes == 1) { builder.append(" byte), "); } else { builder.append(" bytes), "); } builder.append("\n") .append("read data length is ") .append(readBits) .append("-bit (") .append(readSize); if (readSize == 1) { builder.append(" byte)."); } else { builder.append(" bytes)."); } } return builder.toString(); } // Add single field private void addField(Field field) { if (isFieldPresent(field)) { if (field.getReferenceFields().size() > 0) { for (Field subField : field.getReferenceFields()) { addField(subField); } } else { if (field.getBitfield() != null) { addBitfield(field); } else if (field.getEnumerations() != null && field.getEnumerations().size() > 0) { addEnumeration(field); } else { addValue(field); } } } } // Initializes byte array with empty characteristic content private void prepareValueData() { int size = characteristicSize(); if (size != 0) { value = new byte[size]; } } // Returns characteristic size in bytes private int characteristicSize() { int size = 0; for (Field field : mCharact.getFields()) { size += fieldSize(field); } return size; } // Returns only one field size in bytes private int fieldSize(Field field) { String format = field.getFormat(); if (format != null) { return Engine.getInstance().getFormat(format); } else if (field.getReferenceFields().size() > 0) { int subFieldsSize = 0; for (Field subField : field.getReferenceFields()) { subFieldsSize += fieldSize(subField); } return subFieldsSize; } else { return 0; } } // Checks if field is present based on it's requirements and bitfield // settings private boolean isFieldPresent(Field field) { if (parseProblem) { return true; } if (field.getRequirement() == null || field.getRequirement().equals(Consts.REQUIREMENT_MANDATORY)) { return true; } else { for (Field bitField : getBitFields()) { for (Bit bit : bitField.getBitfield().getBits()) { for (Enumeration enumeration : bit.getEnumerations()) { if (enumeration.getRequires() != null && field.getRequirement().equals(enumeration.getRequires())) { return checkRequirement(bitField, enumeration, bit); } } } } } return false; } // Checks requirement on exactly given bitfield, enumeration and bit private boolean checkRequirement(Field bitField, Enumeration enumeration, Bit bit) { int formatLength = Engine.getInstance().getFormat(bitField.getFormat()); int off = getFieldOffset(bitField); int val = readInt(off, formatLength); int enumVal = readEnumInt(bit.getIndex(), bit.getSize(), val); return (enumVal == enumeration.getKey()); } /* * * --- VALUE SETTERS & GETTERS SECTION --- */ // Converts string given in hexadecimal system to byte array public byte[] hexToByteArray(String hex) { if (hex.length() != 0 && hex.length() % 2 != 0) { hex = "0" + hex; } int len = hex.length() / 2; byte[] byteArr = new byte[len]; for (int i = 0; i < byteArr.length; i++) { int init = i * 2; int end = init + 2; int temp = Integer.parseInt(hex.substring(init, end), 16); byteArr[i] = (byte) (temp & 0xFF); } return byteArr; } // Converts string given in decimal system to byte array private byte[] decToByteArray(String dec) { if (dec.length() == 0) { return new byte[]{}; } String[] decArray = dec.split(" "); byte[] byteArr = new byte[decArray.length]; for (int i = 0; i < decArray.length; i++) { try { byteArr[i] = (byte) (Integer.parseInt(decArray[i])); } catch (NumberFormatException e) { return new byte[]{0}; } } return byteArr; } // Converts int to byte array private byte[] intToByteArray(int newVal, int formatLength) { byte[] val = new byte[formatLength]; for (int i = 0; i < formatLength; i++) { val[i] = (byte) (newVal & 0xff); newVal >>= 8; } return val; } // Checks if decimal input value is valid private boolean isDecValueValid(String decValue) { char[] value = decValue.toCharArray(); int valLength = value.length; boolean valid = false; if (decValue.length() < 4) { valid = true; } else { valid = value[valLength - 1] == ' ' || value[valLength - 2] == ' ' || value[valLength - 3] == ' ' || value[valLength - 4] == ' '; } return valid; } // Reads integer value for given offset and field size private int readInt(int offset, int size) { int val = 0; for (int i = 0; i < size; i++) { val <<= 8; val |= value[offset + i]; } return val; } private String getSint16AsString(byte[] array) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < array.length; i++) { if (array[i] < 0) { array[i] = (byte) (array[i] + 256); } } for (int i = array.length - 1; i >= 0; i--) { builder.append(Converters.getHexValue(array[i])); } int result = Integer.parseInt(builder.toString(), 16); if (result >= 32768) result = result - 65536; return String.valueOf(result); } // Reads next value for given format private String readNextValue(String format) { if (value == null) { return ""; } int formatLength = Engine.getInstance().getFormat(format); // if format is sint16 if (format.toLowerCase().equals("sint16")) { byte[] array = Arrays.copyOfRange(value, offset, offset + formatLength); offset += formatLength; return getSint16AsString(array); } // binaryString is used for sints, used to fix original bluegiga code ignoring data format type StringBuilder binaryString = new StringBuilder(); try { for (int i = offset; i < offset + formatLength; i++) { binaryString.append(String.format("%8s", Integer.toBinaryString(value[i] & 0xFF)).replace(' ', '0')); } } catch (Exception e) { e.printStackTrace(); } StringBuilder result = new StringBuilder(); // If field length equals 0 then reads from offset to end of characteristic data if (formatLength == 0) { if (format.toLowerCase().equals("reg-cert-data-list")) { result = new StringBuilder("0x" + Converters.getHexValue(Arrays.copyOfRange(value, offset, value.length))); result = new StringBuilder(result.toString().replace(" ", "")); } else { result = new StringBuilder(new String(Arrays.copyOfRange(value, offset, value.length))); } offset += value.length; } else { // If format type is kind of float type then reads float value if (format.equals(TYPE_SFLOAT) || format.equals(TYPE_FLOAT) || format.equals(TYPE_FLOAT_32) || format.equals(TYPE_FLOAT_64)) { double fValue = readFloat(format, formatLength); result = new StringBuilder(String.format(Locale.US, "%.3f", fValue)); } else { for (int i = offset; i < offset + formatLength; i++) { result.append((int) (value[i] & 0xff)); } } offset += formatLength; } // bluegiga code fix, original source code did not check for sint or uint if (format.toLowerCase().startsWith("sint")) { try { result = new StringBuilder(Converters.getDecimalValueFromTwosComplement(binaryString.toString())); return result.toString(); } catch (Exception e) { e.printStackTrace(); } } else if (format.toLowerCase().startsWith("uint")) { try { // note that the (- formatLength) gets the original offset. // java uses big endian, payload is little endian byte[] bytes = Arrays.copyOfRange(value, offset - formatLength, offset); Long uintAsLong = 0L; for (int i = 0; i < formatLength; i++) { uintAsLong = uintAsLong << 8; int byteAsInt = (bytes[formatLength - 1 - i] & 0xff); uintAsLong = uintAsLong | byteAsInt; } String uintVal = formatLength < 9 ? "" + uintAsLong : (new BigInteger("0" + binaryString, 2)).toString(16); return "" + uintVal; } catch (Exception e) { e.printStackTrace(); } } return result.toString(); } // Reads float value for given format private double readFloat(String format, int formatLength) { double result = 0.0; switch (format) { case TYPE_SFLOAT: result = Common.readSfloat(value, offset, formatLength - 1); break; case TYPE_FLOAT: result = Common.readFloat(value, offset, formatLength - 1); break; case TYPE_FLOAT_32: result = Common.readFloat32(value, offset, formatLength); break; case TYPE_FLOAT_64: result = Common.readFloat64(value, offset, formatLength); break; } return result; } // Reads enum for given value private int readEnumInt(int index, int size, int val) { int result = 0; for (int i = 0; i < size; i++) { result <<= 8; result |= ((val >> (index + i)) & 0x1); } return result; } // Sets value from offset position private void setValue(int off, byte[] val) { for (int i = off; i < off + val.length; i++) { value[i] = val[i - off]; } } // Gets field offset in bytes private int getFieldOffset(Field searchField) { foundField = false; int off = 0; for (Field field : mCharact.getFields()) { off += getOffset(field, searchField); } foundField = true; return off; } // Gets field offset when field has references to other fields private int getOffset(Field field, Field searchField) { int off = 0; if (field == searchField) { foundField = true; return off; } if (!foundField && isFieldPresent(field)) { if (field.getReferenceFields().size() > 0) { for (Field subField : field.getReferenceFields()) { off += getOffset(subField, searchField); } } else { if (field.getFormat() != null) { off += Engine.getInstance().getFormat(field.getFormat()); } } } return off; } // Gets all bit fields for this characteristic private ArrayList<Field> getBitFields() { ArrayList<Field> bitFields = new ArrayList<>(); for (Field field : mCharact.getFields()) { bitFields.addAll(getBitField(field)); } return bitFields; } // Gets bit field when field has references to other fields private ArrayList<Field> getBitField(Field field) { ArrayList<Field> bitFields = new ArrayList<>(); if (field.getBitfield() != null) { bitFields.add(field); } else if (field.getReferenceFields().size() > 0) { for (Field subField : field.getReferenceFields()) { bitFields.addAll(getBitField(subField)); } } return bitFields; } /* * * --- UI SECTION */ // Builds activity UI if characteristic is not standard characteristic (from // Bluetooth specifications) private void addRawValue() { // read only fields and value display for characteristic (inline) LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View readableFieldsForInline = layoutInflater.inflate(R.layout.characteristic_value_read_only, null); hex = readableFieldsForInline.findViewById(R.id.hex_readonly); ascii = readableFieldsForInline.findViewById(R.id.ascii_readonly); decimal = readableFieldsForInline.findViewById(R.id.decimal_readonly); ImageView hexCopyIV = readableFieldsForInline.findViewById(R.id.hex_copy); ImageView asciiCopyIV = readableFieldsForInline.findViewById(R.id.ascii_copy); ImageView decimalCopyIV = readableFieldsForInline.findViewById(R.id.decimal_copy); hex.setId(EDIT_NOT_CLEAR_ID); ascii.setId(EDIT_NOT_CLEAR_ID); decimal.setId(EDIT_NOT_CLEAR_ID); hex.setKeyListener(null); ascii.setKeyListener(null); decimal.setKeyListener(null); hex.setText(Converters.getHexValue(value)); ascii.setText(Converters.getAsciiValue(value)); decimal.setText(Converters.getDecimalValue(value)); rawValueViews.add(hex); rawValueViews.add(ascii); rawValueViews.add(decimal); setCopyListener(hex, hexCopyIV); setCopyListener(ascii, asciiCopyIV); setCopyListener(decimal, decimalCopyIV); valuesLayout.addView(readableFieldsForInline); if (writeable || writeableWithoutResponse) { View writableFieldsForDialog = layoutInflater.inflate(R.layout.characteristic_value, null); hexEdit = writableFieldsForDialog.findViewById(R.id.hex_edit); asciiEdit = writableFieldsForDialog.findViewById(R.id.ascii_edit); decimalEdit = writableFieldsForDialog.findViewById(R.id.decimal_edit); ImageView hexPasteIV = writableFieldsForDialog.findViewById(R.id.hex_paste); ImageView asciiPasteIV = writableFieldsForDialog.findViewById(R.id.ascii_paste); ImageView decimalPasteIV = writableFieldsForDialog.findViewById(R.id.decimal_paste); editTexts.add(hexEdit); editTexts.add(asciiEdit); editTexts.add(decimalEdit); TextWatcher hexWatcher = getHexTextWatcher(); TextWatcher decWatcher = getDecTextWatcher(); TextWatcher asciiWatcher = getAsciiTextWatcher(); View.OnFocusChangeListener hexListener = getHexFocusChangeListener(); hexEdit.setOnFocusChangeListener(hexListener); WriteCharacteristic commiter = new WriteCharacteristic(); hexEdit.setOnEditorActionListener(commiter); asciiEdit.setOnEditorActionListener(commiter); decimalEdit.setOnEditorActionListener(commiter); hexEdit.addTextChangedListener(hexWatcher); asciiEdit.addTextChangedListener(asciiWatcher); decimalEdit.addTextChangedListener(decWatcher); setPasteListener(hexEdit, hexPasteIV, HEX_ID); setPasteListener(asciiEdit, asciiPasteIV, ASCII_ID); setPasteListener(decimalEdit, decimalPasteIV, DECIMAL_ID); updateSaveButtonState(); if (writableFieldsContainer != null) { writableFieldsContainer.removeAllViews(); writableFieldsContainer.addView(writableFieldsForDialog); } } } private void setCopyListener(final EditText copyFromET, final ImageView copyIV) { copyIV.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ClipboardManager clipboardManager = context.getSystemService(ClipboardManager.class); ClipData clip = ClipData.newPlainText("characteristic-value", copyFromET.getText().toString()); if (clipboardManager != null) { clipboardManager.setPrimaryClip(clip); Toast.makeText(context, getString(R.string.Copied_to_clipboard), Toast.LENGTH_SHORT).show(); } } }); } private void setPasteListener(final EditText pasteToET, final ImageView pasteIV, final String expectedPasteType) { pasteIV.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ClipboardManager clipboardManager = context.getSystemService(ClipboardManager.class); if (clipboardManager != null && clipboardManager.getPrimaryClip() != null) { ClipData clip = clipboardManager.getPrimaryClip(); String text = clip.getItemAt(0).getText().toString(); pasteToET.requestFocus(); switch (expectedPasteType) { case HEX_ID: text = StringUtils.getStringWithoutWhitespaces(text); if (isHexStringCorrect(text)) pasteToET.setText(text); else Toast.makeText(context, getString(R.string.Incorrect_data_format), Toast.LENGTH_SHORT).show(); break; case ASCII_ID: pasteToET.setText(text); break; case DECIMAL_ID: if (isDecimalCorrect(text)) pasteToET.setText(text); else Toast.makeText(context, getString(R.string.Incorrect_data_format), Toast.LENGTH_SHORT).show(); break; } } } }); } private boolean isHexStringCorrect(String text) { for (int i = 0; i < text.length(); i++) { if (!StringUtils.HEX_VALUES.contains(String.valueOf(text.charAt(i)))) return false; } return true; } private boolean isDecimalCorrect(String text) { String[] arr = text.split(" "); try { for (String s : arr) { int tmp = Integer.parseInt(s); if (!(0 <= tmp && tmp <= 255)) return false; } } catch (Exception e) { return false; } return true; } private void initCharacteristicWriteDialog() { editableFieldsDialog = new Dialog(getActivity()); editableFieldsDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); editableFieldsDialog.setContentView(R.layout.dialog_characteristic_write); editableFieldsDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); writableFieldsContainer = editableFieldsDialog.findViewById(R.id.characteristic_writable_fields_container); int width = (int) (getResources().getDisplayMetrics().widthPixels * 0.9); editableFieldsDialog.getWindow().setLayout(width, LinearLayout.LayoutParams.WRAP_CONTENT); initWriteModeView(editableFieldsDialog); saveValueBtn = editableFieldsDialog.findViewById(R.id.save_btn); clearBtn = editableFieldsDialog.findViewById(R.id.clear_btn); closeIV = editableFieldsDialog.findViewById(R.id.image_view_close); } private boolean isAnyWriteFieldEmpty() { for (EditText e : editTexts) { if (e.getId() == EDIT_NOT_CLEAR_ID) continue; if (e.getText().toString().isEmpty()) return true; } return false; } public void showCharacteristicWriteDialog() { // if any textfields are empty, save rounded_button_red will be initialized to be disabled updateSaveButtonState(); saveValueBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!isAnyWriteFieldEmpty()) { writeValueToCharacteristic(); } else { Toast.makeText(getActivity(), getString(R.string.You_cannot_send_empty_value_to_charac), Toast.LENGTH_SHORT).show(); } } }); clearBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { for (EditText et : editTexts) { if (et.getId() != EDIT_NOT_CLEAR_ID) { et.setText(""); } } } }); closeIV.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (editableFieldsDialog != null) { editableFieldsDialog.dismiss(); } } }); String serviceName = mService != null ? mService.getName().trim() : getString(R.string.unknown_service); serviceName = Common.checkOTAService(mGattService.getUuid().toString(), serviceName); String characteristicName; if (mCharact != null) { characteristicName = mCharact.getName().trim(); } else { characteristicName = getOtaSpecificCharacteristicName(mBluetoothCharact.getUuid().toString()); } String characteristicUuid = mCharact != null ? Common.getUuidText(mCharact.getUuid()) : Common.getUuidText(mBluetoothCharact.getUuid()); TextView serviceNameTextView = editableFieldsDialog.findViewById(R.id.picker_dialog_service_name); TextView characteristicTextView = editableFieldsDialog.findViewById(R.id.characteristic_dialog_characteristic_name); TextView uuidTextView = editableFieldsDialog.findViewById(R.id.picker_dialog_characteristic_uuid); serviceNameTextView.setText(serviceName); characteristicTextView.setText(characteristicName); uuidTextView.setText(characteristicUuid); LinearLayout propertiesContainer = editableFieldsDialog.findViewById(R.id.picker_dialog_properties_container); initPropertiesForEditableFieldsDialog(propertiesContainer); //Clear EditText fields for (EditText et : editTexts) { if (et.getId() != EDIT_NOT_CLEAR_ID) { et.setText(""); } } editableFieldsDialog.show(); updateSaveButtonState(); } private void initPropertiesForEditableFieldsDialog(LinearLayout propertiesContainer) { propertiesContainer.removeAllViews(); String propertiesString = Common.getProperties(getActivity(), mBluetoothCharact.getProperties()); String[] propsExploded = propertiesString.split(","); for (String propertyValue : propsExploded) { TextView propertyView = new TextView(context); String propertyValueTrimmed = propertyValue.trim(); // length 13 is used to cut off property string at length "Write no resp" propertyValueTrimmed = propertyValue.length() > 13 ? propertyValue.substring(0, 13) : propertyValueTrimmed; propertyValueTrimmed.toUpperCase(); propertyView.setText(propertyValueTrimmed); propertyView.append(" "); propertyView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_property_text_size)); propertyView.setTextColor(ContextCompat.getColor(context, R.color.silabs_blue)); propertyView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); LinearLayout propertyContainer = new LinearLayout(context); propertyContainer.setOrientation(LinearLayout.HORIZONTAL); ImageView propertyIcon = new ImageView(context); int iconId; if (propertyValue.trim().toUpperCase().equals("BROADCAST")) { iconId = R.drawable.debug_prop_broadcast; } else if (propertyValue.trim().toUpperCase().equals("READ")) { iconId = R.drawable.ic_icon_read_on; } else if (propertyValue.trim().toUpperCase().equals("WRITE NO RESPONSE")) { iconId = R.drawable.debug_prop_write_no_resp; } else if (propertyValue.trim().toUpperCase().equals("WRITE")) { iconId = R.drawable.ic_icon_edit_on; } else if (propertyValue.trim().toUpperCase().equals("NOTIFY")) { iconId = R.drawable.ic_icon_notify_on; } else if (propertyValue.trim().toUpperCase().equals("INDICATE")) { iconId = R.drawable.ic_icon_indicate_on; } else if (propertyValue.trim().toUpperCase().equals("SIGNED WRITE")) { iconId = R.drawable.debug_prop_signed_write; } else if (propertyValue.trim().toUpperCase().equals("EXTENDED PROPS")) { iconId = R.drawable.debug_prop_ext; } else { iconId = R.drawable.debug_prop_ext; } propertyIcon.setBackgroundResource(iconId); LinearLayout.LayoutParams paramsText = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); paramsText.gravity = Gravity.CENTER_VERTICAL | Gravity.LEFT; LinearLayout.LayoutParams paramsIcon = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); paramsIcon.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; if (propertyValue.trim().toUpperCase().equals("WRITE NO RESPONSE")) { float d = getResources().getDisplayMetrics().density; paramsIcon = new LinearLayout.LayoutParams((int) (24 * d), ((int) (24 * d))); paramsIcon.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; } propertyContainer.addView(propertyView, paramsText); propertyContainer.addView(propertyIcon, paramsIcon); LinearLayout.LayoutParams paramsTextAndIconContainer = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); paramsTextAndIconContainer.gravity = Gravity.RIGHT; propertiesContainer.addView(propertyContainer, paramsTextAndIconContainer); } } // Gets text watcher for hex edit view private TextWatcher getHexTextWatcher() { return new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (hexEdit.hasFocus()) { int textLength = hexEdit.getText().toString().length(); byte[] newValue; if (textLength % 2 == 1) { String temp = hexEdit.getText().toString(); temp = temp.substring(0, textLength - 1) + "0" + temp.charAt(textLength - 1); newValue = hexToByteArray(temp.replaceAll("\\s+", "")); } else { newValue = hexToByteArray(hexEdit.getText().toString().replaceAll("\\s+", "")); } asciiEdit.setText(Converters.getAsciiValue(newValue)); decimalEdit.setText(Converters.getDecimalValue(newValue)); } updateSaveButtonState(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { updateSaveButtonState(); } @Override public void afterTextChanged(Editable s) { updateSaveButtonState(); } }; } // Gets text watcher for decimal edit view private TextWatcher getDecTextWatcher() { return new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (decimalEdit.hasFocus()) { if (isDecValueValid(decimalEdit.getText().toString())) { byte[] newValue = decToByteArray(decimalEdit.getText().toString()); hexEdit.setText(Converters.getHexValue(newValue)); asciiEdit.setText(Converters.getAsciiValue(newValue)); } else { decimalEdit.setText(decimalEdit.getText().toString().substring(0, decimalEdit.getText().length() - 1)); decimalEdit.setSelection(decimalEdit.getText().length()); Toast.makeText(context, R.string.invalid_dec_value, Toast.LENGTH_SHORT) .show(); } } updateSaveButtonState(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { updateSaveButtonState(); } @Override public void afterTextChanged(Editable s) { updateSaveButtonState(); } }; } // Gets text watcher for ascii edit view private TextWatcher getAsciiTextWatcher() { TextWatcher watcher = new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (asciiEdit.hasFocus()) { byte[] newValue = asciiEdit.getText().toString().getBytes(); hexEdit.setText(Converters.getHexValue(newValue)); decimalEdit.setText(Converters.getDecimalValue(newValue)); } updateSaveButtonState(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { updateSaveButtonState(); } @Override public void afterTextChanged(Editable s) { updateSaveButtonState(); } }; return watcher; } // Gets focus listener for hex edit view private View.OnFocusChangeListener getHexFocusChangeListener() { View.OnFocusChangeListener listener = new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { hexEdit.setText(hexEdit.getText().toString().replaceAll("\\s+", "")); } else { int textLength = hexEdit.getText().toString().length(); String hexValue; if (textLength % 2 == 1) { String temp = hexEdit.getText().toString(); hexValue = temp.substring(0, textLength - 1) + "0" + temp.charAt(textLength - 1); } else { hexValue = hexEdit.getText().toString(); } byte[] value = hexToByteArray(hexValue); hexEdit.setText(Converters.getHexValue(value)); } updateSaveButtonState(); } }; return listener; } // Adds views related to single field value private void addValue(final Field field) { LinearLayout parentLayout = new LinearLayout(context); LinearLayout.LayoutParams parentParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); parentLayout.setOrientation(LinearLayout.VERTICAL); parentParams.setMargins(0, defaultMargin, 0, defaultMargin / 2); parentLayout.setLayoutParams(parentParams); LinearLayout valueLayout = addValueLayout(); TextView fieldNameView = addValueFieldName(field.getName(), valueLayout.getId()); fieldNameView.setGravity(Gravity.CENTER_VERTICAL); fieldNameView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_label_text_size)); TextView fieldUnitView = addValueUnit(field); fieldUnitView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_label_text_size)); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); layoutParams.gravity = Gravity.BOTTOM; fieldUnitView.setLayoutParams(layoutParams); hidableViews.add(fieldUnitView); if (!parseProblem && field.getReference() == null) { String format = field.getFormat(); String val = readNextValue(format); if (!(format.toLowerCase().equals("utf8s") || format.toLowerCase().equals("utf16s"))) { int decimalExponentAbs = (int) abs(field.getDecimalExponent()); double divider = Math.pow(10, decimalExponentAbs); double valDouble = Double.parseDouble(val); double valTmp = valDouble / divider; val = Double.toString(valTmp); } else { writeString = true; val = val.replace("\0", ""); } if (writeable || writeableWithoutResponse) { // inline field value EditText fieldValueEdit = addValueEdit(field, val); fieldValueEdit.setGravity(Gravity.CENTER_VERTICAL); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.5f); params.setMargins(8, 0, 0, 0); params.gravity = Gravity.CENTER_VERTICAL; fieldValueEdit.setLayoutParams(params); fieldValueEdit.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); editTexts.add(fieldValueEdit); TextView fieldValue = (TextView) addFieldName(fieldValueEdit.getText().toString()); fieldValue.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); hidableViews.add(fieldValue); fieldValue.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_primary_text)); valueLayout.addView(fieldValue); // dialog field value // field name View fieldName = addFieldName(field.getName()); params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.5f); params.setMargins(0, 0, 8, 0); params.gravity = Gravity.CENTER_VERTICAL; fieldName.setLayoutParams(params); // container for editable field value and field name LinearLayout fieldContainer = new LinearLayout(context); fieldContainer.setOrientation(LinearLayout.HORIZONTAL); params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.setMargins(0, 5, 0, 5); fieldContainer.setLayoutParams(params); fieldContainer.addView(fieldName); fieldContainer.addView(fieldValueEdit); fieldContainer.setPadding(0, FIELD_CONTAINER_PADDING_TOP, 0, FIELD_CONTAINER_PADDING_BOTTOM); writableFieldsContainer.addView(fieldContainer); } else { TextView fieldValueView = addValueText(val); fieldValueView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); hidableViews.add(fieldValueView); valueLayout.addView(fieldValueView); } updateSaveButtonState(); } valueLayout.addView(fieldUnitView); parentLayout.addView(valueLayout); parentLayout.addView(fieldNameView); valuesLayout.addView(parentLayout); } // Adds parent layout for normal value private LinearLayout addValueLayout() { LinearLayout valueLayout = new LinearLayout(context); valueLayout.setBackgroundColor(viewBackgroundColor); RelativeLayout.LayoutParams valueLayoutParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); valueLayoutParams.setMargins(0, 0, defaultMargin, 0); valueLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); valueLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL); valueLayout.setLayoutParams(valueLayoutParams); valueLayout.setOrientation(LinearLayout.HORIZONTAL); //noinspection ResourceType valueLayout.setId(2); return valueLayout; } // Adds unit text view private TextView addValueUnit(Field field) { TextView fieldUnitView = new TextView(context); fieldUnitView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); fieldUnitView.setBackgroundColor(viewBackgroundColor); LinearLayout.LayoutParams fieldUnitParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); fieldUnitParams.setMargins(0, 0, 0, 0); fieldUnitView.setLayoutParams(fieldUnitParams); Unit unit = Engine.getInstance().getUnit(field.getUnit()); if (unit != null) { if (!TextUtils.isEmpty(unit.getSymbol())) { if (unit.getFullName().toLowerCase().equals("celsius") || unit.getFullName().toLowerCase().equals("fahrenheit")) { // this makes sure that the degrees symbol for temperature is displayed correctly fieldUnitView.setText(Html.fromHtml(unit.getSymbol())); } else { fieldUnitView.setText(unit.getSymbol()); } } else { fieldUnitView.setText(unit.getFullName()); } } fieldUnitView.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_primary_text)); return fieldUnitView; } private boolean isNumberFormat(String format) { switch (format) { case "uint8": case "uint16": case "uint24": case "uint32": case "uint40": case "uint48": case "sint8": case "sint16": case "sint24": case "sint32": case "sint40": case "sint48": case "float32": case "float64": return true; case "utf8s": case "utf16s": default: return false; } } // Adds value edit view private EditText addValueEdit(final Field field, String value) { final EditText fieldValueEdit = new EditText(context); fieldValueEdit.setBackgroundResource(R.drawable.edittext_custom_color); fieldValueEdit.setPadding(FIELD_VALUE_EDIT_TEXT_PADDING_LEFT, FIELD_VALUE_EDIT_TEXT_PADDING_TOP, FIELD_VALUE_EDIT_TEXT_PADDING_RIGHT, FIELD_VALUE_EDIT_TEXT_PADDING_BOTTOM); LinearLayout.LayoutParams fieldValueParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); fieldValueParams.gravity = Gravity.CENTER_VERTICAL; fieldValueParams.leftMargin = FIELD_VALUE_EDIT_LEFT_MARGIN; fieldValueEdit.setLayoutParams(fieldValueParams); fieldValueEdit.setTextColor(context.getColor(R.color.silabs_primary_text)); fieldValueEdit.setSingleLine(); fieldValueEdit.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); fieldValueEdit.setText(value); int formatLength = Engine.getInstance().getFormat(field.getFormat()); // Bluegiga code had a bug where formatlength = 0 fields were ignored on write if (formatLength == 0) { formatLength = value.length(); } final byte[] valArr = new byte[formatLength]; if (isNumberFormat(field.getFormat())) { fieldValueEdit.setRawInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); } fieldValueEdit.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (writeString) { byte[] array = fieldValueEdit.getText().toString().getBytes(); fillValue(array); } else { Arrays.fill(valArr, (byte) 0); String inputVal = fieldValueEdit.getText().toString(); int decimalExponentAbs = (int) abs(field.getDecimalExponent()); StringBuilder inputValMoved = new StringBuilder(inputVal); if (decimalExponentAbs != 0) { int index = inputValMoved.indexOf("."); if (index != -1) { if (inputValMoved.length() - 1 - index > decimalExponentAbs) { inputValMoved = new StringBuilder(inputValMoved.toString().replace(".", "")); inputValMoved = new StringBuilder(inputValMoved.substring(0, index + decimalExponentAbs) + "." + inputValMoved.substring(index + decimalExponentAbs)); } else if (inputValMoved.length() - 1 - index == decimalExponentAbs) { inputValMoved = new StringBuilder(inputValMoved.toString().replace(".", "")); } else { inputValMoved = new StringBuilder(inputValMoved.toString().replace(".", "")); for (int i = inputValMoved.length() - index; i < decimalExponentAbs; i++) { inputValMoved.append("0"); } } } else { for (int i = 0; i < decimalExponentAbs; i++) { inputValMoved.append("0"); } } } Pair<byte[], Boolean> pair = Converters.convertStringTo(inputValMoved.toString(), field.getFormat()); byte[] newVal = pair.first; Boolean inRange = pair.second; Log.d("write_val", "Value to write from edittext conversion (hex): " + Converters.getHexValue(newVal)); for (int i = 0; i < valArr.length; i++) { if (i < newVal.length) { valArr[i] = newVal[i]; } } int off = getFieldOffset(field); fieldsInRangeMap.put(field, inRange); if (isNumberFormat(field.getFormat())) { fieldsValidMap.put(field, isNumeric(inputVal)); } else { fieldsValidMap.put(field, true); } setValue(off, valArr); } updateSaveButtonState(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { updateSaveButtonState(); } @Override public void afterTextChanged(Editable s) { updateSaveButtonState(); } }); return fieldValueEdit; } private void fillValue(byte[] array) { value = new byte[array.length]; System.arraycopy(array, 0, value, 0, array.length); } private void updateSaveButtonState() { /* boolean emptyFieldExists = true; for (EditText editableField : editTexts) { emptyFieldExists = !editableField.getText().toString().equals(""); Log.d("editableField", " Empty: " + editableField.getText().toString().equals("")); } */ if (saveValueBtn != null) { saveValueBtn.setEnabled(true); saveValueBtn.setClickable(true); } } // Adds value text view private TextView addValueText(String value) { TextView fieldValueView = new TextView(context); fieldValueView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); fieldValueView.setBackgroundColor(viewBackgroundColor); LinearLayout.LayoutParams fieldValueParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); fieldValueView.setLayoutParams(fieldValueParams); fieldValueView.setText(value); fieldValueView.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_primary_text)); return fieldValueView; } // Adds TextView with field name private TextView addValueFieldName(String name, int leftViewId) { TextView fieldNameView = new TextView(context); fieldNameView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_label_text_size)); fieldNameView.setBackgroundColor(viewBackgroundColor); fieldNameView.setText(name); RelativeLayout.LayoutParams fieldNameParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); fieldNameParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); fieldNameParams.addRule(RelativeLayout.LEFT_OF, leftViewId); fieldNameParams.addRule(RelativeLayout.CENTER_VERTICAL); fieldNameParams.setMargins(0, 0, 0, 15); fieldNameView.setLayoutParams(fieldNameParams); fieldNameView.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_subtle_text)); return fieldNameView; } private TextView getBitNameView(String name, LinearLayout bitsLayout) { TextView bitNameView = new TextView(context); bitNameView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_label_text_size)); bitNameView.setBackgroundColor(viewBackgroundColor); RelativeLayout.LayoutParams bitNameParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); bitNameParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); bitNameParams.addRule(RelativeLayout.CENTER_VERTICAL); bitNameParams.addRule(RelativeLayout.LEFT_OF, bitsLayout.getId()); bitNameParams.setMargins(0, 0, 0, 0); bitNameView.setLayoutParams(bitNameParams); bitNameView.setText(name); bitNameView.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_primary_text)); return bitNameView; } private TextView getCheckboxListHeader(String name) { TextView checkboxListHeader = (TextView) addFieldName(name); checkboxListHeader.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); checkboxListHeader.setTextColor(Color.BLACK); return checkboxListHeader; } private LinearLayout getBitsLayout() { LinearLayout bitsLayout = new LinearLayout(context); bitsLayout.setBackgroundColor(viewBackgroundColor); //noinspection ResourceType bitsLayout.setId(1); RelativeLayout.LayoutParams bitsLayoutParams = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); bitsLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); bitsLayout.setLayoutParams(bitsLayoutParams); bitsLayout.setOrientation(LinearLayout.HORIZONTAL); return bitsLayout; } private void setStringBuilderBitsInRange(StringBuilder builder, int startBit, int endBit, int value) { while (startBit < endBit) { char bitValue = ((value & 1) == 1) ? '1' : '0'; builder.setCharAt(endBit - 1, bitValue); value >>= 1; endBit--; } } // Get value from bitsString in range start (inclusive) -> end (exclusive) // bits String order must be: // least significant (index 0), to most significant (index last) private int getValueInStringBitsRange(int start, int end, String bits) { int result = 0; while (start < end) { if (bits.charAt(start) == '1') { result |= 1; } else { result |= 0; } if (start + 1 < end) { result <<= 1; } start++; } return result; } // Get value bits as String from offset to offest+formatLength // bits are parsed in order: LSO(least significant octet) ===> MSO (most significant octet) // String charAt(0) - least significant bit, // String charAt(last) most significant bit. private String getFieldValueAs_LSO_MSO_BitsString(int offset, int formatLength) { StringBuilder result = new StringBuilder(); for (int i = offset; i < offset + formatLength; i++) { byte val = value[i]; for (int j = 0; j < 8; j++) { if ((val & 0b0000_0001) == 0b0000_0001) { result.append(1); } else { result.append(0); } val >>= 1; } } return result.toString(); } private StringBuilder fillStringBuilderWithZeros(int count) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < count; i++) { builder.append('0'); } return builder; } // Convert String of bits where bitsString charAt(0) is least significant // to byte array private byte[] bitsStringToByteArray(String bitsString, int length) { byte arr[] = new byte[length]; for (int i = 0; i < length; i++) { int tmp = 0; for (int j = 8 * (i + 1) - 1; j >= i * 8; j--) { char bitChar = bitsString.charAt(j); if (bitChar == '1') { tmp |= 1; } else { tmp |= 0; } if (j > i * 8) { tmp <<= 1; } } arr[i] = (byte) tmp; } return arr; } // Adds views related to bitfield value private void addBitfield(Field field) { if (field.getReference() == null) { final int formatLength = Engine.getInstance().getFormat(field.getFormat()); final int bitsLength = formatLength * 8; int currentBit = 0; String valueBits = getFieldValueAs_LSO_MSO_BitsString(offset, formatLength); final StringBuilder builder = fillStringBuilderWithZeros(bitsLength); // Display read bitfields for (Bit bit : field.getBitfield().getBits()) { final ArrayList<String> enumerations = new ArrayList<>(); // Bits in range startBitIndex to endBitIndex will be replaced with new value for given bitField final int startBitIndex = currentBit; final int endBitIndex = currentBit + bit.getSize(); for (Enumeration enumeration : bit.getEnumerations()) { enumerations.add(enumeration.getValue()); } LinearLayout.LayoutParams nameAndValueParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); nameAndValueParams.setMargins(0, defaultMargin, 0, 12 + defaultMargin / 2); LinearLayout nameAndValueContainer = new LinearLayout(context); nameAndValueContainer.setOrientation(LinearLayout.VERTICAL); nameAndValueContainer.setLayoutParams(nameAndValueParams); View valueText = addValueText(enumerations.get(getValueInStringBitsRange(startBitIndex, endBitIndex, valueBits))); View nameText = addFieldName(bit.getName()); nameAndValueContainer.addView(valueText); nameAndValueContainer.addView(nameText); valuesLayout.addView(nameAndValueContainer); hidableViews.add(valueText); if (writeable || writeableWithoutResponse) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.5f); params.gravity = Gravity.CENTER_VERTICAL; params.setMargins(8, 0, 0, 0); final Spinner spinner = new Spinner(context); ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(context, R.layout.enumeration_spinner_dropdown_item, enumerations); spinner.setAdapter(spinnerArrayAdapter); spinner.setLayoutParams(params); final int off = offset; spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { // After each spinner selection bits are prepared for characteristic write - value array is updated with selected value setStringBuilderBitsInRange(builder, startBitIndex, endBitIndex, position); byte[] val = bitsStringToByteArray(builder.toString(), formatLength); //intToByteArray(Integer.parseInt(builder.toString(), 2), formatLength); setValue(off, val); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); View fieldName = addFieldName(bit.getName()); params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.5f); params.gravity = Gravity.CENTER_VERTICAL; params.setMargins(0, 0, 8, 0); fieldName.setLayoutParams(params); LinearLayout linearLayout = new LinearLayout(context); linearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); linearLayout.setOrientation(LinearLayout.HORIZONTAL); linearLayout.addView(fieldName); linearLayout.addView(spinner); writableFieldsContainer.addView(linearLayout); } currentBit = currentBit + bit.getSize(); } offset += formatLength; } } private void initWriteModeView(Dialog dialog) { final RadioButton writeWithResponseRB = dialog.findViewById(R.id.write_with_resp_radio_button); final RadioButton writeWithoutResponseRB = dialog.findViewById(R.id.write_without_resp_radio_button); final LinearLayout writeMethodLL = dialog.findViewById(R.id.write_method_linear_layout); if (writeable) { writeWithResponseRB.setChecked(true); writeWithResponseRB.setChecked(true); writeWithResponse = true; writeWithResponseRB.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { writeWithResponse = true; } }); } else { writeWithResponseRB.setEnabled(false); writeWithResponseRB.setChecked(false); } if (writeableWithoutResponse) { writeWithoutResponseRB.setEnabled(true); if (!writeWithResponseRB.isChecked()) { writeWithoutResponseRB.setChecked(true); writeWithResponse = false; } writeWithoutResponseRB.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { writeWithResponse = false; } }); } else { writeWithoutResponseRB.setEnabled(false); writeWithoutResponseRB.setChecked(false); } if (!writeableWithoutResponse && !writeable) { writeMethodLL.setVisibility(View.GONE); } } // Adds views related to enumeration value // Each enumeration is presented as Spinner view private void addEnumeration(final Field field) { if (field.getReference() == null) { final ArrayList<String> enumerationArray = new ArrayList<>(); for (Enumeration en : field.getEnumerations()) { enumerationArray.add(en.getValue()); } if (!parseProblem) { int formatLength = Engine.getInstance().getFormat(field.getFormat()); int pos = 0; int val = 0; if (field.getFormat().toLowerCase().equals("16bit")) { if (offset == value.length - 1) { // case for when only 8 bits of 16 are sent val = value[offset] & 0xff; val = val << 8; } else if (offset < value.length - 1) { // for field "Category, last 6 bits of payload are used for sub categories if (field.getName().equals("Category")) { int byte1 = value[offset] & 0xff; int byte2 = value[offset + 1] & 0xff; val = (byte2 << 8) | byte1; val = 0xffc0 & val; } else { // case for when 16 full bits are sent val = value[offset] & 0xff; val = val << 8; val = val | (value[offset + 1] & 0xff); } } } else { val = readInt(offset, formatLength); } // Bluegiga code was using getFieldOffset() and getting wrong offset // this ensures that fields are consistently offset while reading characteristic offset += formatLength; if (val != 0) { // value was read or notified for (Enumeration en : field.getEnumerations()) { if (en.getKey() == val) { break; } pos++; } } if (pos >= enumerationArray.size()) { pos = 0; } LinearLayout.LayoutParams nameAndValueParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); nameAndValueParams.setMargins(0, defaultMargin, 0, 12 + defaultMargin / 2); LinearLayout nameAndValueContainer = new LinearLayout(context); nameAndValueContainer.setOrientation(LinearLayout.VERTICAL); nameAndValueContainer.setLayoutParams(nameAndValueParams); View valueText = addValueText(enumerationArray.get(pos)); hidableViews.add(valueText); View nameText = addFieldName(field.getName()); nameAndValueContainer.addView(valueText); nameAndValueContainer.addView(nameText); valuesLayout.addView(nameAndValueContainer); if (writeable || writeableWithoutResponse) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.5f); params.gravity = Gravity.CENTER_VERTICAL; final int offset = getFieldOffset(field); final Spinner spinner = new Spinner(context); ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(context, R.layout.enumeration_spinner_dropdown_item, enumerationArray); spinner.setAdapter(spinnerArrayAdapter); spinner.setLayoutParams(params); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { int key = field.getEnumerations().get(position).getKey(); int formatLength = Engine.getInstance().getFormat(field.getFormat()); byte[] val = intToByteArray(key, formatLength); setValue(offset, val); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); View fieldName = addFieldName(field.getName()); fieldName.setLayoutParams(params); LinearLayout linearLayout = new LinearLayout(context); linearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); linearLayout.setOrientation(LinearLayout.HORIZONTAL); linearLayout.addView(fieldName); linearLayout.addView(spinner); writableFieldsContainer.addView(linearLayout); } } } } // Adds TextView with error info // Called when characteristic parsing error occured private void addProblemInfoView() { TextView problemTextView = new TextView(context); problemTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_text_size)); problemTextView.setBackgroundColor(viewBackgroundColor); LinearLayout.LayoutParams fieldValueParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); fieldValueParams.setMargins(0, 0, 0, 0); problemTextView.setLayoutParams(fieldValueParams); problemTextView.setTypeface(Typeface.DEFAULT_BOLD); //problemTextView.setText(getText(R.string.parse_problem)); problemTextView.setText(parsingProblemInfo); valuesLayout.addView(problemTextView); problemTextView.setTextColor(Color.RED); } // Adds TextView with field name private View addFieldName(String name) { TextView fieldNameView = new TextView(context); fieldNameView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.characteristic_list_item_value_label_text_size)); fieldNameView.setBackgroundColor(viewBackgroundColor); LinearLayout.LayoutParams fieldNameParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); fieldNameParams.gravity = Gravity.CENTER_VERTICAL; fieldNameParams.setMargins(0, 0, 0, 0); fieldNameView.setLayoutParams(fieldNameParams); fieldNameView.setText(name); fieldNameView.setTextColor(ContextCompat.getColor(getActivity(), R.color.silabs_subtle_text)); return fieldNameView; } // Adds horizontal line to separate UI sections private View addHorizontalLine(int height) { View horizontalLine = new View(context); LinearLayout.LayoutParams lineParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, height); lineParams.setMargins(0, convertPxToDp(5), 0, 0); horizontalLine.setLayoutParams(lineParams); horizontalLine.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.silabs_divider)); return horizontalLine; } // Converts pixels to 'dp' unit private int convertPxToDp(int sizeInPx) { float scale = getResources().getDisplayMetrics().density; return (int) (sizeInPx * scale + 0.5f); } class WriteCharacteristic implements TextView.OnEditorActionListener { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { writeValueToCharacteristic(); return true; } return false; } } private boolean isNumeric(String string) { try { Double.parseDouble(string); return true; } catch (NumberFormatException e) { return false; } } private String getOtaSpecificCharacteristicName(String uuid) { uuid = uuid.toUpperCase(); switch (uuid) { case "F7BF3564-FB6D-4E53-88A4-5E37E0326063": return "OTA Control Attribute"; case "984227F3-34FC-4045-A5D0-2C581F81A153": return "OTA Data Attribute"; case "4F4A2368-8CCA-451E-BFFF-CF0E2EE23E9F": return "AppLoader version"; case "4CC07BCF-0868-4B32-9DAD-BA4CC41E5316": return "OTA version"; case "25F05C0A-E917-46E9-B2A5-AA2BE1245AFE": return "Gecko Bootloader version"; case "0D77CC11-4AC1-49F2-BFA9-CD96AC7A92F8": return "Application version"; default: return getString(R.string.unknown_characteristic_label); } } }