package it.geosolutions.geocollect.android.core.wmc.ui; import it.geosolutions.geocollect.android.app.BuildConfig; import it.geosolutions.geocollect.android.app.R; import it.geosolutions.geocollect.android.core.form.FormEditActivity; import it.geosolutions.geocollect.android.core.form.FormPageFragment; import it.geosolutions.geocollect.android.core.mission.Mission; import it.geosolutions.geocollect.android.core.mission.utils.MissionUtils; import it.geosolutions.geocollect.android.core.wmc.bluetooth.BluetoothUtil; import it.geosolutions.geocollect.android.core.wmc.model.Configuration; import it.geosolutions.geocollect.android.core.wmc.model.WMCCommand; import it.geosolutions.geocollect.android.core.wmc.model.WMCReadResult; import it.geosolutions.geocollect.android.core.wmc.service.WMCService; import it.geosolutions.geocollect.android.core.wmc.service.events.RequestWMCDataEvent; import it.geosolutions.geocollect.android.core.wmc.service.events.WMCCommunicationResultEvent; import it.geosolutions.geocollect.android.core.wmc.service.events.WMCConnectionStateChangedEvent; import it.geosolutions.geocollect.android.core.wmc.util.TimeSlotInputFilter; import it.geosolutions.geocollect.model.config.MissionTemplate; import it.geosolutions.geocollect.model.viewmodel.Page; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManager.RunningServiceInfo; import android.app.AlertDialog; import android.app.ProgressDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.InputFilter; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; import android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; /** * Created by Robert Oehler on 05.11.16. * * WMC form is the main class to interact with a wmc device */ public class WMCForm extends SherlockFragment implements View.OnClickListener, AdapterView.OnItemSelectedListener, TextWatcher { private final static String TAG = "WMCForm"; private final static int REQUEST_ENABLE_BT = 321; public final static String WMC_DEVICE_PREFIX = "wmc"; public final static String ARG_INFLATED= "inflated"; public final static String ARG_CONNECTED = "connected"; public final static String ARG_RSSI = "rssi"; private static boolean debug = true; //for this UI only branch always use WMC mock private final static int POLLING_INTERVAL = 1000; private final static int FIRST_POLLING_DELAY = 1000; private final static int TIME_SLOT_MIN = 0; private final static int TIME_SLOT_MAX = 23; public final static String EXPORT_FILE_NAME = "wmc_configuration.xml"; //Spinner private Spinner deviceSpinner; private Spinner digitsSpinner; private Spinner weekdaySpinner; private Spinner typeSpinner; //Buttons private Button presetButton; private Button clearButton; private Button graphButton; private Button syncButton; private Button smsTestButton; private Button gsmOnButton; private Button connectButton; private ImageButton refreshButton; //EditTexts //Site private EditText sideCodeEt; private EditText currTimeEt; private EditText currDateEt; private EditText deliveryEt; //Overall private EditText overallTotalEt; private EditText overallTS1Et; private EditText overallTS2Et; private EditText overallPresetEt; //Counter private EditText counterTotalEt; private EditText counterTS1Et; private EditText counterTS2Et; //Sensor private EditText sensorLitresRoundEt; private EditText sensorLFConstEt; //Communication private EditText providerEt; private EditText pinCodeEt; private EditText smsRecipientEt; private EditText smsOriginEt; private EditText timeServerEt; //Time Slots private EditText timeSlot1StartEt; private EditText timeSlot1EndEt; private EditText timeSlot2StartEt; private EditText timeSlot2EndEt; //Extras private EditText gsmRssiEt; private EditText smsTestEt; //textviews private TextView versionTV; private TextView firmWareTV; private TextView statusTV; //model private Configuration currentConfiguration; private Mission mission; private Page page; private ProgressDialog progressDialog; private AlertDialog alertDialog; private Handler handler; private ArrayAdapter<BluetoothDevice> deviceAdapter; private BluetoothDevice selectedDevice; private SimpleDateFormat date_sdf = new SimpleDateFormat("dd/MM/yy", Locale.getDefault()); private SimpleDateFormat time_sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); private boolean connected = false; private boolean isPolling = true; private boolean inflated = false; private boolean recreated = false; private boolean didAskToEnableBluetooth = false; private int rssi = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { this.inflated = savedInstanceState.getBoolean(ARG_INFLATED); this.rssi = savedInstanceState.getInt(ARG_RSSI); } setHasOptionsMenu(true); mission = (Mission) getActivity().getIntent().getExtras().getSerializable(FormPageFragment.ARG_MISSION); if(getArguments() != null && getArguments().containsKey(FormPageFragment.ARG_OBJECT)){ Integer pageNumber = (Integer) getArguments().get(FormPageFragment.ARG_OBJECT); if(pageNumber!=null){ MissionTemplate t = MissionUtils.getDefaultTemplate(getActivity()); //if page number exists i suppose pages is not empty page = t.sop_form.pages.get(pageNumber); } } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.i(TAG,"onCreateView"); final View view = inflater.inflate(R.layout.wmc_layout,container, false); //spinner deviceSpinner = (Spinner) view.findViewById(R.id.device_spinner); digitsSpinner = (Spinner) view.findViewById(R.id.digits_spinner); weekdaySpinner = (Spinner) view.findViewById(R.id.weekday_spinner); typeSpinner = (Spinner) view.findViewById(R.id.type_spinner); //buttons presetButton = (Button) view.findViewById(R.id.button_preset); clearButton = (Button) view.findViewById(R.id.button_clear); graphButton = (Button) view.findViewById(R.id.button_graph); syncButton = (Button) view.findViewById(R.id.sync_button); smsTestButton = (Button) view.findViewById(R.id.sms_test_button); gsmOnButton = (Button) view.findViewById(R.id.gsm_on_button); connectButton = (Button) view.findViewById(R.id.connect_device_button); refreshButton = (ImageButton) view.findViewById(R.id.refresh_devices_button); //edittexts sideCodeEt = (EditText) view.findViewById(R.id.code_et); currTimeEt = (EditText) view.findViewById(R.id.curr_time_et); currDateEt = (EditText) view.findViewById(R.id.curr_date_et); deliveryEt = (EditText) view.findViewById(R.id.delivery_et); overallTotalEt = (EditText) view.findViewById(R.id.overall_total_et); overallTS1Et = (EditText) view.findViewById(R.id.overall_ts1_et); overallTS2Et = (EditText) view.findViewById(R.id.overall_ts2_et); overallPresetEt = (EditText) view.findViewById(R.id.preset_et); counterTotalEt = (EditText) view.findViewById(R.id.counter_total_et); counterTS1Et = (EditText) view.findViewById(R.id.counter_ts1_et); counterTS2Et = (EditText) view.findViewById(R.id.counter_ts2_et); sensorLitresRoundEt = (EditText) view.findViewById(R.id.hf_et); sensorLFConstEt = (EditText) view.findViewById(R.id.k_const_et); providerEt = (EditText) view.findViewById(R.id.provider_et); pinCodeEt = (EditText) view.findViewById(R.id.pin_code_et); smsRecipientEt = (EditText) view.findViewById(R.id.sms_rec_et); smsOriginEt = (EditText) view.findViewById(R.id.sms_origin_et); timeServerEt = (EditText) view.findViewById(R.id.time_server_et); final TimeSlotInputFilter filter = new TimeSlotInputFilter(TIME_SLOT_MIN,TIME_SLOT_MAX); timeSlot1StartEt = (EditText) view.findViewById(R.id.tslots_t1_b_et); timeSlot1StartEt.setFilters(new InputFilter[]{filter}); timeSlot1EndEt = (EditText) view.findViewById(R.id.tslots_t1_e_et); timeSlot1EndEt.setFilters(new InputFilter[]{filter}); timeSlot2StartEt = (EditText) view.findViewById(R.id.tslots_t2_b_et); timeSlot2StartEt.setFilters(new InputFilter[]{filter}); timeSlot2EndEt = (EditText) view.findViewById(R.id.tslots_t2_e_et); timeSlot2EndEt.setFilters(new InputFilter[]{filter}); gsmRssiEt = (EditText) view.findViewById(R.id.rssi_et); smsTestEt = (EditText) view.findViewById(R.id.sms_test_et); versionTV = (TextView) view.findViewById(R.id.version_tv); firmWareTV = (TextView) view.findViewById(R.id.firmware_tv); statusTV = (TextView) view.findViewById(R.id.state_tv); weekdaySpinner.setFocusable(false); presetButton.setOnClickListener(this); clearButton.setOnClickListener(this); graphButton.setOnClickListener(this); syncButton.setOnClickListener(this); smsTestButton.setOnClickListener(this); gsmOnButton.setOnClickListener(this); connectButton.setOnClickListener(this); refreshButton.setOnClickListener(this); EventBus.getDefault().register(this); //update the connection state this.connected = isServiceRunning(getActivity()); Log.i(TAG, "view recreated connection state is "+ (this.connected ? "connected" : "not connected")); //update the UI according to the current state changeState(this.connected, false); if(this.connected){ updatePairedDevices(); } if(!inflated){ inflated = true; }else{ recreated = true; } return view; } @Override public void onResume() { super.onResume(); final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if(bluetoothAdapter == null){ //this device does not have Bluetooth reportStatus(R.string.state_bt_not_available); connectButton.setEnabled(false); }else{ //is bluetooth enabled ? if(!bluetoothAdapter.isEnabled()){ if(!didAskToEnableBluetooth){ //need to enable, ask user startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT); didAskToEnableBluetooth = true; }else{ //asked earlier, did not want to enable connectButton.setEnabled(false); } }else{ if(isConnected()){ if(BuildConfig.DEBUG){ Log.d(TAG, "onResume starting polling"); } startPolling(); }else{ if(BuildConfig.DEBUG){ Log.d(TAG, "onResume not connected"); } if(recreated){ clearForm(); } updatePairedDevices(); startDeviceSpinnerListening(); } } } } @Override public void onPause() { super.onPause(); if(isConnected()){ if(BuildConfig.DEBUG){ Log.d(TAG, "onPause stopping polling"); } stopPolling(); }else{ if(BuildConfig.DEBUG){ Log.d(TAG, "onPause not connected"); } stopDeviceSpinnerListening(); } persistConfigToDB(); } @Override public void onDestroyView() { super.onDestroyView(); EventBus.getDefault().unregister(this); if(BuildConfig.DEBUG){ Log.d(TAG, "onDestroyView"); } if(alertDialog != null && alertDialog.isShowing()){ alertDialog.dismiss(); } if(progressDialog != null && progressDialog.isShowing()){ progressDialog.dismiss(); } } @Override public void onSaveInstanceState(Bundle outState) { persistConfigToDB(); outState.putBoolean(ARG_INFLATED, this.inflated); outState.putInt(ARG_RSSI, this.rssi); super.onSaveInstanceState(outState); } private void persistConfigToDB(){ if(mission != null && mission.db == null){ if(getActivity() instanceof FormEditActivity){ Log.d(TAG, "Connecting to Activity database"); mission.db = ((FormEditActivity)getActivity()).spatialiteDatabase; } }else if(mission == null){ Log.w(TAG, "mission null, aborting..."); return; } //get table String tableName = mission.getTemplate().id + MissionTemplate.DEFAULT_TABLE_DATA_SUFFIX; if(mission.getTemplate().schema_sop != null && mission.getTemplate().schema_sop.localFormStore != null && !mission.getTemplate().schema_sop.localFormStore.isEmpty()){ tableName = mission.getTemplate().schema_sop.localFormStore; } if(tableName == null || tableName.isEmpty()){ Log.w(TAG, "Empty tableName, aborting..."); return; } if(mission.db != null){ //TODO convert current config to String //persist String in table Log.i(TAG, "ready to persist wmc config"); }else{ Log.w(TAG, "Database not found, aborting..."); } } /** * starts listening to device spinner and refresh button * when not connected to a device */ private void startDeviceSpinnerListening(){ postSpinner(deviceSpinner); deviceSpinner.setEnabled(true); refreshButton.setVisibility(View.VISIBLE); } /** * stops listening to device spinner and refresh button * when connected to a device */ private void stopDeviceSpinnerListening(){ deviceSpinner.setOnItemSelectedListener(null); deviceSpinner.setEnabled(false); refreshButton.setVisibility(View.GONE); } /** * starts listening to spinner selected / editText text changed events * done when connected to a device */ private void startFormListening(){ postSpinner(typeSpinner); sideCodeEt.addTextChangedListener(this); sensorLitresRoundEt.addTextChangedListener(this); sensorLFConstEt.addTextChangedListener(this); timeSlot1StartEt.addTextChangedListener(this); timeSlot1EndEt.addTextChangedListener(this); timeSlot2StartEt.addTextChangedListener(this); timeSlot2EndEt.addTextChangedListener(this); providerEt.addTextChangedListener(this); pinCodeEt.addTextChangedListener(this); smsRecipientEt.addTextChangedListener(this); smsOriginEt.addTextChangedListener(this); timeServerEt.addTextChangedListener(this); } /** * stops listening to spinner selected / editText text changed events * done when disconnected to a device */ private void stopFormListening(){ typeSpinner.setOnItemSelectedListener(null); sideCodeEt.removeTextChangedListener(this); sensorLitresRoundEt.removeTextChangedListener(this); sensorLFConstEt.removeTextChangedListener(this); timeSlot1StartEt.removeTextChangedListener(this); timeSlot1EndEt.removeTextChangedListener(this); timeSlot2StartEt.removeTextChangedListener(this); timeSlot2EndEt.removeTextChangedListener(this); providerEt.removeTextChangedListener(this); pinCodeEt.removeTextChangedListener(this); smsRecipientEt.removeTextChangedListener(this); smsOriginEt.removeTextChangedListener(this); timeServerEt.removeTextChangedListener(this); } /** * adds the item selected listener to the spinner when the UI was fully inflated * this avoids that the onItemSelected event is fired with non-human events * @param spinner the spinner to add the listener to */ private void postSpinner(final Spinner spinner){ spinner.post(new Runnable() { @Override public void run() { spinner.setOnItemSelectedListener(WMCForm.this); } }); } private void startPolling(){ getHandler().removeCallbacks(mPollingTask); getHandler().postDelayed(mPollingTask, FIRST_POLLING_DELAY); } private void stopPolling(){ getHandler().removeCallbacks(mPollingTask); } /** /////////////// Form State ////////////////////////// */ public void changeState(final boolean connected, final boolean report){ changeFormMode(connected); deviceSpinner.setFocusable(!connected); refreshButton.setFocusable(!connected); if(connected){ //change form state to connected startFormListening(); stopDeviceSpinnerListening(); connectButton.setText(getString(R.string.button_disconnect)); //start polling isPolling = true; readWaterData(); startPolling(); }else{ //stop polling isPolling = false; stopPolling(); stopFormListening(); startDeviceSpinnerListening(); //change form state to disconnected connectButton.setText(getString(R.string.button_connect)); //clear clearForm(); //report if(report){ reportStatus(getString(R.string.state_disconnected)); } } getActivity().invalidateOptionsMenu(); } /** * changes the state of the form according to the @param isConnected mode * giving the possibility to use/edit the buttons spinners and editTexts * @param connected if a device is currently isConnected */ private void changeFormMode(final boolean connected){ //remove / add load/write conf option in menu getActivity().invalidateOptionsMenu(); if(!connected) { weekdaySpinner.setSelection(0); gsmRssiEt.setText("---"); } syncButton.setEnabled(connected); gsmOnButton.setEnabled(connected); //sms test button will only be enabled when rssi is received -> see readWaterData() if(this.rssi != 0){ smsTestButton.setEnabled(true); }else{ smsTestButton.setEnabled(false); } presetButton.setEnabled(connected); clearButton.setEnabled(connected); graphButton.setEnabled(connected); weekdaySpinner.setEnabled(connected); digitsSpinner.setEnabled(connected); typeSpinner.setEnabled(connected); //editable edit texts : sideCodeEt.setFocusable(connected); sideCodeEt.setFocusableInTouchMode(connected); sensorLitresRoundEt.setFocusable(connected); sensorLitresRoundEt.setFocusableInTouchMode(connected); sensorLFConstEt.setFocusable(connected); sensorLFConstEt.setFocusableInTouchMode(connected); timeSlot1StartEt.setFocusable(connected); timeSlot1StartEt.setFocusableInTouchMode(connected); timeSlot1EndEt.setFocusable(connected); timeSlot1EndEt.setFocusableInTouchMode(connected); timeSlot2StartEt.setFocusable(connected); timeSlot2StartEt.setFocusableInTouchMode(connected); timeSlot2EndEt.setFocusable(connected); timeSlot2EndEt.setFocusableInTouchMode(connected); //can also be edited providerEt.setFocusable(connected); providerEt.setFocusableInTouchMode(connected); pinCodeEt.setFocusable(connected); pinCodeEt.setFocusableInTouchMode(connected); smsRecipientEt.setFocusable(connected); smsRecipientEt.setFocusableInTouchMode(connected); smsOriginEt.setFocusable(connected); smsOriginEt.setFocusableInTouchMode(connected); timeServerEt.setFocusable(connected); timeServerEt.setFocusableInTouchMode(connected); smsTestEt.setFocusable(connected); smsTestEt.setFocusableInTouchMode(connected); } /** * populates the WMC form with a @param config * @param config the config to apply */ private void populateForm(final Configuration config){ //time slots timeSlot1StartEt.setText(String.format(Locale.getDefault(),"%d",config.timerSlot1Start)); timeSlot1EndEt.setText(String.format(Locale.getDefault(),"%d",config.timerSlot1Stop)); timeSlot2StartEt.setText(String.format(Locale.getDefault(),"%d",config.timerSlot2Start)); timeSlot2EndEt.setText(String.format(Locale.getDefault(),"%d",config.timerSlot2Stop)); //sensor type typeSpinner.setSelection(config.sensorType); sensorLitresRoundEt.setText(String.format(Locale.getDefault(),"%d",config.sensorLitresRound)); sensorLFConstEt.setText(String.format(Locale.getDefault(),"%d",config.sensor_LF_Const)); if (config.sensorType == 1) { //sensorType "HF & Dir" makes lf const non-editable sensorLFConstEt.setEnabled(false); } else { // with sensorType "LF" this const is editable sensorLFConstEt.setEnabled(true); } //Strings if(config.provider != null){ providerEt.setText(config.provider); } if (config.pinCode != null){ pinCodeEt.setText(config.pinCode); } if (config.recipientNum != null) { smsRecipientEt.setText(config.recipientNum); } if (config.ntpAddress != null) { timeServerEt.setText(config.ntpAddress); } if (config.originNum != null){ smsOriginEt.setText(config.originNum); } //site code sideCodeEt.setText(String.format(Locale.getDefault(),"%04d",config.siteCode)); updateDeliveryAccordingToSiteCode(config.siteCode); //digits if(config.version >= 0x200) { //enable and set according to config digitsSpinner.setEnabled(true); if (config.digits == 6) { digitsSpinner.setSelection(0); } else if (config.digits == 7){ digitsSpinner.setSelection(1); } else { digitsSpinner.setSelection(2); } }else{ //disable, allow 6 digitsSpinner.setEnabled(false); digitsSpinner.setSelection(0); } //week day index , select TODAY weekdaySpinner.setSelection(0); //version and firmware firmWareTV.setText(String.format(Locale.getDefault(),"F.#%s",Integer.toString(config.version, 16))); String versionCode = "1.0"; try { versionCode = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName; } catch (NameNotFoundException e) { Log.e(TAG, "error getting current version name",e); } versionTV.setText(String.format(Locale.getDefault(),"V.%s", versionCode)); } /** * clears the form */ private void clearForm(){ //time slots timeSlot1StartEt.setText(""); timeSlot1EndEt.setText(""); timeSlot2StartEt.setText(""); timeSlot2EndEt.setText(""); //sensor type sensorLitresRoundEt.setText(""); sensorLFConstEt.setText(""); //"Strings" providerEt.setText(""); pinCodeEt.setText(""); smsRecipientEt.setText(""); timeServerEt.setText(""); smsOriginEt.setText(""); //site code sideCodeEt.setText(""); deliveryEt.setText(""); overallTotalEt.setText(""); overallTS1Et.setText(""); overallTS2Et.setText(""); counterTotalEt.setText(""); counterTS1Et.setText(""); counterTS2Et.setText(""); //time update currDateEt.setText(""); currTimeEt.setText(""); gsmRssiEt.setText("---"); overallPresetEt.setText(""); //version and firmware firmWareTV.setText(""); } private boolean updateDeliveryAccordingToSiteCode(final int site_Code){ if (site_Code > 2999) { reportStatus(getString(R.string.state_site_code_invalid)); sideCodeEt.setText(""); return false; } final String[] weekdWithTODAY = getResources().getStringArray(R.array.weekday_values); //remove "TODAY" final String[] weekd = Arrays.copyOfRange(weekdWithTODAY, 1, weekdWithTODAY.length); //algorithm taken as it is from Windows source code Form1.cs line 1134 ff. boolean done = false; int count = site_Code; int h1 = 0; int d1 = 0; int h2; int d2; int wday_b; int hour_b; int wday_e; int hour_e; while (!done) { count -= 10; if (count >= 0) { h1++; if (h1 == 23) { h1 = 0; d1++; if (d1 == 7) d1 = 0; } } else { done = true; } } wday_b = d1; hour_b = h1; h2 = (char)(h1 + 1); d2 = d1; if (h2 >= 24) { h2 = 0; d2++; } wday_e = d2; hour_e = h2; String delivery = String.format(Locale.US,"%s,%d - %s,%d",weekd[wday_b], hour_b, weekd[wday_e], hour_e); deliveryEt.setText(delivery); return true; } /** * update the paired devices spinner */ private void updatePairedDevices() { BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if(bluetoothAdapter == null){ //no bluetooth available reportStatus(R.string.state_bt_not_available); } else { //get currently paired devices //TODO apply filtering ? final ArrayList<BluetoothDevice> pairedDevices = BluetoothUtil.getPairedDevices(WMC_DEVICE_PREFIX); if (pairedDevices != null && pairedDevices.size() > 0) { getBluetoothDeviceAdapter().clear(); getBluetoothDeviceAdapter().addAll(pairedDevices); getBluetoothDeviceAdapter().setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); deviceSpinner.setAdapter(getBluetoothDeviceAdapter()); //as in the windows application (Form1.cs line 241) we assume the first of the available devices as being currently selected selectedDevice = pairedDevices.get(0); } else { reportStatus(R.string.state_bt_none_available); } } } /** ////////////// Spinner selection listener//////////////// */ @Override public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { Log.i(TAG, "Spinner item selected " + position); if(spinner.equals(deviceSpinner)){ final BluetoothDevice device = deviceAdapter.getItem(deviceSpinner.getSelectedItemPosition()); String deviceName = device.getName() == null ? "null " : device.getName(); statusTV.setText(getString(R.string.state_device_selected,deviceName)); selectedDevice = device; }else if(spinner.equals(typeSpinner)){ if(currentConfiguration != null) { if(BuildConfig.DEBUG) { Log.i(TAG, "sensor type changed to " + typeSpinner.getSelectedItemPosition() + " which is " + typeSpinner.getSelectedItem().toString()); } currentConfiguration.sensorType = typeSpinner.getSelectedItemPosition(); //enable/disable const editText according to device type sensorLFConstEt.setEnabled(currentConfiguration.sensorType != 1); reportStatus(R.string.state_sensor_type_changed); } } } @Override public void onNothingSelected(AdapterView<?> parent) { } /** ////////////// EditText text changed watcher///////////////// */ @Override public void afterTextChanged(Editable s) { if(currentConfiguration == null){ return; } if(s.length() == 0){ if(BuildConfig.DEBUG) { Log.i(TAG, "changed to length 0, returning"); } return; } //identifying sender according to the hashCode of the senders content @source http://stackoverflow.com/questions/4283062/textwatcher-for-more-than-one-edittext if(sideCodeEt.getText().hashCode() == s.hashCode()){ try{ int newSiteCode = Integer.parseInt(sideCodeEt.getText().toString()); if(updateDeliveryAccordingToSiteCode(newSiteCode)){ currentConfiguration.siteCode = newSiteCode; } }catch (NumberFormatException e){ reportStatus(getString(R.string.state_incorrect_input)); } }else if(sensorLitresRoundEt.getText().hashCode() == s.hashCode()){ try{ currentConfiguration.sensorLitresRound = Integer.parseInt(sensorLitresRoundEt.getText().toString()); }catch (NumberFormatException e){ reportStatus(getString(R.string.state_incorrect_input)); } }else if(sensorLFConstEt.getText().hashCode() == s.hashCode()){ try{ currentConfiguration.sensor_LF_Const = Integer.parseInt(sensorLFConstEt.getText().toString()); }catch (NumberFormatException e){ reportStatus(getString(R.string.state_incorrect_input)); } }else if(timeSlot1StartEt.getText().hashCode() == s.hashCode()){ validateTimeSlot(timeSlot1StartEt, true); }else if(timeSlot1EndEt.getText().hashCode() == s.hashCode()){ validateTimeSlot(timeSlot1EndEt, false); }else if(timeSlot2StartEt.getText().hashCode() == s.hashCode()){ validateTimeSlot(timeSlot2StartEt, true); }else if(timeSlot2EndEt.getText().hashCode() == s.hashCode()){ validateTimeSlot(timeSlot2EndEt, false); }else if(providerEt.getText().hashCode() == s.hashCode()){ if(TextUtils.isEmpty(providerEt.getText().toString()) || providerEt.getText().toString().length() > Configuration.BYTES_MAX_PROVIDER){ reportStatus(getString(R.string.state_incorrect_input)); return; } currentConfiguration.provider = providerEt.getText().toString(); }else if(pinCodeEt.getText().hashCode() == s.hashCode()){ if(TextUtils.isEmpty(pinCodeEt.getText().toString()) || pinCodeEt.getText().toString().length() > 5){ reportStatus(getString(R.string.state_incorrect_input)); return; } currentConfiguration.pinCode = pinCodeEt.getText().toString(); }else if(smsRecipientEt.getText().hashCode() == s.hashCode()){ if(TextUtils.isEmpty(smsRecipientEt.getText().toString()) || smsRecipientEt.getText().toString().length() > Configuration.BYTES_MAX_RECIPIENT){ reportStatus(getString(R.string.state_incorrect_input)); return; } currentConfiguration.recipientNum = smsRecipientEt.getText().toString(); }else if(smsOriginEt.getText().hashCode() == s.hashCode()){ if(TextUtils.isEmpty(smsOriginEt.getText().toString()) || smsOriginEt.getText().toString().length() > Configuration.BYTES_MAX_ORIGINNUM){ reportStatus(getString(R.string.state_incorrect_input)); return; } currentConfiguration.originNum = smsOriginEt.getText().toString(); }else if(timeServerEt.getText().hashCode() == s.hashCode()){ if(TextUtils.isEmpty(timeServerEt.getText().toString()) || timeServerEt.getText().toString().length() > Configuration.BYTES_MAX_NTPADDRESS){ reportStatus(getString(R.string.state_incorrect_input)); return; } currentConfiguration.ntpAddress = timeServerEt.getText().toString(); } } /** * validates a new value for a time slot edittext * @param ed the editText to validate * @param isStart if it is a start slot */ private void validateTimeSlot(final EditText ed, final boolean isStart){ final String name = isStart ? getString(R.string.wmc_ts_1) : getString(R.string.wmc_ts_2); //empty ? if (TextUtils.isEmpty(ed.getText().toString())) { statusTV.setText(isStart ? getString(R.string.state_time_slot_start_null, name) : getString(R.string.state_time_slot_end_null, name)); return; } try{ //valid ? due to filtering @TimeSlotInputFilter this should not be necessary, but better double check final int newValue = Integer.parseInt(ed.getText().toString()); if(newValue > TIME_SLOT_MAX || newValue < TIME_SLOT_MIN){ statusTV.setText(isStart ? getString(R.string.state_time_slot_start_oob, name) : getString(R.string.state_time_slot_end_oob, name)); return; } //valid if(ed.hashCode() == timeSlot1StartEt.hashCode()) { currentConfiguration.timerSlot1Start = newValue; }else if(ed.hashCode() == timeSlot1EndEt.hashCode()){ currentConfiguration.timerSlot1Stop = newValue; }else if(ed.hashCode() == timeSlot2StartEt.hashCode()){ currentConfiguration.timerSlot2Start = newValue; }else if(ed.hashCode() == timeSlot2EndEt.hashCode()){ currentConfiguration.timerSlot2Stop = newValue; } statusTV.setText(isStart ? getString(R.string.state_time_slot_start_edited, name, newValue) : getString(R.string.state_time_slot_end_edited, name, newValue)); }catch (NumberFormatException e){ statusTV.setText(getString(R.string.state_incorrect_input)); } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } /** * ////////////// Click listener for all buttons//////////////// * * In the windows app buttons are disabled during communication with a wmc. * In Android we do the communication in background and show meanwhile a progress view, * hence button deactivation is not necessary as the user cannot click another button */ @Override public void onClick(View v) { if(v.equals(connectButton)){ //already connected ? if(isConnected()){ // --> disconnect disconnect(true); } else { // --> connect //not connected yet, do we have a device ? if(selectedDevice == null){ reportStatus(R.string.state_bt_none_selected); } else { //okay, connect showProgressDialog(); startService(selectedDevice, debug); } } }else if(v.equals(gsmOnButton)){ if(isConnected()){ executeWMCCommand(WMCCommand.RSSI); }else{ reportStatus(R.string.state_not_connected); } }else if(v.equals(smsTestButton)){ final String phone = smsTestEt.getText().toString(); if(!TextUtils.isEmpty(phone)){ //is not empty, also valid ? if(phone.length() == 13 && android.util.Patterns.PHONE.matcher(phone).matches()){ //looks valid, go executeWMCCommand(WMCCommand.TEST_SMS, phone); }else{ reportStatus(R.string.state_test_sms_invalid); } }else{ reportStatus(R.string.state_recipient_null); } }else if(v.equals(syncButton)){ if(isConnected()){ executeWMCCommand(WMCCommand.WRITE_TIME); }else{ reportStatus(R.string.state_not_connected); } }else if(v.equals(presetButton)){ final String preset = overallPresetEt.getText().toString(); if(!TextUtils.isEmpty(preset)){ //validate double limit_lo; double limit_hi; String s; if(digitsSpinner.getSelectedItemPosition() == 0 || currentConfiguration.version < 0x200){ limit_hi = 999999.99; limit_lo = -999999.99; }else if (digitsSpinner.getSelectedItemPosition() == 1){ limit_hi = 9999999.99; limit_lo = -9999999.99; } else { limit_hi = 99999999.99; limit_lo = -99999999.99; } s = getString(R.string.state_preset_failure,limit_lo,limit_hi); try{ double value = Double.parseDouble(preset); if (value > limit_hi || value < limit_lo){ reportStatus(s); }else{ executeWMCCommand(WMCCommand.PRESET, preset); } }catch (NumberFormatException e){ Log.e(TAG, "exception parsing preset "+ preset, e ); reportStatus(R.string.state_incorrect_input); } }else{ reportStatus(R.string.state_incorrect_input); } } else if(v.equals(clearButton)){ if(isConnected()){ final int week_day_index = weekdaySpinner.getSelectedItemPosition(); executeWMCCommand(WMCCommand.CLEAR_COUNTER, Integer.toString(week_day_index)); }else{ reportStatus(R.string.state_not_connected); } } else if(v.equals(refreshButton)){ updatePairedDevices(); } } /** * Starts the WMC service * @param the device to connect to * @param useMock if to use a mock connection */ private void startService(final BluetoothDevice device, final boolean useMock){ Intent serviceIntent = new Intent(getActivity(), WMCService.class); serviceIntent.putExtra(WMCService.PARAM_DEVICE, device); serviceIntent.putExtra(WMCService.PARAM_DEBUG, useMock); getActivity().startService(serviceIntent); } /** * when a configuration was read this populates the form with the new configuration */ public void onConfigurationRead(final Configuration configuration, final String name){ if (configuration != null) { currentConfiguration = configuration; //pause listening to form events during the new configuration is applied stopFormListening(); populateForm(configuration); startFormListening(); statusTV.setText(getString(R.string.state_read_success, name)); } else { //config could not be read statusTV.setText(R.string.state_comm_error); } } /** * disconnects from the current connected wmc * and changed the form into not listening mode * and clears its content * @param disconnectListener listener to be informed when disconnecting async */ public void disconnect(boolean withUI){ Log.i(TAG, "disconnect"); //disconnect changeState(false, withUI); if(withUI){ showProgressDialog(); }else{ //wont receive events this.connected = false; } executeWMCCommand(WMCCommand.DISCONNECT); } /////////////// COMMUNICATION////////////////// /** * reads (water meter) data from the device and updates the UI * this is done frequently using POLLING_INTERVAL and therefore * checks if the last task finished which will nullify the reference to the task */ private void readWaterData(){ if(isPolling && isConnected()){ EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.READ_WATER, null)); } } /** * executes a command to the wmc * @param command the command to execute * @param args arguments which may be necessary e.g. the telephone number for test sms */ private void executeWMCCommand(final WMCCommand command, final String... args){ isPolling = false; showProgressDialog(); switch (command){ case READ_CONFIG: EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.READ_CONFIG, null)); break; case WRITE_CONFIG: EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.WRITE_CONFIG, currentConfiguration)); break; case READ_WATER: //is handled separately break; case DISCONNECT: EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.DISCONNECT, null)); break; case RSSI: //currently this is always an "on" request //no "off" in Windows source code EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.RSSI, true)); break; case TEST_SMS: if(args == null || args.length < 1 || args[0] == null){ Log.w(TAG, "No telephone number provided for test sms"); return; } EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.TEST_SMS, args[0])); break; case WRITE_TIME: EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.WRITE_TIME, null)); break; case PRESET: if(args == null || args.length < 1 || args[0] == null){ Log.w(TAG, "No preset provided"); return; } EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.PRESET, args[0])); break; case CLEAR_COUNTER: if(args == null || args.length < 1 || args[0] == null){ Log.w(TAG, "No clear week day parameter provided"); return; } EventBus.getDefault().post(new RequestWMCDataEvent(WMCCommand.CLEAR_COUNTER, args[0])); break; } } /** * receives the result of a data request from the service * @param event */ @Subscribe public void onEvent(WMCCommunicationResultEvent event){ hideProgress(); switch (event.getCommand()){ case READ_CONFIG: if(event.isSuccess()){ final Configuration configuration = event.getConfiguration(); if(configuration != null){ onConfigurationRead(configuration, event.getDeviceName()); }else{ reportStatus(R.string.state_read_failure); } }else{ reportStatus(R.string.state_read_failure); } break; case WRITE_CONFIG: if (event.isSuccess()) { showAlertDialog(getString(R.string.state_write_success)); } else { reportStatus(R.string.state_write_failure); } break; case READ_WATER: if(!event.isSuccess()) { statusTV.setText(R.string.state_comm_error); Log.w(TAG, "error polling"); }else{ final WMCReadResult readResult = event.getReadResult(); overallTotalEt.setText(String.format(Locale.US, "%.2f", readResult.overall_total)); overallTS1Et.setText(String.format(Locale.US, "%.2f", readResult.overall_Slot1)); overallTS2Et.setText(String.format(Locale.US, "%.2f", readResult.overall_Slot2)); final int week_day_index = weekdaySpinner.getSelectedItemPosition(); counterTotalEt.setText(String.format(Locale.US, "%.2f", readResult.week_total[week_day_index])); counterTS1Et.setText(String.format(Locale.US, "%.2f", readResult.week_Slot1[week_day_index])); counterTS2Et.setText(String.format(Locale.US, "%.2f", readResult.week_Slot2[week_day_index])); //time update currDateEt.setText(String.format(Locale.getDefault(), "%s", date_sdf.format(readResult.date))); currTimeEt.setText(String.format(Locale.getDefault(), "%s", time_sdf.format(readResult.date))); //update the result of the rssi request if (readResult.rssi == 0 || readResult.rssi == 99) { gsmRssiEt.setText("---"); } else { gsmRssiEt.setText(String.format(Locale.US, "%d", readResult.rssi)); smsTestButton.setEnabled(true); this.rssi = readResult.rssi; } } break; case RSSI: if(event.isSuccess()){ reportStatus(R.string.state_gms_request_sent); }else{ reportStatus(R.string.state_comm_error); } break; case TEST_SMS: if(event.isSuccess()){ reportStatus(R.string.state_test_sms_sent); }else{ reportStatus(R.string.state_comm_error); } break; case WRITE_TIME: if(event.isSuccess()){ reportStatus(R.string.state_sync_success); }else{ reportStatus(R.string.state_sync_failure); } break; case PRESET: if(event.isSuccess()){ reportStatus(R.string.state_preset_success); }else{ reportStatus(R.string.state_comm_error); } break; case CLEAR_COUNTER: if(event.isSuccess()){ reportStatus(R.string.state_clear_success); }else{ reportStatus(R.string.state_comm_error); } break; case DISCONNECT: //nothing, this is done by state listener break; } //event handling done, allow polling if this was not a disconnect event if(event.getCommand() != WMCCommand.DISCONNECT){ isPolling = true; } } /** * validates the current config and sends it to the device */ public void sendConfigToDevice(){ //TODO implement "real" validation //pair.first contains validation result (success), pair.second the error message if validation failed final Pair<Boolean,String> validationResult = new Pair<Boolean, String>(true, null); if(isConnected() && validationResult.first) { executeWMCCommand(WMCCommand.WRITE_CONFIG); }else if(validationResult.second != null){ reportStatus(validationResult.second); }else if(!isConnected()){ Log.w(TAG, "send config to device should not be available when not connected to a device"); } } /** * reads the current configuration from the device */ public void readConfigFromDevice(){ if(isConnected()){ executeWMCCommand(WMCCommand.READ_CONFIG); }else{ Log.w(TAG, "read config from device should not be available when not connected to a device"); } } /** * connection state event receiver coming from service * * @param event new state */ @Subscribe public void onEvent(WMCConnectionStateChangedEvent event){ this.connected = event.isConnected(); switch (event.getState()){ case CONNECTED: hideProgress(); changeState(true, true); //connected, request config executeWMCCommand(WMCCommand.READ_CONFIG); break; case DISCONNECTED: if(getActivity() == null || getActivity().isFinishing()){ //no need to update ui when we're closing return; } hideProgress(); changeState(false, true); break; case CONNECTION_ERROR: hideProgress(); reportStatus(getString(R.string.state_connect_failure, selectedDevice.getName())); break; } } /////////////// Utils ////////////////// /** * shows a progress view */ private void showProgressDialog() { try{ if(progressDialog == null) { LayoutInflater inflater = LayoutInflater.from(getActivity()); @SuppressLint("InflateParams") View progress = inflater.inflate(R.layout.progress_layout, null); progressDialog = new ProgressDialog(getActivity()); progressDialog.setView(progress); } progressDialog.setMessage(getString(R.string.progress_please_wait)); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setCancelable(false); progressDialog.show(); }catch(Exception e){ Log.e(TAG, "error showing progress",e); } } /** * hides the progress view */ public void hideProgress(){ try { if (progressDialog != null && progressDialog.isShowing()) { progressDialog.dismiss(); } } catch (Exception e) { Log.e(TAG, "error hiding progress", e); } } /** * shows a generic alert dialog * @param text message to show */ protected void showAlertDialog(final String text){ if(alertDialog == null) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setCancelable(false); builder.setTitle(R.string.app_name); builder.setMessage(text); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { alertDialog.dismiss(); } }); alertDialog = builder.create(); alertDialog.setCanceledOnTouchOutside(false); }else{ alertDialog.setMessage(text); } alertDialog.show(); } /** * shows a dialog asking the user if he wants to disconnect the device * @param context context to show dialog * @param listener to inform subscribers that the user selected disconnection */ public void showAskForDisconnectDialog(final Context context, final OnDisconnectListener listener){ AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setCancelable(false); builder.setTitle(R.string.app_name); builder.setMessage(R.string.state_active_connection); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); disconnect(false); if(listener != null){ listener.onDisconnect(); } } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); AlertDialog alertDialog = builder.create(); alertDialog.setCanceledOnTouchOutside(false); alertDialog.show(); } /** * array adapter for Bluetooth devices * @return the adapter */ private ArrayAdapter<BluetoothDevice> getBluetoothDeviceAdapter(){ if(deviceAdapter == null){ deviceAdapter = new ArrayAdapter<BluetoothDevice>(getActivity(), android.R.layout.simple_spinner_item, new ArrayList<BluetoothDevice>()){ @Override public View getView(int position, View convertView, ViewGroup parent) { return getViewForItem(position, convertView); } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { return getViewForItem(position, convertView); } private View getViewForItem(int position, View convertView){ View view; if (convertView == null) { view = View.inflate(getActivity(), android.R.layout.simple_spinner_item, null); } else { view = convertView; } final BluetoothDevice device = getItem(position); /** * on windows the display name consists of serial port - bluetooth id - wmc id * on Android exist only the latter ones - address and name */ //TODO should the device name be built ??? ((TextView) view).setText(String.format(Locale.getDefault(),"%s - %s",device.getAddress(), device.getName() == null ? "null" : device.getName())); return view; } }; } return deviceAdapter; } /** * task to poll data from the device, * repeats execution after POLLING_INTERVAL */ private Runnable mPollingTask = new Runnable() { public void run() { if(isConnected() && isPolling) { readWaterData(); } getHandler().postDelayed(this, POLLING_INTERVAL); } }; private Handler getHandler() { if(handler == null){ handler = new Handler(); } return handler; } protected void reportStatus(int stringResource){ this.reportStatus(getString(stringResource)); } /** * reports messages to the user, currently to both * -statsTV and * -alerdialog * * @param string message to report */ protected void reportStatus(String string){ statusTV.setText(string); showAlertDialog(string); } /** * applies the new config by setting it as the current config * and populating the UI using it * @param newConfig the new conf */ public void applyNewConfig(final Configuration newConfig){ this.currentConfiguration = newConfig; populateForm(currentConfiguration); } public Configuration getCurrentConfiguration(){ return currentConfiguration; } public boolean isConnected() { return connected; } public boolean isServiceRunning(Context context) { ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if ("it.geosolutions.geocollect.android.core.wmc.service.WMCService".equals(service.service.getClassName())) { return true; } } return false; } public void setDebug(final boolean debug){ this.debug = debug; } public void setTest(final boolean test){ EventBus.getDefault().unregister(this); } public interface OnDisconnectListener { public void onDisconnect(); } }