pl.mrwojtek.sensrec.app.HeartRateDialog.java Source code

Java tutorial

Introduction

Here is the source code for pl.mrwojtek.sensrec.app.HeartRateDialog.java

Source

/*
 * (C) Copyright 2013, 2015 Wojciech Mruczkiewicz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     Wojciech Mruczkiewicz
 */

package pl.mrwojtek.sensrec.app;

import android.annotation.TargetApi;
import android.app.Dialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.SwitchCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

import pl.mrwojtek.sensrec.SensorsRecorder;
import pl.mrwojtek.sensrec.app.util.MaterialUtils;
import pl.mrwojtek.sensrec.ble.BleRecorder;

/**
 * Dialog for pairing Bluetooth heart rate sensors.
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class HeartRateDialog extends DialogFragment implements DialogInterface.OnClickListener {

    public static final String DIALOG_TAG = "HeartRateDialog";
    public static final String TAG = "SensRec";

    public static final int BLUETOOTH_ENABLE_REQUEST_CODE = 100;

    private static final long SCAN_PERIOD = 10000;

    private BluetoothAdapter bluetoothAdapter;
    private Handler handler;
    private Runnable scanStopRunnable;
    private boolean scanning;

    private SharedPreferences prefs;
    private ProgressBar progress;
    private Button button;
    private ListView list;
    private DevicesAdapter adapter;

    private int textColorPrimary;
    private int textColorSecondary;
    private int textColorTertiary;

    private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (adapter.add(device)) {
                        adapter.notifyDataSetChanged();
                    }
                }
            });
        }
    };

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        ContextThemeWrapper context = new ContextThemeWrapper(getActivity(), R.style.DialogTheme);
        LayoutInflater inflater = getActivity().getLayoutInflater().cloneInContext(context);
        View view = inflater.inflate(R.layout.heart_rate_dialog, null);

        prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());

        progress = (ProgressBar) view.findViewById(R.id.progress);
        button = (Button) view.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (bluetoothAdapter.isEnabled()) {
                    scanLeDevice(true);
                } else {
                    requestEnableBluetooth();
                }
            }
        });

        updateProgressAndButton();

        list = (ListView) view.findViewById(R.id.devices_list);
        list.setAdapter(adapter);

        textColorPrimary = ContextCompat.getColor(context, R.color.colorTextPrimary);
        textColorSecondary = ContextCompat.getColor(context, R.color.colorTextSecondary);
        textColorTertiary = ContextCompat.getColor(context, R.color.colorTextHint);

        AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.DialogTheme);
        builder.setTitle(R.string.ble_title);
        builder.setView(view);
        builder.setNegativeButton(R.string.action_cancel, null);
        builder.setPositiveButton(R.string.action_ok, this);
        return MaterialUtils.fixTitle(context, builder.create(), null);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        handler = new Handler(getContext().getMainLooper());
        adapter = new DevicesAdapter();

        SensorsRecorder recorder = RecordingService.getRecorder(getActivity());
        SortedMap<Integer, BleRecorder> bleRecorders = recorder.getBleRecorders();
        for (Map.Entry<Integer, BleRecorder> r : bleRecorders.entrySet()) {
            adapter.add(r.getValue());
        }

        final BluetoothManager bluetoothManager = (BluetoothManager) getContext()
                .getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();
        if (!bluetoothAdapter.isEnabled()) {
            Log.w(TAG, "Bluetooth adapter not enabled");
            requestEnableBluetooth();
        } else {
            scanLeDevice(true);
        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {
            updateDevices();
        }
    }

    @Override
    public void onDestroy() {
        scanLeDevice(false);
        super.onDestroy();
    }

    private void requestEnableBluetooth() {
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(intent, BLUETOOTH_ENABLE_REQUEST_CODE);
    }

    private void scanLeDevice(final boolean enable) {
        if (enable && !scanning) {
            scanStopRunnable = new Runnable() {
                @Override
                public void run() {
                    scanning = false;
                    updateProgressAndButton();
                    bluetoothAdapter.stopLeScan(leScanCallback);
                    scanStopRunnable = null;
                }
            };

            // Stops scanning after a pre-defined scan period.
            handler.postDelayed(scanStopRunnable, SCAN_PERIOD);

            scanning = true;
            bluetoothAdapter.startLeScan(leScanCallback);
        } else {
            scanning = false;
            bluetoothAdapter.stopLeScan(leScanCallback);
            if (scanStopRunnable != null) {
                handler.removeCallbacks(scanStopRunnable);
            }
        }

        updateProgressAndButton();
    }

    public void updateProgressAndButton() {
        if (progress != null) {
            progress.setVisibility(scanning ? View.VISIBLE : View.INVISIBLE);
        }

        if (button != null) {
            boolean enabled = bluetoothAdapter.isEnabled();
            button.setText(enabled ? R.string.ble_rescan : R.string.ble_enable);
            button.setVisibility(scanning ? View.INVISIBLE : View.VISIBLE);
        }
    }

    public void onBluetoothAdapterState(int state) {
        switch (state) {
        case BluetoothAdapter.STATE_OFF:
            scanLeDevice(false);
            break;
        case BluetoothAdapter.STATE_TURNING_OFF:
            // Ignore for now
            break;
        case BluetoothAdapter.STATE_ON:
            scanLeDevice(true);
            break;
        case BluetoothAdapter.STATE_TURNING_ON:
            // Ignore for now
            break;
        }
    }

    private void updateDevices() {
        List<Integer> previousIds = new ArrayList<>();
        Set<String> previous = prefs.getStringSet(SensorsRecorder.PREF_BLE_DEVICES, null);
        if (previous != null) {
            for (String address : previous) {
                previousIds.add(prefs.getInt(SensorsRecorder.PREF_BLE_ID_ + address, 0));
            }
        }

        SharedPreferences.Editor editor = prefs.edit();

        int lastId = -1;
        int lastIndex = 0;
        Set<String> devices = new HashSet<>();
        for (int i = 0; i < adapter.getCount(); ++i) {
            Device device = (Device) adapter.getItem(i);
            if (device.isEnabled()) {
                String address = device.getAddress();
                devices.add(address);
                if (previous == null || !previous.contains(address)) {
                    for (++lastId; lastIndex < previousIds.size(); ++lastIndex) {
                        if (previousIds.get(lastIndex) > lastId) {
                            break;
                        } else if (previousIds.get(lastIndex) == lastId) {
                            ++lastId;
                        }
                    }
                }
                editor.putInt(SensorsRecorder.PREF_BLE_ID_ + address, lastId);
                editor.putString(SensorsRecorder.PREF_BLE_NAME_ + address, device.getName());
            }
        }

        if (previous != null) {
            for (String address : previous) {
                if (!devices.contains(address)) {
                    editor.remove(SensorsRecorder.PREF_BLE_ID_ + address);
                    editor.remove(SensorsRecorder.PREF_BLE_NAME_ + address);
                }
            }
        }

        editor.putStringSet(SensorsRecorder.PREF_BLE_DEVICES, devices);
        editor.apply();
    }

    private class Device {
        String name;
        String address;
        boolean enabled;
        private boolean present;

        Device(BleRecorder recorder) {
            name = recorder.getName();
            address = recorder.getAddress();
            enabled = true;
            present = false;
        }

        Device(BluetoothDevice device) {
            name = device.getName();
            address = device.getAddress();
            enabled = false;
            present = true;
        }

        public String getName() {
            return name;
        }

        public String getAddress() {
            return address;
        }

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public boolean isPresent() {
            return present;
        }

        public boolean setPresent(boolean present) {
            if (this.present != present) {
                this.present = present;
                return true;
            }
            return false;
        }
    }

    private class DevicesAdapter extends BaseAdapter {

        private ArrayList<Device> devices;
        private Map<String, Long> deviceId;
        private Map<String, Device> deviceMap;
        private DeviceComparator deviceComparator;

        public DevicesAdapter() {
            devices = new ArrayList<>();
            deviceId = new HashMap<>();
            deviceMap = new HashMap<>();
            deviceComparator = new DeviceComparator();
        }

        public boolean add(BluetoothDevice device) {
            if (!deviceId.containsKey(device.getAddress())) {
                Device d = new Device(device);
                devices.add(d);
                deviceId.put(device.getAddress(), (long) devices.size());
                deviceMap.put(device.getAddress(), d);
                return true;
            } else {
                return deviceMap.get(device.getAddress()).setPresent(true);
            }
        }

        public boolean add(BleRecorder recorder) {
            if (!deviceId.containsKey(recorder.getAddress())) {
                Device d = new Device(recorder);
                devices.add(d);
                deviceId.put(recorder.getAddress(), (long) devices.size());
                deviceMap.put(recorder.getAddress(), d);
                return true;
            } else {
                return false;
            }
        }

        @Override
        public void notifyDataSetChanged() {
            Collections.sort(devices, deviceComparator);
            super.notifyDataSetChanged();
        }

        @Override
        public boolean hasStableIds() {
            return true;
        }

        @Override
        public int getCount() {
            return devices.size();
        }

        @Override
        public long getItemId(int position) {
            return deviceId.get(devices.get(position).getAddress());
        }

        @Override
        public Object getItem(int position) {
            return devices.get(position);
        }

        public Device getDevice(int position) {
            return devices.get(position);
        }

        @Override
        public View getView(int position, View view, ViewGroup parent) {
            DeviceViewHolder holder;

            if (view == null) {
                LayoutInflater inflater = getActivity().getLayoutInflater();
                view = inflater.inflate(R.layout.ble_device_layout, parent, false);
                holder = new DeviceViewHolder(view);
                view.setTag(holder);
            } else {
                holder = (DeviceViewHolder) view.getTag();
            }

            holder.setDevice(getDevice(position));

            return view;
        }
    }

    private class DeviceViewHolder implements CompoundButton.OnCheckedChangeListener {
        TextView nameText;
        TextView addressText;
        SwitchCompat enabledSwitch;

        Device device;

        DeviceViewHolder(View view) {
            nameText = (TextView) view.findViewById(R.id.name_text);
            addressText = (TextView) view.findViewById(R.id.address_text);
            enabledSwitch = (SwitchCompat) view.findViewById(R.id.enabled_switch);
            enabledSwitch.setOnCheckedChangeListener(this);
        }

        public void setDevice(Device device) {
            this.device = device;

            addressText.setText(device.getAddress());

            String name = device.getName();
            if (!TextUtils.isEmpty(name)) {
                nameText.setText(name);
            } else {
                nameText.setText(R.string.ble_unknown_device);
            }

            enabledSwitch.setChecked(device.isEnabled());

            if (device.isPresent()) {
                nameText.setTextColor(textColorPrimary);
                addressText.setTextColor(textColorSecondary);
            } else {
                addressText.setTextColor(textColorTertiary);
                nameText.setTextColor(textColorTertiary);
            }
        }

        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            device.setEnabled(isChecked);
        }
    }

    private static class DeviceComparator implements Comparator<Device> {

        @Override
        public int compare(Device lhs, Device rhs) {
            int c = 0;

            boolean lhsEmpty = TextUtils.isEmpty(lhs.getName());
            boolean rhsEmpty = TextUtils.isEmpty(lhs.getName());
            if (lhsEmpty || rhsEmpty) {
                c = (rhsEmpty ? 1 : 0) - (lhsEmpty ? 1 : 0);
            }

            if (c == 0) {
                c = lhs.getAddress().compareTo(rhs.getAddress());
            }

            return c;
        }
    }
}