com.zologic.tardis.app.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.zologic.tardis.app.MainActivity.java

Source

package com.zologic.tardis.app;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.FragmentManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.Button;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import com.zologic.tardis.R;
import com.zologic.tardis.app.settings.SettingsActivity;
import com.zologic.tardis.app.update.FirmwareUpdater;
import com.zologic.tardis.app.update.ReleasesParser;
import com.zologic.tardis.ble.BleDevicesScanner;
import com.zologic.tardis.ble.BleManager;
import com.zologic.tardis.ble.BleUtils;
import com.zologic.tardis.ui.utils.DialogUtils;
import com.zologic.tardis.ui.utils.ExpandableHeightExpandableListView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;

import static android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;

public class MainActivity extends AppCompatActivity implements BleManager.BleManagerListener,
        BleUtils.ResetBluetoothAdapterListener, FirmwareUpdater.FirmwareUpdaterListener {
    // Constants
    private final static String TAG = MainActivity.class.getSimpleName();
    private final static long kMinDelayToUpdateUI = 200; // in milliseconds
    private static final String kGenericAttributeService = "00001801-0000-1000-8000-00805F9B34FB";
    private static final String kServiceChangedCharacteristic = "00002A05-0000-1000-8000-00805F9B34FB";

    // Components
    private final static int kComponentsNameIds[] = { R.string.scan_connectservice_info,
            R.string.scan_connectservice_uart, R.string.scan_connectservice_pinio,
            R.string.scan_connectservice_controller, R.string.scan_connectservice_beacon, };

    // Activity request codes (used for onActivityResult)
    private static final int kActivityRequestCode_EnableBluetooth = 1;
    private static final int kActivityRequestCode_Settings = 2;
    private static final int kActivityRequestCode_ConnectedActivity = 3;

    // UI
    private ExpandableHeightExpandableListView mScannedDevicesListView;
    private ExpandableListAdapter mScannedDevicesAdapter;
    private Button mScanButton;
    private long mLastUpdateMillis;
    private TextView mNoDevicesTextView;
    private ScrollView mDevicesScrollView;
    private SwipeRefreshLayout mSwipeRefreshLayout;

    private AlertDialog mConnectingDialog;

    // Data
    private BleManager mBleManager;
    private boolean mIsScanPaused = true;
    private BleDevicesScanner mScanner;
    private FirmwareUpdater mFirmwareUpdater;

    private ArrayList<BluetoothDeviceData> mScannedDevices;
    private BluetoothDeviceData mSelectedDeviceData;
    private Class<?> mComponentToStartWhenConnected;
    private boolean mShouldEnableWifiOnQuit = false;
    private String mLatestCheckedDeviceAddress;

    private DataFragment mRetainedDataFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Init variables
        mBleManager = BleManager.getInstance(this);
        restoreRetainedDataFragment();

        // UI
        mScannedDevicesListView = (ExpandableHeightExpandableListView) findViewById(R.id.scannedDevicesListView);
        mScannedDevicesAdapter = new ExpandableListAdapter(mScannedDevices);
        mScannedDevicesListView.setAdapter(mScannedDevicesAdapter);
        mScannedDevicesListView.setExpanded(true);

        mScannedDevicesListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
            @Override
            public void onGroupExpand(int groupPosition) {
            }
        });

        mScanButton = (Button) findViewById(R.id.scanButton);

        mNoDevicesTextView = (TextView) findViewById(R.id.nodevicesTextView);
        mDevicesScrollView = (ScrollView) findViewById(R.id.devicesScrollView);
        mDevicesScrollView.setVisibility(View.GONE);

        mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
        mSwipeRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh() {
                mScannedDevices.clear();
                startScan(null, null);

                mSwipeRefreshLayout.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mSwipeRefreshLayout.setRefreshing(false);
                    }
                }, 500);
            }
        });

        // Setup when activity is created for the first time
        if (savedInstanceState == null) {
            // Read preferences
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            boolean autoResetBluetoothOnStart = sharedPreferences.getBoolean("pref_resetble", false);
            boolean disableWifi = sharedPreferences.getBoolean("pref_disableWifi", false);
            boolean updatesEnabled = sharedPreferences.getBoolean("pref_updatesenabled", true);

            // Update SoftwareUpdateManager
            if (updatesEnabled) {
                mFirmwareUpdater = new FirmwareUpdater(this, this);
                mFirmwareUpdater.refreshSoftwareUpdatesDatabase();
            }

            // Turn off wifi
            if (disableWifi) {
                final boolean isWifiEnabled = BleUtils.isWifiEnabled(this);
                if (isWifiEnabled) {
                    BleUtils.enableWifi(false, this);
                    mShouldEnableWifiOnQuit = true;
                }
            }

            // Check if bluetooth adapter is available
            final boolean wasBluetoothEnabled = manageBluetoothAvailability();
            final boolean areLocationServicesReadyForScanning = manageLocationServiceAvailabilityForScanning();

            // Reset bluetooth
            if (autoResetBluetoothOnStart && wasBluetoothEnabled && areLocationServicesReadyForScanning) {
                BleUtils.resetBluetoothAdapter(this, this);
            }
        }
    }

    @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_main, 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_settings) {
            Intent intent = new Intent();
            intent.setClass(MainActivity.this, SettingsActivity.class);
            startActivityForResult(intent, kActivityRequestCode_Settings);
            return true;
        } else if (id == R.id.action_licenses) {
            Intent intent = new Intent(this, CommonHelpActivity.class);
            intent.putExtra("title", getString(R.string.licenses_title));
            intent.putExtra("help", "licenses.html");
            startActivity(intent);
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onResume() {
        super.onResume();

        // Set listener
        mBleManager.setBleListener(this);

        // Autostart scan
        if (BleUtils.getBleStatus(this) == BleUtils.STATUS_BLE_ENABLED) {
            // If was connected, disconnect
            mBleManager.disconnect();

            // Force restart scanning
            if (mScannedDevices != null) { // Fixed a weird bug when resuming the app (this was null on very rare occasions even if it should not be)
                mScannedDevices.clear();
            }
            startScan(null, null);
        }

        // Update UI
        updateUI();
    }

    @Override
    public void onPause() {
        // Stop scanning
        if (mScanner != null && mScanner.isScanning()) {
            mIsScanPaused = true;
            stopScanning();
        }

        super.onPause();
    }

    public void onStop() {
        if (mConnectingDialog != null) {
            mConnectingDialog.cancel();
            mConnectingDialog = null;
        }

        super.onStop();
    }

    @Override
    public void onBackPressed() {
        if (mShouldEnableWifiOnQuit) {
            mShouldEnableWifiOnQuit = false;
            new AlertDialog.Builder(this).setTitle(getString(R.string.settingsaction_confirmenablewifi_title))
                    .setMessage(getString(R.string.settingsaction_confirmenablewifi_message))
                    .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            Log.d(TAG, "enableNotification wifi");
                            BleUtils.enableWifi(true, MainActivity.this);
                            MainActivity.super.onBackPressed();
                        }

                    }).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            MainActivity.super.onBackPressed();
                        }

                    }).show();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    protected void onDestroy() {
        // Stop ble adapter reset if in progress
        BleUtils.cancelBluetoothAdapterReset();

        // Retain data
        saveRetainedDataFragment();

        // Clean
        if (mConnectingDialog != null) {
            mConnectingDialog.cancel();
        }

        super.onDestroy();
    }

    private void resumeScanning() {
        if (mIsScanPaused) {
            startScan(null, null);
            mIsScanPaused = mScanner == null;
        }
    }

    private void showChooseDeviceServiceDialog(final BluetoothDevice device) {
        // Prepare dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        String deviceName = device.getName();
        String title = String.format(getString(R.string.scan_connectto_dialog_title_format),
                deviceName != null ? deviceName : device.getAddress());
        String[] items = new String[kComponentsNameIds.length];
        for (int i = 0; i < kComponentsNameIds.length; i++)
            items[i] = getString(kComponentsNameIds[i]);

        builder.setTitle(title).setItems(items, new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                switch (kComponentsNameIds[which]) {
                case R.string.scan_connectservice_info: { // Info
                    mComponentToStartWhenConnected = InfoActivity.class;
                    break;
                }
                case R.string.scan_connectservice_uart: { // Uart
                    mComponentToStartWhenConnected = UartActivity.class;
                    break;
                }
                case R.string.scan_connectservice_pinio: { // PinIO
                    mComponentToStartWhenConnected = PinIOActivity.class;
                    break;
                }
                case R.string.scan_connectservice_controller: { // Controller
                    mComponentToStartWhenConnected = ControllerActivity.class;
                    break;
                }
                case R.string.scan_connectservice_beacon: { // Beacon
                    mComponentToStartWhenConnected = BeaconActivity.class;
                    break;
                }
                }

                if (mComponentToStartWhenConnected != null) {
                    connect(device); // First connect to the device, and when connected go to selected activity
                }
            }
        });

        // Show dialog
        AlertDialog dialog = builder.create();
        dialog.show();
    }

    private boolean manageBluetoothAvailability() {
        boolean isEnabled = true;

        // Check Bluetooth HW status
        int errorMessageId = 0;
        final int bleStatus = BleUtils.getBleStatus(getBaseContext());
        switch (bleStatus) {
        case BleUtils.STATUS_BLE_NOT_AVAILABLE:
            errorMessageId = R.string.dialog_error_no_ble;
            isEnabled = false;
            break;
        case BleUtils.STATUS_BLUETOOTH_NOT_AVAILABLE: {
            errorMessageId = R.string.dialog_error_no_bluetooth;
            isEnabled = false; // it was already off
            break;
        }
        case BleUtils.STATUS_BLUETOOTH_DISABLED: {
            isEnabled = false; // it was already off
            // if no enabled, launch settings dialog to enable it (user should always be prompted before automatically enabling bluetooth)
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBtIntent, kActivityRequestCode_EnableBluetooth);
            // execution will continue at onActivityResult()
            break;
        }
        }
        if (errorMessageId > 0) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            AlertDialog dialog = builder.setMessage(errorMessageId).setPositiveButton(R.string.dialog_ok, null)
                    .show();
            DialogUtils.keepDialogOnOrientationChanges(dialog);
        }

        return isEnabled;
    }

    private boolean manageLocationServiceAvailabilityForScanning() {

        boolean areLocationServiceReady = true;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Location services are only needed to be enabled from Android 6.0
            int locationMode = Settings.Secure.LOCATION_MODE_OFF;
            try {
                locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);

            } catch (Settings.SettingNotFoundException e) {
                e.printStackTrace();
            }
            areLocationServiceReady = locationMode != Settings.Secure.LOCATION_MODE_OFF;

            if (!areLocationServiceReady) {

                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                AlertDialog dialog = builder
                        .setMessage(R.string.dialog_error_nolocationservices_requiredforscan_marshmallow)
                        .setPositiveButton(R.string.dialog_ok, null).show();
                DialogUtils.keepDialogOnOrientationChanges(dialog);
            }
        }

        return areLocationServiceReady;
    }

    private void connect(BluetoothDevice device) {
        boolean isConnecting = mBleManager.connect(this, device.getAddress());
        if (isConnecting) {
            showConnectionStatus(true);
        }
    }

    private void startHelp() {
        // Launch app help activity
        Intent intent = new Intent(this, MainHelpActivity.class);
        startActivity(intent);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == kActivityRequestCode_ConnectedActivity) {
            if (resultCode < 0) {
                Toast.makeText(this, R.string.scan_unexpecteddisconnect, Toast.LENGTH_LONG).show();
            }
        } else if (requestCode == kActivityRequestCode_EnableBluetooth) {
            if (resultCode == Activity.RESULT_OK) {
                // Bluetooth was enabled, resume scanning
                resumeScanning();
            } else if (resultCode == Activity.RESULT_CANCELED) {
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                AlertDialog dialog = builder.setMessage(R.string.dialog_error_no_bluetooth)
                        .setPositiveButton(R.string.dialog_ok, null).show();
                DialogUtils.keepDialogOnOrientationChanges(dialog);

            }
        } else if (requestCode == kActivityRequestCode_Settings) {
            // Return from activity settings. Update app behaviour if needed
            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
            boolean updatesEnabled = sharedPreferences.getBoolean("pref_updatesenabled", true);
            if (updatesEnabled) {
                mLatestCheckedDeviceAddress = null;
                mFirmwareUpdater.refreshSoftwareUpdatesDatabase();
            } else {
                mFirmwareUpdater = null;
            }
        }
    }

    private void showConnectionStatus(boolean enable) {
        showStatusDialog(enable, R.string.scan_connecting);
    }

    private void showGettingUpdateInfoState() {
        showConnectionStatus(false);
        showStatusDialog(true, R.string.scan_gettingupdateinfo);
    }

    private void showStatusDialog(boolean show, int stringId) {
        if (show) {

            // Remove if a previous dialog was open (maybe because was clicked 2 times really quick)
            if (mConnectingDialog != null) {
                mConnectingDialog.cancel();
            }

            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage(stringId);

            // Show dialog
            mConnectingDialog = builder.create();
            mConnectingDialog.setCanceledOnTouchOutside(false);

            mConnectingDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
                @Override
                public boolean onKey(DialogInterface arg0, int keyCode, KeyEvent event) {
                    if (keyCode == KeyEvent.KEYCODE_BACK) {
                        mBleManager.disconnect();
                        mConnectingDialog.cancel();
                    }
                    return true;
                }
            });
            mConnectingDialog.show();
        } else {
            if (mConnectingDialog != null) {
                mConnectingDialog.cancel();
            }
        }
    }

    // region Actions
    public void onClickScannedDevice(final View view) {
        final int groupPosition = (Integer) view.getTag();

        if (mScannedDevicesListView.isGroupExpanded(groupPosition)) {
            mScannedDevicesListView.collapseGroup(groupPosition);
        } else {
            mScannedDevicesListView.expandGroup(groupPosition, true);

            // Force scrolling to view the children
            mDevicesScrollView.post(new Runnable() {
                @Override
                public void run() {
                    mScannedDevicesListView.scrollToGroup(groupPosition, view, mDevicesScrollView);
                }
            });
        }
    }

    public void onClickDeviceConnect(View view) {
        stopScanning();

        final int scannedDeviceIndex = (Integer) view.getTag();
        if (scannedDeviceIndex < mScannedDevices.size()) {
            mSelectedDeviceData = mScannedDevices.get(scannedDeviceIndex);
            BluetoothDevice device = mSelectedDeviceData.device;

            mBleManager.setBleListener(MainActivity.this); // Force set listener (could be still checking for updates...)

            if (mSelectedDeviceData.type == BluetoothDeviceData.kType_Uart) { // if is uart, show all the available activities
                showChooseDeviceServiceDialog(device);
            } else { // if no uart, then go directly to info
                Log.d(TAG, "No UART service found. Go to InfoActivity");
                mComponentToStartWhenConnected = InfoActivity.class;
                connect(device);
            }
        } else {
            Log.w(TAG, "onClickDeviceConnect index does not exist: " + scannedDeviceIndex);
        }
    }

    public void onClickScan(View view) {
        boolean isScanning = mScanner != null && mScanner.isScanning();
        if (isScanning) {
            stopScanning();
        } else {
            startScan(null, null);
        }
    }
    // endregion

    // region Scan
    private void startScan(final UUID[] servicesToScan, final String deviceNameToScanFor) {
        Log.d(TAG, "startScan");

        // Stop current scanning (if needed)
        stopScanning();

        // Configure scanning
        BluetoothAdapter bluetoothAdapter = BleUtils.getBluetoothAdapter(getApplicationContext());
        if (BleUtils.getBleStatus(this) != BleUtils.STATUS_BLE_ENABLED) {
            Log.w(TAG, "startScan: BluetoothAdapter not initialized or unspecified address.");
        } else {
            mScanner = new BleDevicesScanner(bluetoothAdapter, servicesToScan,
                    new BluetoothAdapter.LeScanCallback() {
                        @Override
                        public void onLeScan(final BluetoothDevice device, final int rssi, byte[] scanRecord) {
                            final String deviceName = device.getName();
                            //Log.d(TAG, "Discovered device: " + (deviceName != null ? deviceName : "<unknown>"));

                            BluetoothDeviceData previouslyScannedDeviceData = null;
                            if (deviceNameToScanFor == null
                                    || (deviceName != null && deviceName.equalsIgnoreCase(deviceNameToScanFor))) { // Workaround for bug in service discovery. Discovery filtered by service uuid is not working on Android 4.3, 4.4
                                if (mScannedDevices == null)
                                    mScannedDevices = new ArrayList<>(); // Safeguard

                                // Check that the device was not previously found
                                for (BluetoothDeviceData deviceData : mScannedDevices) {
                                    if (deviceData.device.getAddress().equals(device.getAddress())) {
                                        previouslyScannedDeviceData = deviceData;
                                        break;
                                    }
                                }

                                BluetoothDeviceData deviceData;
                                if (previouslyScannedDeviceData == null) {
                                    // Add it to the mScannedDevice list
                                    deviceData = new BluetoothDeviceData();
                                    mScannedDevices.add(deviceData);
                                } else {
                                    deviceData = previouslyScannedDeviceData;
                                }

                                deviceData.device = device;
                                deviceData.rssi = rssi;
                                deviceData.scanRecord = scanRecord;
                                decodeScanRecords(deviceData);

                                // Update device data
                                long currentMillis = SystemClock.uptimeMillis();
                                if (previouslyScannedDeviceData == null
                                        || currentMillis - mLastUpdateMillis > kMinDelayToUpdateUI) { // Avoid updating when not a new device has been found and the time from the last update is really short to avoid updating UI so fast that it will become unresponsive
                                    mLastUpdateMillis = currentMillis;

                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            updateUI();
                                        }
                                    });
                                }
                            }
                        }
                    });

            // Start scanning
            mScanner.start();
        }

        // Update UI
        updateUI();
    }

    private void stopScanning() {
        // Stop scanning
        if (mScanner != null) {
            mScanner.stop();
            mScanner = null;
        }

        updateUI();
    }
    // endregion

    private void decodeScanRecords(BluetoothDeviceData deviceData) {
        // based on http://stackoverflow.com/questions/24003777/read-advertisement-packet-in-android
        final byte[] scanRecord = deviceData.scanRecord;

        ArrayList<UUID> uuids = new ArrayList<>();
        byte[] advertisedData = Arrays.copyOf(scanRecord, scanRecord.length);
        int offset = 0;
        deviceData.type = BluetoothDeviceData.kType_Unknown;

        // Check if is an iBeacon ( 0x02, 0x0x1, a flag byte, 0x1A, 0xFF, manufacturer (2bytes), 0x02, 0x15)
        final boolean isBeacon = advertisedData[0] == 0x02 && advertisedData[1] == 0x01 && advertisedData[3] == 0x1A
                && advertisedData[4] == (byte) 0xFF && advertisedData[7] == 0x02 && advertisedData[8] == 0x15;

        // Check if is an URIBeacon
        final byte[] kUriBeaconPrefix = { 0x03, 0x03, (byte) 0xD8, (byte) 0xFE };
        final boolean isUriBeacon = Arrays.equals(Arrays.copyOf(scanRecord, kUriBeaconPrefix.length),
                kUriBeaconPrefix) && advertisedData[5] == 0x16 && advertisedData[6] == kUriBeaconPrefix[2]
                && advertisedData[7] == kUriBeaconPrefix[3];

        if (isBeacon) {
            deviceData.type = BluetoothDeviceData.kType_Beacon;

            // Read uuid
            offset = 9;
            UUID uuid = BleUtils.getUuidFromByteArrayBigEndian(Arrays.copyOfRange(scanRecord, offset, offset + 16));
            uuids.add(uuid);
            offset += 16;

            // Skip major minor
            offset += 2 * 2; // major, minor

            // Read txpower
            final int txPower = advertisedData[offset++];
            deviceData.txPower = txPower;
        } else if (isUriBeacon) {
            deviceData.type = BluetoothDeviceData.kType_UriBeacon;

            // Read txpower
            final int txPower = advertisedData[9];
            deviceData.txPower = txPower;
        } else {
            // Read standard advertising packet
            while (offset < advertisedData.length - 2) {
                // Length
                int len = advertisedData[offset++];
                if (len == 0)
                    break;

                // Type
                int type = advertisedData[offset++];
                if (type == 0)
                    break;

                // Data
                //            Log.d(TAG, "record -> lenght: " + length + " type:" + type + " data" + data);

                switch (type) {
                case 0x02: // Partial list of 16-bit UUIDs
                case 0x03: {// Complete list of 16-bit UUIDs
                    while (len > 1) {
                        int uuid16 = advertisedData[offset++] & 0xFF;
                        uuid16 |= (advertisedData[offset++] << 8);
                        len -= 2;
                        uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", uuid16)));
                    }
                    break;
                }

                case 0x06: // Partial list of 128-bit UUIDs
                case 0x07: { // Complete list of 128-bit UUIDs
                    while (len >= 16) {
                        try {
                            // Wrap the advertised bits and order them.
                            UUID uuid = BleUtils.getUuidFromByteArraLittleEndian(
                                    Arrays.copyOfRange(advertisedData, offset, offset + 16));
                            uuids.add(uuid);

                        } catch (IndexOutOfBoundsException e) {
                            Log.e(TAG, "BlueToothDeviceFilter.parseUUID: " + e.toString());
                        } finally {
                            // Move the offset to read the next uuid.
                            offset += 16;
                            len -= 16;
                        }
                    }
                    break;
                }

                case 0x0A: { // TX Power
                    final int txPower = advertisedData[offset++];
                    deviceData.txPower = txPower;
                    break;
                }

                default: {
                    offset += (len - 1);
                    break;
                }
                }
            }

            // Check if Uart is contained in the uuids
            boolean isUart = false;
            for (UUID uuid : uuids) {
                if (uuid.toString().equalsIgnoreCase(UartInterfaceActivity.UUID_SERVICE)) {
                    isUart = true;
                    break;
                }
            }
            if (isUart) {
                deviceData.type = BluetoothDeviceData.kType_Uart;
            }
        }

        deviceData.uuids = uuids;
    }

    private void updateUI() {
        // Scan button
        boolean isScanning = mScanner != null && mScanner.isScanning();
        mScanButton
                .setText(getString(isScanning ? R.string.scan_scanbutton_scanning : R.string.scan_scanbutton_scan));

        // Show list and hide "no devices" label
        final boolean isListEmpty = mScannedDevices == null || mScannedDevices.size() == 0;
        mNoDevicesTextView.setVisibility(isListEmpty ? View.VISIBLE : View.GONE);
        mDevicesScrollView.setVisibility(isListEmpty ? View.GONE : View.VISIBLE);

        // devices list
        mScannedDevicesAdapter.notifyDataSetChanged();
    }

    // region ResetBluetoothAdapterListener
    @Override
    public void resetBluetoothCompleted() {
        Log.d(TAG, "Reset completed -> Resume scanning");
        resumeScanning();
    }
    // endregion

    private void launchComponentActivity() {
        // Enable generic attribute service
        final BluetoothGattService genericAttributeService = mBleManager.getGattService(kGenericAttributeService);
        if (genericAttributeService != null) {
            Log.d(TAG, "kGenericAttributeService found. Check if kServiceChangedCharacteristic exists");

            final UUID characteristicUuid = UUID.fromString(kServiceChangedCharacteristic);
            final BluetoothGattCharacteristic dataCharacteristic = genericAttributeService
                    .getCharacteristic(characteristicUuid);
            if (dataCharacteristic != null) {
                Log.d(TAG, "kServiceChangedCharacteristic exists. Enable indication");
                mBleManager.enableIndication(genericAttributeService, kServiceChangedCharacteristic, true);
            } else {
                Log.d(TAG, "Skip enable indications for kServiceChangedCharacteristic. Characteristic not found");
            }
        } else {
            Log.d(TAG,
                    "Skip enable indications for kServiceChangedCharacteristic. kGenericAttributeService not found");
        }

        // Launch activity
        showConnectionStatus(false);
        if (mComponentToStartWhenConnected != null) {
            Intent intent = new Intent(MainActivity.this, mComponentToStartWhenConnected);
            if (mComponentToStartWhenConnected == BeaconActivity.class && mSelectedDeviceData != null) {
                intent.putExtra("rssi", mSelectedDeviceData.rssi);
            }
            startActivityForResult(intent, kActivityRequestCode_ConnectedActivity);
        }
    }

    // region BleManagerListener
    @Override
    public void onConnected() {
    }

    @Override
    public void onConnecting() {
    }

    @Override
    public void onDisconnected() {
        showConnectionStatus(false);
    }

    @Override
    public void onServicesDiscovered() {
        Log.d(TAG, "services discovered");

        // Check if there is a failed installation that was stored to retry
        boolean isFailedInstallationDetected = FirmwareUpdater.isFailedInstallationRecoveryAvailable(this,
                mBleManager.getConnectedDeviceAddress());
        if (isFailedInstallationDetected) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "Failed installation detected");
                    // Ask user if should update
                    new AlertDialog.Builder(MainActivity.this).setTitle(R.string.scan_failedupdatedetected_title)
                            .setMessage(R.string.scan_failedupdatedetected_message)
                            .setPositiveButton(R.string.scan_failedupdatedetected_ok,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            showConnectionStatus(false); // hide current dialogs because software update will display a dialog
                                            stopScanning();

                                            mFirmwareUpdater.startFailedInstallationRecovery(MainActivity.this);
                                        }
                                    })
                            .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    FirmwareUpdater.clearFailedInstallationRecoveryParams(MainActivity.this);
                                    launchComponentActivity();
                                }
                            }).setCancelable(false).show();
                }
            });
        } else {
            // Check if a firmware update is available
            boolean isCheckingFirmware = false;
            if (mFirmwareUpdater != null) {
                // Don't bother the user waiting for checks if the latest connected device was this one too
                String deviceAddress = mBleManager.getConnectedDeviceAddress();
                if (!deviceAddress.equals(mLatestCheckedDeviceAddress)) {
                    mLatestCheckedDeviceAddress = deviceAddress;

                    // Check if should update device software
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            showGettingUpdateInfoState();
                        }
                    });
                    mFirmwareUpdater.checkFirmwareUpdatesForTheCurrentConnectedDevice(); // continues asynchronously in onFirmwareUpdatesChecked
                    isCheckingFirmware = true;
                } else {
                    Log.d(TAG, "Updates: Device already checked previously. Skipping...");
                }
            }

            if (!isCheckingFirmware) {
                onFirmwareUpdatesChecked(false, null, null, null);
            }
        }
    }

    @Override
    public void onDataAvailable(BluetoothGattCharacteristic characteristic) {
    }

    @Override
    public void onDataAvailable(BluetoothGattDescriptor descriptor) {
    }

    @Override
    public void onReadRemoteRssi(int rssi) {

    }
    // endregion

    // region SoftwareUpdateManagerListener
    @Override
    public void onFirmwareUpdatesChecked(boolean isUpdateAvailable, final ReleasesParser.FirmwareInfo latestRelease,
            FirmwareUpdater.DeviceInfoData deviceInfoData, Map<String, ReleasesParser.BoardInfo> allReleases) {
        mBleManager.setBleListener(this); // Restore listener

        if (isUpdateAvailable) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // Ask user if should update
                    String message = String.format(getString(R.string.scan_softwareupdate_messageformat),
                            latestRelease.version);
                    new AlertDialog.Builder(MainActivity.this).setTitle(R.string.scan_softwareupdate_title)
                            .setMessage(message).setPositiveButton(R.string.scan_softwareupdate_install,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            showConnectionStatus(false); // hide current dialogs because software update will display a dialog
                                            stopScanning();
                                            //BluetoothDevice device = mBleManager.getConnectedDevice();
                                            mFirmwareUpdater.downloadAndInstall(MainActivity.this, latestRelease);
                                        }
                                    })
                            .setNeutralButton(R.string.scan_softwareupdate_notnow,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            launchComponentActivity();
                                        }
                                    })
                            .setNegativeButton(R.string.scan_softwareupdate_dontask,
                                    new DialogInterface.OnClickListener() {
                                        @Override
                                        public void onClick(DialogInterface dialog, int which) {
                                            mFirmwareUpdater.ignoreVersion(latestRelease.version);
                                            launchComponentActivity();
                                        }
                                    })
                            .setCancelable(false).show();
                }
            });
        } else {
            Log.d(TAG, "onFirmwareUpdatesChecked: No software update available");
            launchComponentActivity();
        }
    }

    @Override
    public void onUpdateCancelled() {
        Log.d(TAG, "Software version installation cancelled");

        mLatestCheckedDeviceAddress = null;

        mScannedDevices.clear();
        startScan(null, null);
    }

    @Override
    public void onUpdateCompleted() {
        Log.d(TAG, "Software version installation completed successfully");

        Toast.makeText(this, R.string.scan_softwareupdate_completed, Toast.LENGTH_LONG).show();

        mScannedDevices.clear();
        startScan(null, null);
    }

    @Override
    public void onUpdateFailed(boolean isDownloadError) {
        Log.d(TAG, "Software version installation failed");
        Toast.makeText(this, isDownloadError ? R.string.scan_softwareupdate_downloaderror
                : R.string.scan_softwareupdate_updateerror, Toast.LENGTH_LONG).show();

        mLatestCheckedDeviceAddress = null;

        mScannedDevices.clear();
        startScan(null, null);
    }

    @Override
    public void onUpdateDeviceDisconnected() {

        // Update UI
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                onDisconnected();

                mLatestCheckedDeviceAddress = null;

                mScannedDevices.clear();
                startScan(null, null);

            }
        });

    }
    // endregion

    // region Helpers
    private class BluetoothDeviceData {
        public BluetoothDevice device;
        public int rssi;
        public byte[] scanRecord;

        // Decoded scan record (update R.array.scan_devicetypes if this list is modified)
        public static final int kType_Unknown = 0;
        public static final int kType_Uart = 1;
        public static final int kType_Beacon = 2;
        public static final int kType_UriBeacon = 3;

        public int type;
        public int txPower;
        public ArrayList<UUID> uuids;
    }
    //endregion

    // region adapters
    private class ExpandableListAdapter extends BaseExpandableListAdapter {
        // Constants for common child
        private static final int kChildCommon_Name = 0;
        private static final int kChildCommon_Address = 1;
        private static final int kChildCommon_Services = 2;
        private static final int kChildCommon_TXPower = 3;

        // Constants for beacon child
        private static final int kChildBeacon_Name = 0;
        private static final int kChildBeacon_Address = 1;
        private static final int kChildBeacon_Manufacturer = 2;
        private static final int kChildBeacon_UUID = 3;
        private static final int kChildBeacon_Major = 4;
        private static final int kChildBeacon_Minor = 5;
        private static final int kChildBeacon_TXPower = 6;

        // Constants for uribeacon child
        private static final int kChildUriBeacon_Name = 0;
        private static final int kChildUriBeacon_Address = 1;
        private static final int kChildUriBeacon_Uri = 2;
        private static final int kChildUriBeacon_TXPower = 3;

        // Data
        private ArrayList<BluetoothDeviceData> mBluetoothDevices;

        private class GroupViewHolder {
            TextView nameTextView;
            TextView descriptionTextView;
            ImageView rssiImageView;
            TextView rssiTextView;
            Button connectButton;

        }

        public ExpandableListAdapter(ArrayList<BluetoothDeviceData> bluetoothDevices) {
            mBluetoothDevices = bluetoothDevices;
        }

        @Override
        public int getGroupCount() {
            int count = 0;
            if (mBluetoothDevices != null) {
                count = mBluetoothDevices.size();
            }
            return count;
        }

        @Override
        public int getChildrenCount(int groupPosition) {
            BluetoothDeviceData deviceData = mBluetoothDevices.get(groupPosition);
            switch (deviceData.type) {
            case BluetoothDeviceData.kType_Beacon:
                return 7; // Local name, Address, Manufacturer,  UUID, Major, Minor and TX Power Level

            case BluetoothDeviceData.kType_UriBeacon:
                return 4; // Local name, Address, Uri and TX Power Level

            default:
                return 4; // Local name, Address, Service UUIDs and TX Power Level
            }
        }

        @Override
        public Object getGroup(int groupPosition) {
            return mBluetoothDevices.get(groupPosition);
        }

        @Override
        public Object getChild(int groupPosition, int childPosition) {
            BluetoothDeviceData deviceData = mBluetoothDevices.get(groupPosition);

            switch (deviceData.type) {
            case BluetoothDeviceData.kType_Beacon:
                return getChildBeacon(deviceData, childPosition);

            case BluetoothDeviceData.kType_UriBeacon:
                return getChildUriBeacon(deviceData, childPosition);

            default:
                return getChildCommon(deviceData, childPosition);
            }
        }

        private Object getChildUriBeacon(BluetoothDeviceData deviceData, int childPosition) {
            switch (childPosition) {
            case kChildUriBeacon_Name: {
                String name = deviceData.device.getName();
                return getString(R.string.scan_device_localname) + ": " + (name == null ? "" : name);
            }
            case kChildUriBeacon_Address: {
                String address = deviceData.device.getAddress();
                return getString(R.string.scan_device_address) + ": " + (address == null ? "" : address);
            }
            case kChildUriBeacon_Uri: {
                String uri = UriBeaconUtils.getUriFromAdvertisingPacket(deviceData.scanRecord);
                return getString(R.string.scan_device_uribeacon_uri) + ": " + uri;
            }
            case kChildUriBeacon_TXPower: {
                return getString(R.string.scan_device_txpower) + ": " + deviceData.txPower;
            }
            default:
                return null;
            }
        }

        private Object getChildCommon(BluetoothDeviceData deviceData, int childPosition) {
            switch (childPosition) {
            case kChildCommon_Name: {
                String name = deviceData.device.getName();
                return getString(R.string.scan_device_localname) + ": " + (name == null ? "" : name);
            }
            case kChildCommon_Address: {
                String address = deviceData.device.getAddress();
                return getString(R.string.scan_device_address) + ": " + (address == null ? "" : address);
            }
            case kChildCommon_Services: {
                StringBuilder text = new StringBuilder();
                if (deviceData.uuids != null) {
                    int i = 0;
                    for (UUID uuid : deviceData.uuids) {
                        if (i > 0)
                            text.append(", ");
                        text.append(uuid.toString().toUpperCase());
                        i++;
                    }
                }
                return getString(R.string.scan_device_services) + ": " + text;
            }
            case kChildCommon_TXPower: {
                return getString(R.string.scan_device_txpower) + ": " + deviceData.txPower;
            }
            default:
                return null;
            }
        }

        private Object getChildBeacon(BluetoothDeviceData deviceData, int childPosition) {
            switch (childPosition) {
            case kChildBeacon_Name: {
                String name = deviceData.device.getName();
                return getString(R.string.scan_device_localname) + ": " + (name == null ? "" : name);
            }
            case kChildBeacon_Address: {
                String address = deviceData.device.getAddress();
                return getString(R.string.scan_device_address) + ": " + (address == null ? "" : address);
            }
            case kChildBeacon_Manufacturer: {
                final byte[] manufacturerBytes = { deviceData.scanRecord[6], deviceData.scanRecord[5] }; // Little endan
                String manufacturer = BleUtils.bytesToHex(manufacturerBytes);

                // Check if the manufacturer is known, and replace the id for a name
                String kKnownManufacturers[] = getResources().getStringArray(R.array.beacon_manufacturers_ids);
                int knownIndex = Arrays.asList(kKnownManufacturers).indexOf(manufacturer);
                if (knownIndex >= 0) {
                    String kManufacturerNames[] = getResources().getStringArray(R.array.beacon_manufacturers_names);
                    manufacturer = kManufacturerNames[knownIndex];
                }

                return getString(R.string.scan_device_beacon_manufacturer) + ": "
                        + (manufacturer == null ? "" : manufacturer);
            }

            case kChildBeacon_UUID: {
                StringBuilder text = new StringBuilder();
                if (deviceData.uuids != null && deviceData.uuids.size() == 1) {
                    UUID uuid = deviceData.uuids.get(0);
                    text.append(uuid.toString().toUpperCase());
                }
                return getString(R.string.scan_device_uuid) + ": " + text;
            }

            case kChildBeacon_Major: {
                final byte[] majorBytes = { deviceData.scanRecord[25], deviceData.scanRecord[26] }; // Big endian
                String major = BleUtils.bytesToHex(majorBytes);
                return getString(R.string.scan_device_beacon_major) + ": " + major;
            }

            case kChildBeacon_Minor: {
                final byte[] minorBytes = { deviceData.scanRecord[27], deviceData.scanRecord[28] }; // Big endian
                String minor = BleUtils.bytesToHex(minorBytes);
                return getString(R.string.scan_device_beacon_minor) + ": " + minor;
            }

            case kChildBeacon_TXPower: {
                return getString(R.string.scan_device_txpower) + ": " + deviceData.txPower;
            }
            default:
                return null;
            }
        }

        @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) {
            GroupViewHolder holder;

            if (convertView == null) {
                convertView = getLayoutInflater().inflate(R.layout.layout_scan_item_title, parent, false);

                holder = new GroupViewHolder();

                holder.nameTextView = (TextView) convertView.findViewById(R.id.nameTextView);
                holder.descriptionTextView = (TextView) convertView.findViewById(R.id.descriptionTextView);
                holder.rssiImageView = (ImageView) convertView.findViewById(R.id.rssiImageView);
                holder.rssiTextView = (TextView) convertView.findViewById(R.id.rssiTextView);
                holder.connectButton = (Button) convertView.findViewById(R.id.connectButton);

                convertView.setTag(R.string.scan_tag_id, holder);

            } else {
                holder = (GroupViewHolder) convertView.getTag(R.string.scan_tag_id);
            }

            convertView.setTag(groupPosition);
            holder.connectButton.setTag(groupPosition);

            BluetoothDeviceData deviceData = mBluetoothDevices.get(groupPosition);
            String deviceName = deviceData.device.getName();
            holder.nameTextView.setText(deviceName != null ? deviceName : deviceData.device.getAddress());

            holder.descriptionTextView.setVisibility(
                    deviceData.type != BluetoothDeviceData.kType_Unknown ? View.VISIBLE : View.INVISIBLE);
            holder.descriptionTextView
                    .setText(getResources().getStringArray(R.array.scan_devicetypes)[deviceData.type]);
            holder.rssiTextView.setText(deviceData.rssi == 127 ? getString(R.string.scan_device_rssi_notavailable)
                    : String.valueOf(deviceData.rssi));

            int rrsiDrawableResource = getDrawableIdForRssi(deviceData.rssi);
            holder.rssiImageView.setImageResource(rrsiDrawableResource);

            return convertView;
        }

        private int getDrawableIdForRssi(int rssi) {
            int index;
            if (rssi == 127 || rssi <= -84) { // 127 reserved for RSSI not available
                index = 0;
            } else if (rssi <= -72) {
                index = 1;
            } else if (rssi <= -60) {
                index = 2;
            } else if (rssi <= -48) {
                index = 3;
            } else {
                index = 4;
            }

            final int kSignalDrawables[] = { R.drawable.signalstrength0, R.drawable.signalstrength1,
                    R.drawable.signalstrength2, R.drawable.signalstrength3, R.drawable.signalstrength4 };
            return kSignalDrawables[index];
        }

        @Override
        public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView,
                ViewGroup parent) {
            if (convertView == null) {
                convertView = getLayoutInflater().inflate(R.layout.layout_scan_item_child, parent, false);
            }

            // We don't expect many items so for clarity just find the views each time instead of using a ViewHolder
            TextView textView = (TextView) convertView.findViewById(R.id.textView);
            String text = (String) getChild(groupPosition, childPosition);
            textView.setText(text);

            return convertView;
        }

        @Override
        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return false;
        }
    }
    //endregion

    // region DataFragment
    public static class DataFragment extends Fragment {
        private ArrayList<BluetoothDeviceData> mScannedDevices;
        private Class<?> mComponentToStartWhenConnected;
        private boolean mShouldEnableWifiOnQuit;
        private FirmwareUpdater mFirmwareUpdater;
        private String mLatestCheckedDeviceAddress;
        private BluetoothDeviceData mSelectedDeviceData;

        @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).commitAllowingStateLoss(); // http://stackoverflow.com/questions/7575921/illegalstateexception-can-not-perform-this-action-after-onsaveinstancestate-h

            mScannedDevices = new ArrayList<>();

        } else {
            // Restore status
            mScannedDevices = mRetainedDataFragment.mScannedDevices;
            mComponentToStartWhenConnected = mRetainedDataFragment.mComponentToStartWhenConnected;
            mShouldEnableWifiOnQuit = mRetainedDataFragment.mShouldEnableWifiOnQuit;
            mFirmwareUpdater = mRetainedDataFragment.mFirmwareUpdater;
            mLatestCheckedDeviceAddress = mRetainedDataFragment.mLatestCheckedDeviceAddress;
            mSelectedDeviceData = mRetainedDataFragment.mSelectedDeviceData;

            if (mFirmwareUpdater != null) {
                mFirmwareUpdater.changedParentActivity(this); // set the new activity
            }
        }
    }

    private void saveRetainedDataFragment() {
        mRetainedDataFragment.mScannedDevices = mScannedDevices;
        mRetainedDataFragment.mComponentToStartWhenConnected = mComponentToStartWhenConnected;
        mRetainedDataFragment.mShouldEnableWifiOnQuit = mShouldEnableWifiOnQuit;
        mRetainedDataFragment.mFirmwareUpdater = mFirmwareUpdater;
        mRetainedDataFragment.mLatestCheckedDeviceAddress = mLatestCheckedDeviceAddress;
        mRetainedDataFragment.mSelectedDeviceData = mSelectedDeviceData;
    }
    // endregion
}