diff --git a/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/MainActivity.kt b/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/MainActivity.kt index 7368a9a..b17a167 100644 --- a/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/MainActivity.kt +++ b/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/MainActivity.kt @@ -147,106 +147,114 @@ class MainActivity : AppCompatActivity(), AnkoLogger { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val fields = arrayListOf() + // editText, size + val fields = arrayListOf>() initConfigurations() - verticalLayout { - padding = dip(20) + with(config.params) { + verticalLayout { + padding = dip(20) - textView { - text = config.params.name.toUpperCase() - bottomPadding = dip(5) - typeface = Typeface.DEFAULT_BOLD - setTextSize(TypedValue.COMPLEX_UNIT_DIP, 24f) - isFocusableInTouchMode = true - singleLine = true - ellipsize = TextUtils.TruncateAt.END - } + // config name + textView { + text = name.toUpperCase() + bottomPadding = dip(5) + typeface = Typeface.DEFAULT_BOLD + setTextSize(TypedValue.COMPLEX_UNIT_DIP, 24f) + isFocusableInTouchMode = true + singleLine = true + ellipsize = TextUtils.TruncateAt.END + } - textInputLayout { - horizontalPadding = dip(40) - val phone = editText() { - lparams(width = matchParent) - inputType = InputType.TYPE_CLASS_PHONE - hint = getString(R.string.hint_phone_number) + // phone + textInputLayout { + horizontalPadding = dip(40) + val edtPhone = editText() { + lparams(width = matchParent) + inputType = InputType.TYPE_CLASS_PHONE + hint = getString(R.string.hint_phone_number) - if (config.params.phone.isNotBlank()) { - setText(config.params.phone) + if (phone.isNotBlank()) { + setText(phone) + } + + setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_call_black_24dp, 0) + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + phone = (view as EditText).text.toString() + saveConfig() + } + } + } + fields.add(Pair(edtPhone, 0)) + } + + // master code + textInputLayout { + horizontalPadding = dip(40) + val edtMasterCode = editText() { + lparams(width = matchParent) + inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + hint = getString(R.string.hint_master_code) + filters = arrayOf(InputFilter.LengthFilter(size)) + setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_verified_user_black_24dp, 0) + imeOptions = EditorInfo.IME_ACTION_DONE + + if (master.isNotBlank()) { + setText(master) + } + + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + master = (view as EditText).text.toString() + saveConfig() + } + } + + setOnEditorActionListener { v, id, event -> + if (id == EditorInfo.IME_ACTION_DONE) { + clearFocus() + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(windowToken, 0) + true + } else { + false + } + } + } + fields.add(Pair(edtMasterCode, size)) + } + + // programming title + textView { + topPadding = dip(10) + text = getString(R.string.programming_heading) + setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f) + typeface = Typeface.DEFAULT_BOLD + }.lparams(width = matchParent) + + // options list + listView { + val opts = config.opts.sorted() + val titles = arrayListOf() + opts.all { + titles.add(it.title) } - setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_call_black_24dp, 0) - setOnFocusChangeListener { view, hasFocus -> - if (!hasFocus) { - config.params.phone = (view as EditText).text.toString() + adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_list_item_1, titles) + isTextFilterEnabled = true + isScrollbarFadingEnabled = false + onItemClickListener = AdapterView.OnItemClickListener { parent, v, position, id -> + if (validateFields(fields)) { saveConfig() + startActivity( + "net.thauvin.erik.android.tesremoteprogrammer.models.Params" to config.params, + "net.thauvin.erik.android.tesremoteprogrammer.models.Option" to opts[position]) } } - } - fields.add(phone) + }.lparams(width = matchParent) } - - textInputLayout { - horizontalPadding = dip(40) - val masterCode = editText() { - lparams(width = matchParent) - inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD - hint = getString(R.string.hint_master_code) - filters = arrayOf(InputFilter.LengthFilter(config.params.size)) - setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_verified_user_black_24dp, 0) - imeOptions = EditorInfo.IME_ACTION_DONE - - if (config.params.master.isNotBlank()) { - setText(config.params.master) - } - - setOnFocusChangeListener { view, hasFocus -> - if (!hasFocus) { - config.params.master = (view as EditText).text.toString() - saveConfig() - } - } - - setOnEditorActionListener { v, id, event -> - if (id == EditorInfo.IME_ACTION_DONE) { - clearFocus() - val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) - true - } else { - false - } - } - } - fields.add(masterCode) - } - - textView { - topPadding = dip(10) - text = getString(R.string.programming_heading) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f) - typeface = Typeface.DEFAULT_BOLD - }.lparams(width = matchParent) - - val opts = config.opts.sorted() - val titles = arrayListOf() - opts.all { - titles.add(it.title) - } - - listView { - adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_list_item_1, titles) - isTextFilterEnabled = true - isScrollbarFadingEnabled = false - onItemClickListener = AdapterView.OnItemClickListener { parent, v, position, id -> - if (validateFields(fields, config.params.size)) { - saveConfig() - startActivity( - "net.thauvin.erik.android.tesremoteprogrammer.models.Params" to config.params, - "net.thauvin.erik.android.tesremoteprogrammer.models.Option" to opts[position]) - } - } - }.lparams(width = matchParent) } } @@ -347,105 +355,203 @@ class MainActivity : AppCompatActivity(), AnkoLogger { val len = errors.length with(config) { - if (params.name.isBlank()) { - errors.append(getString(R.string.validate_missing_param, "name")) - } - - if (params.type.isBlank()) { - errors.append(getString(R.string.validate_missing_param, "type")) - } else if (!Dtmf.isValidType(params.type)) { - errors.append(getString(R.string.validate_invalid_param, "type")) - } - - if (params.size < 1) { - errors.append(getString(R.string.validate_invalid_param, "size")) - } - - if (params.ack.isBlank()) { - errors.append(getString(R.string.validate_missing_param, "ack")) + // params + with(params) { + // name + if (name.isBlank()) { + errors.append(getString(R.string.validate_missing_param, "name")) + } + + // type + if (type.isBlank()) { + errors.append(getString(R.string.validate_missing_param, "type")) + } else if (!Dtmf.isValidType(type)) { + errors.append(getString(R.string.validate_invalid_param, "type")) + + } + + // size + if (size < 1) { + errors.append(getString(R.string.validate_invalid_param, "size")) + } + + // ack + if (ack.isBlank()) { + errors.append(getString(R.string.validate_missing_param, "ack")) + } } + // options if (opts.size == 0) { errors.append(getString(R.string.validate_missing_opts)) - } - - opts.forEachIndexed { i, option -> - if (option.fields.size == 0) { - errors.append(getString(R.string.validate_missing_fields, i + 1)) - } - - if (option.nosteps && option.nodial) { - errors.append(getString(R.string.validate_invalid_option, i + 1, "nodial/nosteps")) - } - - if (option.dtmf.isBlank()) { - errors.append(getString(R.string.validate_invalid_dtmf, i + 1, "''")) - } - - option.fields.forEachIndexed { j, field -> - if (field.size <= 0) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "size=${field.size}")) - } - - if (field.digits.isNotBlank() && !field.digits.isDigits()) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, "digits")) - } - - if (field.minSize >= 0 && field.minSize > field.size) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "minSize=${field.minSize}/size-${field.size}")) - } - - if (!field.alpha) { - if (field.minSize == 0) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "minSize=${field.minSize}")) - } - - if (field.min >= 0 || field.max >= 0) { - if (field.max < 1) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "max=${field.max}")) + } else { + opts.forEachIndexed { i, option -> + // gson will create a null object on trailing comma + // see: https://github.com/google/gson/issues/494 + if (option == null) { + errors.append(getString(R.string.validate_syntax_error, "opts[]")) + } else { + with(option) { + // title + if (title.isBlank()) { + errors.append(getString( + R.string.validate_missing_opts_prop, + i + 1, + "title")) } - if (field.min < 0) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "min=${field.min}")) + // nosteps/nodial + if (nosteps && nodial) { + errors.append(getString( + R.string.validate_invalid_option, + i + 1, + "nodial/nosteps")) } - if (field.min > field.max) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "min=${field.min}/max=${field.max}")) - } + // dtmf + if (dtmf.isBlank()) { + errors.append(getString( + R.string.validate_missing_opts_prop, + i + 1, + "dtmf")) + } else if (fields.size == 0) { // fields missing + errors.append(getString( + R.string.validate_missing_opts_prop, + i + 1, + "fields")) + } else { + val blank = "\\0" + val mock = Dtmf.mock(option, blank) - if (!params.type.isDKS() && !field.zeros) { - if (field.min > 0 && field.minSize > 0) { - if (field.min.toString().length != field.minSize) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "minSize=${field.minSize}/min=${field.min}")) - } + if (!Dtmf.validate(mock, + "${MainActivity.PAUSE}${params.ack}${params.alt}$blank")) { + errors.append(getString( + R.string.validate_invalid_opts_prop, + i + 1, + "dtmf", + mock.replace(blank, "✓"))) } + } - if (field.size > 0 && field.max > 0) { - if (field.max.toString().length != field.size) { - errors.append(getString(R.string.validate_invalid_attr, i + 1, j + 1, - "size=${field.size}/max=${field.max}")) + // fields + fields.forEachIndexed { j, field -> + if (field == null) { + errors.append(getString( + R.string.validate_syntax_error, + "opts[${i+j}], field[$j]")) + } else { + with(field) { + // size + if (size <= 0) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 1), + "size=$size")) + } + + // digits + if (digits.isNotBlank() && !digits.isDigits()) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 1), + "digits='$digits'")) + } + + // minSize + if (minSize >= 0 && minSize > size) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 2), + "minSize=$minSize > size=$size")) + } + + // numeric fields only + if (!alpha) { + if (minSize == 0) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 1), + "minSize=$minSize")) + } + + // min/max + if (min >= 0 || max >= 0) { + if (max < 1) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 1), + "max=$max")) + } + + if (min < 0) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 1), + "min=$min")) + } + + if (min > max) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 2), + "min=$min > max=$max")) + } + } + + // no leading zeros + if (!params.type.isDKS() && !zeros) { + // minSize/min + if (min >= 0 && minSize > 0) { + if (min.toString().length != minSize) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 2), + "minSize=$minSize/min=$min")) + } + } + + // size/max + if (size > 0 && max > 0) { + if (max.toString().length != size) { + errors.append(getString( + R.string.validate_invalid_field_prop, + i + 1, + j + 1, + resources.getQuantityString(R.plurals.error_prop, 2), + "size=$size/max=$max")) + } + } + } + } + + // unused fields + if (!dtmf.contains(Dtmf.DTMF_FIELD.format(j + 1))) { + errors.append(getString( + R.string.validate_unused_field, + i + 1, + j + 1)) + } } } } } } - - if (!option.dtmf.contains(Dtmf.DTMF_FIELD.format(j + 1))) { - errors.append(getString(R.string.validate_unused_field, i + 1, j + 1)) - } - } - - val blank = "\\0" - val dtmf = Dtmf.mock(option, blank) - if (!Dtmf.validate(dtmf, "${MainActivity.Companion.PAUSE}${params.ack}${params.alt}$blank")) { - errors.append(getString(R.string.validate_invalid_dtmf, i + 1, dtmf.replace(blank, "✓"))) } } } @@ -453,17 +559,19 @@ class MainActivity : AppCompatActivity(), AnkoLogger { return errors.length == len } - fun validateFields(fields: ArrayList, size: Int): Boolean { + fun validateFields(fields: ArrayList>): Boolean { var isValid = true fields.forEach { - if (it.text.isNullOrBlank()) { - it.error = getString(R.string.error_required) - isValid = false - } else if (size > 0 && (fields[1].text.length != size)) { - isValid = false - fields[1].error = getString(R.string.error_invalid_size, size, - resources.getQuantityString(R.plurals.error_digit, size), "") + with(it) { + if (first.text.isNullOrBlank()) { + first.error = getString(R.string.error_required) + isValid = false + } else if (second > 0 && first.text.length != second) { + first.error = getString(R.string.error_invalid_size, second, + resources.getQuantityString(R.plurals.error_digit, second), "") + isValid = false + } } } diff --git a/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/models/Option.kt b/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/models/Option.kt index 355c93c..6665ad1 100644 --- a/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/models/Option.kt +++ b/app/src/main/java/net/thauvin/erik/android/tesremoteprogrammer/models/Option.kt @@ -22,7 +22,7 @@ import android.os.Parcelable import java.io.Serializable data class Option(var title: String, - var fields: List, + var fields: List, var nodial: Boolean, var nosteps: Boolean, var dtmf: String) : Parcelable, Serializable, Comparable