/*
* Copyright (C) 2013 jonas.oreland@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.runnerup.view;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.jjoe64.graphview.DefaultLabelFormatter;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.series.DataPoint;
import com.jjoe64.graphview.series.LineGraphSeries;
import org.runnerup.R;
import org.runnerup.hr.HRData;
import org.runnerup.hr.HRDeviceRef;
import org.runnerup.hr.HRManager;
import org.runnerup.hr.HRProvider;
import org.runnerup.hr.HRProvider.HRClient;
import org.runnerup.util.Formatter;
import org.runnerup.widget.WidgetUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
@TargetApi(Build.VERSION_CODES.FROYO)
public class HRSettingsActivity extends AppCompatActivity implements HRClient {
private final Handler handler = new Handler();
private final StringBuffer logBuffer = new StringBuffer();
List<HRProvider> providers = null;
String btName;
String btAddress;
String btProviderName;
HRProvider hrProvider = null;
Button connectButton = null;
Button scanButton = null;
TextView tvBTName = null;
TextView tvHR = null;
TextView tvLog = null;
TextView tvBatteryLevel = null;
Formatter formatter = null;
GraphView graphView = null;
LineGraphSeries<DataPoint> graphViewSeries = null;
final ArrayList<DataPoint> graphViewListData = new ArrayList<>();
DataPoint graphViewArrayData[] = new DataPoint[0];
static final int GRAPH_HISTORY_SECONDS = 180;
DeviceAdapter deviceAdapter = null;
boolean mIsScanning = false;
final OnClickListener hrZonesClick = new OnClickListener() {
@Override
public void onClick(View arg0) {
startActivity(new Intent(HRSettingsActivity.this, HRZonesActivity.class));
}
};
final OnClickListener scanButtonClick = new OnClickListener() {
public void onClick(View v) {
clear();
stopTimer();
close();
mIsScanning = true;
log("select HR-provider");
selectProvider();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.hr_settings);
WidgetUtil.addLegacyOverflowButton(getWindow());
providers = HRManager.getHRProviderList(this);
deviceAdapter = new DeviceAdapter(this);
if (providers.isEmpty()) {
notSupported();
return;
}
tvLog = (TextView) findViewById(R.id.hr_log);
tvLog.setMovementMethod(new ScrollingMovementMethod());
tvBTName = (TextView) findViewById(R.id.hr_device);
tvHR = (TextView) findViewById(R.id.hr_value);
tvBatteryLevel = (TextView) findViewById(R.id.hr_battery);
tvBatteryLevel.setVisibility(View.GONE);
scanButton = (Button) findViewById(R.id.scan_button);
scanButton.setOnClickListener(scanButtonClick);
connectButton = (Button) findViewById(R.id.connect_button);
connectButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
connect();
}
});
formatter = new Formatter(this);
{
LinearLayout graphLayout = (LinearLayout) findViewById(R.id.hr_graph_layout);
graphView = new GraphView(this);
graphView.setTitle(getString(R.string.Heart_rate));
graphView.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() {
@Override
public String formatLabel(double value, boolean isValueX) {
if (isValueX) {
return formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, (long) value);
} else {
return formatter.formatHeartRate(Formatter.Format.TXT_SHORT, value);
}
}
});
graphLayout.addView(graphView);
}
load();
open();
}
@Override
public void onDestroy() {
super.onDestroy();
close();
stopTimer();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.hrsettings_menu, menu);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
Resources res = getResources();
boolean isChecked = prefs.getBoolean(res.getString(R.string.pref_bt_experimental), false);
MenuItem item = menu.findItem(R.id.menu_hrdevice_expermental);
item.setChecked(isChecked);
isChecked = prefs.getBoolean(res.getString(R.string.pref_bt_mock), false);
item = menu.findItem(R.id.menu_hrdevice_mock);
item.setChecked(isChecked);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_hrsettings_clear:
clearHRSettings();
return true;
case R.id.menu_hrzones:
hrZonesClick.onClick(null);
return true;
case R.id.menu_hrdevice_expermental:
case R.id.menu_hrdevice_mock:
boolean isChecked = !item.isChecked();
item.setChecked(isChecked);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
Resources res = getResources();
SharedPreferences.Editor editor = prefs.edit();
int key;
if (item.getItemId() == R.id.menu_hrdevice_expermental) {
key = R.string.pref_bt_experimental;
} else {
key = R.string.pref_bt_mock;
}
editor.putBoolean(res.getString(key), isChecked);
editor.commit();
providers = HRManager.getHRProviderList(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0) {
if (!hrProvider.isEnabled()) {
log("Bluetooth not enabled!");
scanButton.setEnabled(false);
connectButton.setEnabled(false);
return;
}
load();
open();
return;
}
if (requestCode == 123) {
startScan();
return;
}
}
int lineNo = 0;
private void log(String msg) {
logBuffer.insert(0, Integer.toString(++lineNo) + ": " + msg + "\n");
if (logBuffer.length() > 5000) {
logBuffer.setLength(5000);
}
tvLog.setText(logBuffer.toString());
}
void clearHRSettings() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.Clear_HR_settings));
builder.setMessage(getString(R.string.Are_you_sure));
builder.setPositiveButton(getString(R.string.OK), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
doClear();
}
});
builder.setNegativeButton(getString(R.string.Cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// Do nothing but close the dialog
dialog.dismiss();
}
});
builder.show();
}
private void load() {
Resources res = getResources();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
btName = prefs.getString(res.getString(R.string.pref_bt_name), null);
btAddress = prefs.getString(res.getString(R.string.pref_bt_address), null);
btProviderName = prefs.getString(res.getString(R.string.pref_bt_provider), null);
Log.e(getClass().getName(), "btName: " + btName);
Log.e(getClass().getName(), "btAddress: " + btAddress);
Log.e(getClass().getName(), "btProviderName: " + btProviderName);
if (btProviderName != null) {
log("HRManager.get(" + btProviderName + ")");
hrProvider = HRManager.getHRProvider(this, btProviderName);
}
}
private void open() {
if (hrProvider != null && !hrProvider.isEnabled()) {
if (hrProvider.startEnableIntent(this, 0) == true) {
return;
}
hrProvider = null;
}
if (hrProvider != null) {
log(hrProvider.getProviderName() + ".open(this)");
hrProvider.open(handler, this);
} else {
updateView();
}
}
private void close() {
if (hrProvider != null) {
log(hrProvider.getProviderName() + ".close()");
hrProvider.close();
hrProvider = null;
}
}
public void notSupported() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.Heart_rate_monitor_is_not_supported_for_your_device));
builder.setMessage(getString(R.string.try_again_later));
DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
};
builder.setNegativeButton(getString(R.string.ok_rats), listener);
builder.show();
return;
}
private void clear() {
btAddress = null;
btName = null;
btProviderName = null;
clearGraph();
}
private void clearGraph() {
graphView.removeAllSeries();
graphViewSeries = null;
graphViewListData.clear();
graphViewArrayData = new DataPoint[0];
}
private void updateView() {
if (hrProvider == null) {
scanButton.setEnabled(true);
connectButton.setEnabled(false);
connectButton.setText(getString(R.string.Connect));
tvBTName.setText("");
tvHR.setText("");
return;
}
if (btName != null) {
tvBTName.setText(btName);
} else {
tvBTName.setText("");
tvHR.setText("");
}
if (hrProvider.isConnected()) {
connectButton.setText(getString(R.string.Disconnect));
connectButton.setEnabled(true);
} else if (hrProvider.isConnecting()) {
connectButton.setEnabled(false);
connectButton.setText(getString(R.string.Connecting));
} else {
connectButton.setEnabled(btName != null);
connectButton.setText(getString(R.string.Connect));
}
}
private void selectProvider() {
final CharSequence items[] = new CharSequence[providers.size()];
final CharSequence itemNames[] = new CharSequence[providers.size()];
for (int i = 0; i < items.length; i++) {
items[i] = providers.get(i).getProviderName();
itemNames[i] = providers.get(i).getName();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.Select_type_of_Bluetooth_device));
builder.setPositiveButton(getString(R.string.OK),
new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, int which) {
open();
}
});
builder.setNegativeButton(getString(R.string.Cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
mIsScanning = false;
load();
open();
dialog.dismiss();
}
});
builder.setSingleChoiceItems(itemNames, -1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
hrProvider = HRManager.getHRProvider(HRSettingsActivity.this,
items[arg1].toString());
log("hrProvider = " + hrProvider.getProviderName());
}
});
builder.show();
}
private void startScan() {
log(hrProvider.getProviderName() + ".startScan()");
updateView();
deviceAdapter.deviceList.clear();
hrProvider.startScan();
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.Scanning));
builder.setPositiveButton(getString(R.string.Connect),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
log(hrProvider.getProviderName() + ".stopScan()");
hrProvider.stopScan();
connect();
updateView();
dialog.dismiss();
}
});
if (hrProvider.isBondingDevice()) {
builder.setNeutralButton("Pairing", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
Intent i = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivityForResult(i, 123);
}
});
}
builder.setNegativeButton(getString(R.string.Cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
log(hrProvider.getProviderName() + ".stopScan()");
hrProvider.stopScan();
load();
open();
dialog.dismiss();
updateView();
}
});
builder.setSingleChoiceItems(deviceAdapter, -1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
HRDeviceRef hrDevice = deviceAdapter.deviceList.get(arg1);
btAddress = hrDevice.getAddress();
btName = hrDevice.getName();
}
});
builder.show();
}
void connect() {
stopTimer();
if (hrProvider == null || btName == null || btAddress == null) {
updateView();
return;
}
if (hrProvider.isConnecting() || hrProvider.isConnected()) {
log(hrProvider.getProviderName() + ".disconnect()");
hrProvider.disconnect();
updateView();
return;
}
tvBTName.setText(getName());
tvHR.setText("?");
String name = btName;
if (name == null || name.length() == 0) {
name = btAddress;
}
log(hrProvider.getProviderName() + ".connect(" + name + ")");
hrProvider.connect(HRDeviceRef.create(btProviderName, btName, btAddress));
updateView();
}
private void save() {
Resources res = getResources();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Editor ed = prefs.edit();
ed.putString(res.getString(R.string.pref_bt_name), btName);
ed.putString(res.getString(R.string.pref_bt_address), btAddress);
ed.putString(res.getString(R.string.pref_bt_provider), hrProvider.getProviderName());
ed.commit();
}
private void doClear() {
Resources res = getResources();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Editor ed = prefs.edit();
ed.remove(res.getString(R.string.pref_bt_name));
ed.remove(res.getString(R.string.pref_bt_address));
ed.remove(res.getString(R.string.pref_bt_provider));
ed.commit();
}
private CharSequence getName() {
if (btName != null && btName.length() > 0)
return btName;
return btAddress;
}
Timer hrReader = null;
void startTimer() {
hrReader = new Timer();
hrReader.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
handler.post(new Runnable() {
public void run() {
readHR();
}
});
}
}, 0, 500);
}
void stopTimer() {
if (hrReader == null)
return;
hrReader.cancel();
hrReader.purge();
hrReader = null;
}
long lastTimestamp = 0;
long timerStartTime = 0;
protected void readHR() {
if (hrProvider != null) {
HRData data = hrProvider.getHRData();
if(data != null) {
long age = data.timestamp;
long hrValue = 0;
if(data.hasHeartRate)
hrValue = data.hrValue;
tvHR.setText(String.format(Locale.getDefault(), "%d", hrValue));
if (age != lastTimestamp) {
if (graphViewSeries == null) {
timerStartTime = System.currentTimeMillis();
DataPoint empty[] = {};
graphViewSeries = new LineGraphSeries<>(empty);
graphView.addSeries(graphViewSeries);
graphView.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() {
@Override
public String formatLabel(double value, boolean isValueX) {
if (isValueX) {
return formatter.formatDistance(Formatter.Format.TXT_SHORT, (long) value);
} else {
return formatter.formatHeartRate(Formatter.Format.TXT_SHORT, value);
}
}
});
}
graphViewListData.add(new DataPoint((age - timerStartTime) / 1000, hrValue));
while (graphViewListData.size() > GRAPH_HISTORY_SECONDS) {
graphViewListData.remove(0);
}
graphViewArrayData = graphViewListData.toArray(graphViewArrayData);
graphViewSeries.resetData(graphViewArrayData);
lastTimestamp = age;
}
}
}
}
@Override
public void onOpenResult(boolean ok) {
log(hrProvider.getProviderName() + "::onOpenResult(" + ok + ")");
if (mIsScanning) {
mIsScanning = false;
startScan();
return;
}
updateView();
}
@Override
public void onScanResult(HRDeviceRef device) {
log(hrProvider.getProviderName() + "::onScanResult(" + device.getAddress() + ", "
+ device.getName() + ")");
deviceAdapter.deviceList.add(device);
deviceAdapter.notifyDataSetChanged();
}
@Override
public void onConnectResult(boolean connectOK) {
log(hrProvider.getProviderName() + "::onConnectResult(" + connectOK + ")");
if (connectOK) {
save();
if (hrProvider.getBatteryLevel() > 0) {
tvBatteryLevel.setVisibility(View.VISIBLE);
tvBatteryLevel.setText(getResources().getText(R.string.Battery_level) + ": " + hrProvider.getBatteryLevel() + "%");
}
startTimer();
} else {
}
updateView();
}
@Override
public void onDisconnectResult(boolean disconnectOK) {
log(hrProvider.getProviderName() + "::onDisconnectResult(" + disconnectOK + ")");
}
@Override
public void onCloseResult(boolean closeOK) {
log(hrProvider.getProviderName() + "::onCloseResult(" + closeOK + ")");
}
@Override
public void log(HRProvider src, String msg) {
log(src.getProviderName() + ": " + msg);
}
class DeviceAdapter extends BaseAdapter {
final ArrayList<HRDeviceRef> deviceList = new ArrayList<HRDeviceRef>();
LayoutInflater inflater = null;
Resources resources = null;
DeviceAdapter(Context ctx) {
inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
resources = ctx.getResources();
}
@Override
public int getCount() {
return deviceList.size();
}
@Override
public Object getItem(int position) {
return deviceList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row;
if (convertView == null) {
row = inflater.inflate(android.R.layout.simple_list_item_single_choice,
null);
} else {
row = convertView;
}
TextView tv = (TextView) row.findViewById(android.R.id.text1);
//tv.setTextColor(resources.getColor(R.color.black));
HRDeviceRef btDevice = deviceList.get(position);
tv.setTag(btDevice);
tv.setText(btDevice.getName());
return tv;
}
}
}