package com.adafruit.bluefruit.le.connect.app; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ImageButton; import android.widget.TextView; import com.adafruit.bluefruit.le.connect.R; import com.adafruit.bluefruit.le.connect.app.settings.ConnectedSettingsActivity; import com.adafruit.bluefruit.le.connect.ble.BleManager; import com.adafruit.bluefruit.le.connect.ble.BleUtils; import com.adafruit.bluefruit.le.connect.ble.KnownUUIDs; import com.adafruit.bluefruit.le.connect.ui.utils.ExpandableHeightListView; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public class InfoActivity extends AppCompatActivity implements BleManager.BleManagerListener { // Log private final static String TAG = InfoActivity.class.getSimpleName(); // Activity request codes (used for onActivityResult) private static final int kActivityRequestCode_ConnectedSettingsActivity = 0; // Constants private final static int kDataFormatCount = 2; // UI private ExpandableListView mInfoListView; private ExpandableListAdapter mInfoListAdapter; private View mWaitView; // Data private BleManager mBleManager; private List<ElementPath> mServicesList; // List with service names private Map<String, List<ElementPath>> mCharacteristicsMap; // Map with characteristics for service keys private Map<String, List<ElementPath>> mDescriptorsMap; // Map with descriptors for characteristic keys private Map<String, byte[]> mValuesMap; // Map with values for characteristic and descriptor keys¡ private DataFragment mRetainedDataFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_info); mBleManager = BleManager.getInstance(this); // Init variables restoreRetainedDataFragment(); // UI mWaitView = findViewById(R.id.waitView); mInfoListView = (ExpandableListView) findViewById(R.id.infoListView); mInfoListAdapter = new ExpandableListAdapter(this, mServicesList, mCharacteristicsMap, mDescriptorsMap, mValuesMap); mInfoListView.setAdapter(mInfoListAdapter); BluetoothDevice device = mBleManager.getConnectedDevice(); if (device != null) { TextView nameTextView = (TextView) findViewById(R.id.nameTextView); boolean isNameDefined = device.getName() != null; nameTextView.setText(device.getName()); nameTextView.setVisibility(isNameDefined ? View.VISIBLE : View.GONE); TextView addressTextView = (TextView) findViewById(R.id.addressTextView); final String address = String.format("%s: %s", getString(R.string.scan_device_address), device.getAddress()); addressTextView.setText(address); onServicesDiscovered(); } else { finish(); // Device disconnected for unknown reason } } @Override public void onResume() { super.onResume(); // Setup listeners mBleManager.setBleListener(this); } @Override public void onDestroy() { // Retain data saveRetainedDataFragment(); super.onDestroy(); } // region Menu @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_info, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_help) { startHelp(); return true; } else if (id == R.id.action_connected_settings) { startConnectedSettings(); return true; } else if (id == R.id.action_refreshcache) { if (mBleManager != null) { mBleManager.refreshDeviceCache(); } } return super.onOptionsItemSelected(item); } private void startHelp() { // Launch app help activity Intent intent = new Intent(this, CommonHelpActivity.class); intent.putExtra("title", getString(R.string.info_help_title)); intent.putExtra("help", "info_help.html"); startActivity(intent); } private void startConnectedSettings() { // Launch connected settings activity Intent intent = new Intent(this, ConnectedSettingsActivity.class); startActivityForResult(intent, kActivityRequestCode_ConnectedSettingsActivity); } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { if (resultCode == RESULT_OK && requestCode == kActivityRequestCode_ConnectedSettingsActivity) { finish(); } } // endregion public void onClickInfoService(View view) { int groupPosition = (Integer) view.getTag(); if (mInfoListView.isGroupExpanded(groupPosition)) { mInfoListView.collapseGroup(groupPosition); } else { // Expand this, Collapse the rest int len = mInfoListAdapter.getGroupCount(); for (int i = 0; i < len; i++) { if (i != groupPosition) { mInfoListView.collapseGroup(i); } } mInfoListView.expandGroup(groupPosition, true); } } @Override public void onConnected() { } @Override public void onConnecting() { } @Override public void onDisconnected() { Log.d(TAG, "Disconnected. Back to previous activity"); setResult(-1); // Unexpected Disconnect finish(); } @Override public void onServicesDiscovered() { // Remove old data mServicesList.clear(); mCharacteristicsMap.clear(); mDescriptorsMap.clear(); mValuesMap.clear(); // Services List<BluetoothGattService> services = mBleManager.getSupportedGattServices(); for (BluetoothGattService service : services) { String serviceUuid = service.getUuid().toString(); int instanceId = service.getInstanceId(); String serviceName = KnownUUIDs.getServiceName(serviceUuid); String finalServiceName = serviceName != null ? serviceName : serviceUuid; ElementPath serviceElementPath = new ElementPath(serviceUuid, instanceId, null, null, finalServiceName, serviceUuid); mServicesList.add(serviceElementPath); // Characteristics List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics(); List<ElementPath> characteristicNamesList = new ArrayList<>(characteristics.size()); for (BluetoothGattCharacteristic characteristic : characteristics) { String characteristicUuid = characteristic.getUuid().toString(); String characteristicName = KnownUUIDs.getCharacteristicName(characteristicUuid); String finalCharacteristicName = characteristicName != null ? characteristicName : characteristicUuid; ElementPath characteristicElementPath = new ElementPath(serviceUuid, instanceId, characteristicUuid, null, finalCharacteristicName, characteristicUuid); characteristicNamesList.add(characteristicElementPath); // Read characteristic if (mBleManager.isCharacteristicReadable(service, characteristicUuid)) { mBleManager.readCharacteristic(service, characteristicUuid); } // Descriptors List<BluetoothGattDescriptor> descriptors = characteristic.getDescriptors(); List<ElementPath> descriptorNamesList = new ArrayList<>(descriptors.size()); for (BluetoothGattDescriptor descriptor : descriptors) { String descriptorUuid = descriptor.getUuid().toString(); String descriptorName = KnownUUIDs.getDescriptorName(descriptorUuid); String finalDescriptorName = descriptorName != null ? descriptorName : descriptorUuid; descriptorNamesList.add(new ElementPath(serviceUuid, instanceId, characteristicUuid, descriptorUuid, finalDescriptorName, descriptorUuid)); // Read descriptor // if (mBleManager.isDescriptorReadable(service, characteristicUuid, descriptorUuid)) { mBleManager.readDescriptor(service, characteristicUuid, descriptorUuid); // } } mDescriptorsMap.put(characteristicElementPath.getKey(), descriptorNamesList); } mCharacteristicsMap.put(serviceElementPath.getKey(), characteristicNamesList); } runOnUiThread(new Runnable() { @Override public void run() { updateUI(); } }); } @Override public void onDataAvailable(BluetoothGattCharacteristic characteristic) { BluetoothGattService service = characteristic.getService(); String key = new ElementPath(service.getUuid().toString(), service.getInstanceId(), characteristic.getUuid().toString(), null, null, null).getKey(); byte[] data = characteristic.getValue(); mValuesMap.put(key, data); // Update UI runOnUiThread(new Runnable() { @Override public void run() { updateUI(); } }); } @Override public void onDataAvailable(BluetoothGattDescriptor descriptor) { BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic(); BluetoothGattService service = characteristic.getService(); final String key = new ElementPath(service.getUuid().toString(), service.getInstanceId(), characteristic.getUuid().toString(), descriptor.getUuid().toString(), null, null).getKey(); final byte[] value = descriptor.getValue(); mValuesMap.put(key, value); // Update UI runOnUiThread(new Runnable() { @Override public void run() { updateUI(); } }); } @Override public void onReadRemoteRssi(int rssi) { } private void updateUI() { mInfoListAdapter.notifyDataSetChanged(); // Show progress view if data is not ready yet final boolean isDataEmpty = mInfoListView.getChildCount() == 0; mWaitView.setVisibility(isDataEmpty ? View.VISIBLE : View.GONE); } // region adapters private class ElementPath { public String serviceUUID; public int serviceInstance; public String characteristicUUID; public String descriptorUUID; public String name; public String uuid; boolean isShowingName = true; int dataFormat = kDataFormat_Auto; // will try to display as string, if is not visible then display it as hex ElementPath(String serviceUUID, int serviceInstance, String characteristicUUID, String descriptorUUID, String name, String uuid) { this.serviceUUID = serviceUUID; this.serviceInstance = serviceInstance; this.characteristicUUID = characteristicUUID; this.descriptorUUID = descriptorUUID; this.name = name; this.uuid = uuid; } String getKey() { return serviceUUID + serviceInstance + characteristicUUID + descriptorUUID; } @Override public String toString() { return name; } } private class ExpandableListAdapter extends BaseExpandableListAdapter { private Activity mActivity; private List<ElementPath> mServices; private Map<String, List<ElementPath>> mCharacteristics; private Map<String, List<ElementPath>> mDescriptors; private Map<String, byte[]> mValuesMap; ExpandableListAdapter(Activity activity, List<ElementPath> services, Map<String, List<ElementPath>> characteristics, Map<String, List<ElementPath>> descriptors, Map<String, byte[]> valuesMap) { mActivity = activity; mServices = services; mCharacteristics = characteristics; mDescriptors = descriptors; mValuesMap = valuesMap; } @Override public int getGroupCount() { return mServices != null ? mServices.size() : 0; } @Override public int getChildrenCount(int groupPosition) { List<ElementPath> items = mCharacteristics.get(mServices.get(groupPosition).getKey()); int count = 0; if (items != null) { count = items.size(); } return count; } @Override public Object getGroup(int groupPosition) { return mServices.get(groupPosition); } @Override public Object getChild(int groupPosition, int childPosition) { return mCharacteristics.get(mServices.get(groupPosition).getKey()).get(childPosition); } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public boolean hasStableIds() { return true; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mActivity.getLayoutInflater().inflate(R.layout.layout_info_item_service, parent, false); } // Tag convertView.setTag(groupPosition); // UI TextView item = (TextView) convertView.findViewById(R.id.nameTextView); ElementPath elementPath = (ElementPath) getGroup(groupPosition); item.setText(elementPath.name); return convertView; } @Override public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { ElementPath elementPath = (ElementPath) getChild(groupPosition, childPosition); if (convertView == null) { convertView = mActivity.getLayoutInflater().inflate(R.layout.layout_info_item_characteristic, parent, false); } BluetoothGattService service = mBleManager.getGattService(elementPath.serviceUUID, elementPath.serviceInstance); boolean isReadable = elementPath.characteristicUUID != null && mBleManager.isCharacteristicReadable(service, elementPath.characteristicUUID); // Tag convertView.setTag(elementPath); // Name TextView nameTextView = (TextView) convertView.findViewById(R.id.nameTextView); nameTextView.setText(elementPath.isShowingName ? elementPath.name : elementPath.uuid); // Value TextView valueTextView = (TextView) convertView.findViewById(R.id.valueTextView); byte[] value = mValuesMap.get(elementPath.getKey()); String valueString = getValueFormattedInGraphicCharacters(value, elementPath); valueTextView.setText(valueString); valueTextView.setVisibility(valueString == null ? View.GONE : View.VISIBLE); // Update button ImageButton updateButton = (ImageButton) convertView.findViewById(R.id.updateButton); updateButton.setVisibility(isReadable ? View.VISIBLE : View.GONE); updateButton.setTag(elementPath); // Notify button ImageButton notifyButton = (ImageButton) convertView.findViewById(R.id.notifyButton); boolean isNotifiable = elementPath.characteristicUUID != null && elementPath.descriptorUUID == null && mBleManager.isCharacteristicNotifiable(service, elementPath.characteristicUUID); notifyButton.setVisibility(isNotifiable ? View.VISIBLE : View.GONE); notifyButton.setTag(elementPath); // List setup ExpandableHeightListView listView = (ExpandableHeightListView) convertView.findViewById(R.id.descriptorsListView); listView.setExpanded(true); // Descriptors final String key = elementPath.getKey(); List<ElementPath> descriptorNamesList = mDescriptors.get(key); DescriptorAdapter adapter = new DescriptorAdapter(mActivity, R.layout.layout_info_item_descriptor, descriptorNamesList); listView.setAdapter(adapter); return convertView; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return false; } } private class DescriptorAdapter extends ArrayAdapter<ElementPath> { Activity mActivity; DescriptorAdapter(Activity activity, int resource, List<ElementPath> items) { super(activity, resource, items); mActivity = activity; } @NonNull public View getView(int position, View convertView, @NonNull ViewGroup parent) { ElementPath elementPath = getItem(position); if (convertView == null) { convertView = mActivity.getLayoutInflater().inflate(R.layout.layout_info_item_descriptor, parent, false); } // Tag convertView.setTag(elementPath); // Name TextView nameTextView = (TextView) convertView.findViewById(R.id.nameTextView); nameTextView.setText(elementPath.isShowingName ? elementPath.name : elementPath.uuid); // Value TextView valueTextView = (TextView) convertView.findViewById(R.id.valueTextView); byte[] value = mValuesMap.get(elementPath.getKey()); String valueString = getValueFormattedInGraphicCharacters(value, elementPath); valueTextView.setText(valueString); valueTextView.setVisibility(valueString == null ? View.GONE : View.VISIBLE); // Update button ImageButton updateButton = (ImageButton) convertView.findViewById(R.id.updateButton); updateButton.setTag(elementPath); return convertView; } } //endregion //region Utils private final static int kDataFormat_Auto = -1; private final static int kDataFormat_String = 0; private final static int kDataFormat_Hex = 1; private String getValueFormattedInGraphicCharacters(byte[] value, ElementPath elementPath) { String valueString = getValueFormatted(value, elementPath.dataFormat); // if format is auto and the result is not visible, change the format to hex if (valueString != null && elementPath.dataFormat == kDataFormat_Auto && !TextUtils.isGraphic(valueString)) { elementPath.dataFormat = kDataFormat_Hex; valueString = getValueFormatted(value, elementPath.dataFormat); } return valueString; } private String getValueFormatted(byte[] value, int dataFormat) { String valueString = null; if (value != null) { if (dataFormat == kDataFormat_String || dataFormat == kDataFormat_Auto) { valueString = new String(value); } else if (dataFormat == kDataFormat_Hex) { String hexString = BleUtils.bytesToHex(value); String[] hexGroups = splitStringEvery(hexString, 2); valueString = TextUtils.join("-", hexGroups); } } return valueString; } private static String[] splitStringEvery(String s, int interval) { // based on: http://stackoverflow.com/questions/12295711/split-a-string-at-every-nth-position int arrayLength = (int) Math.ceil(((s.length() / (double) interval))); String[] result = new String[arrayLength]; int j = 0; int lastIndex = result.length - 1; for (int i = 0; i < lastIndex; i++) { result[i] = s.substring(j, j + interval); j += interval; } if (lastIndex >= 0) { result[lastIndex] = s.substring(j); } return result; } //endregion //region Actions public void onClickCharacteristic(View view) { ElementPath elementPath = (ElementPath) view.getTag(); if (elementPath != null) { // Check if is a characteristic if (elementPath.characteristicUUID != null && elementPath.descriptorUUID == null) { BluetoothGattService service = mBleManager.getGattService(elementPath.serviceUUID, elementPath.serviceInstance); if (mBleManager.isCharacteristicReadable(service, elementPath.characteristicUUID)) { Log.d(TAG, "Read char"); mBleManager.readCharacteristic(service, elementPath.characteristicUUID); } } } } public void onClickNotifyCharacteristic(View view) { ElementPath elementPath = (ElementPath) view.getTag(); if (elementPath != null) { // Check if is a characteristic if (elementPath.characteristicUUID != null && elementPath.descriptorUUID == null) { BluetoothGattService service = mBleManager.getGattService(elementPath.serviceUUID, elementPath.serviceInstance); if (mBleManager.isCharacteristicNotifiable(service, elementPath.characteristicUUID)) { Log.d(TAG, "Notify char"); ImageButton imageButton = (ImageButton) view; final boolean selected = !imageButton.isSelected(); imageButton.setSelected(selected); mBleManager.enableNotification(service, elementPath.characteristicUUID, selected); // Button color effect when pressed imageButton.setImageResource(selected ? R.drawable.ic_sync_white_24dp : R.drawable.ic_sync_black_24dp); } } } } public void onClickDescriptor(View view) { ElementPath elementPath = (ElementPath) view.getTag(); if (elementPath != null) { // Check if is a descriptor if (elementPath.characteristicUUID != null && elementPath.descriptorUUID != null) { BluetoothGattService service = mBleManager.getGattService(elementPath.serviceUUID, elementPath.serviceInstance); Log.d(TAG, "Read desc"); mBleManager.readDescriptor(service, elementPath.characteristicUUID, elementPath.descriptorUUID); } } } public void onClickDataFormat(View view) { ElementPath elementPath = (ElementPath) view.getTag(); if (elementPath != null) { Log.d(TAG, "Toggle data format"); if (elementPath.dataFormat == kDataFormat_Auto) { // Special case for auto (the data format as not been set yet) elementPath.dataFormat = kDataFormat_Hex; } else { elementPath.dataFormat = (elementPath.dataFormat + 1) % kDataFormatCount; } mInfoListAdapter.notifyDataSetChanged(); } } // endregion // region DataFragment public static class DataFragment extends Fragment { private List<ElementPath> mServicesList; private Map<String, List<ElementPath>> mCharacteristicsMap; private Map<String, List<ElementPath>> mDescriptorsMap; private Map<String, byte[]> mValuesMap; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } } private void restoreRetainedDataFragment() { // find the retained fragment FragmentManager fm = getFragmentManager(); mRetainedDataFragment = (DataFragment) fm.findFragmentByTag(TAG); if (mRetainedDataFragment == null) { // Create mRetainedDataFragment = new DataFragment(); fm.beginTransaction().add(mRetainedDataFragment, TAG).commit(); mServicesList = new ArrayList<>(); mCharacteristicsMap = new LinkedHashMap<>(); mDescriptorsMap = new LinkedHashMap<>(); mValuesMap = new LinkedHashMap<>(); } else { // Restore status mServicesList = mRetainedDataFragment.mServicesList; mCharacteristicsMap = mRetainedDataFragment.mCharacteristicsMap; mDescriptorsMap = mRetainedDataFragment.mDescriptorsMap; mValuesMap = mRetainedDataFragment.mValuesMap; } } private void saveRetainedDataFragment() { mRetainedDataFragment.mServicesList = mServicesList; mRetainedDataFragment.mCharacteristicsMap = mCharacteristicsMap; mRetainedDataFragment.mDescriptorsMap = mDescriptorsMap; mRetainedDataFragment.mValuesMap = mValuesMap; } // endregion }