content: 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
  }