From ee032dded03597fb1ca1c3622b1d5727ab761baa Mon Sep 17 00:00:00 2001 From: Markus Friedli Date: Fri, 20 Sep 2024 17:52:32 +0200 Subject: [PATCH] Support numeric short codes (only for countries ch, de, fr and us) (#587) --- README.md | 19 ++- .../OperationsCenterContactService.java | 17 +-- .../pikettassist/service/dao/ContactDao.java | 11 +- .../ui/settings/SettingsActivity.java | 3 +- .../pikettassist/util/PhoneNumberType.java | 111 ++++++++++++++++++ 5 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/github/frimtec/android/pikettassist/util/PhoneNumberType.java diff --git a/README.md b/README.md index b958ad61..532d6135 100644 --- a/README.md +++ b/README.md @@ -116,11 +116,20 @@ PAssist will guide you to install this SMS adapter. Defines the contact of your operations center sending alarms via SMS. The SMS received from any phone number of this contact are supervised by PAssist. -PAssist also supports operations centers using alphanumeric short code SMS numbers. -As contacts in Android cannot use letters in phone numbers, such alphanumeric short codes can be -configured in the contact field "Company". -It the operations centers uses several alphanumeric short codes, they can be comma separated in the -contacts company field. +PAssist also supports operations centers using numeric and alphanumeric short code SMS numbers. +Numeric short codes are currently only supported for the following countries: + +* Switzerland +* Germany +* France +* USA + Alphanumeric short codes are supported in any country. + +As contacts in Android cannot contain such short code phone numbers, +they can be configured in the contact field "Company". +It the operations centers uses several short code phone numbers, they can be comma separated in the +contacts company field. Any regular phone numbers stored in the contacts field "Company" are +ignored. ![Operations center contact with two alphanumeric short code SMS numbers](images/Contacts-with-alphanumeric-short-codes.png) diff --git a/app/src/main/java/com/github/frimtec/android/pikettassist/service/OperationsCenterContactService.java b/app/src/main/java/com/github/frimtec/android/pikettassist/service/OperationsCenterContactService.java index 6a5471e7..ec05ce4b 100644 --- a/app/src/main/java/com/github/frimtec/android/pikettassist/service/OperationsCenterContactService.java +++ b/app/src/main/java/com/github/frimtec/android/pikettassist/service/OperationsCenterContactService.java @@ -64,21 +64,8 @@ public boolean isContactsPhoneNumber(Contact contact, String number) { return false; } ContactDao contactDao = getContactDao(); - if (isAlphanumericShortCode(number)) { - return contactDao.getAlphanumericShortCodesFromContact(contact).contains(number); - } - Set contactIds = contactDao.lookupContactIdsByPhoneNumber(number); - return contactIds.contains(contact.reference().id()); - } - - private static boolean isAlphanumericShortCode(String phoneNumber) { - for (int i = 0; i < phoneNumber.length(); i++) { - char ch = phoneNumber.charAt(i); - if (Character.isLetter(ch)) { - return true; - } - } - return false; + return contactDao.lookupContactIdsByPhoneNumber(number).contains(contact.reference().id()) || + contactDao.getShortCodesFromContact(contact).contains(number); } public Set getPhoneNumbers(Contact contact) { diff --git a/app/src/main/java/com/github/frimtec/android/pikettassist/service/dao/ContactDao.java b/app/src/main/java/com/github/frimtec/android/pikettassist/service/dao/ContactDao.java index f827493f..63e39034 100644 --- a/app/src/main/java/com/github/frimtec/android/pikettassist/service/dao/ContactDao.java +++ b/app/src/main/java/com/github/frimtec/android/pikettassist/service/dao/ContactDao.java @@ -1,5 +1,6 @@ package com.github.frimtec.android.pikettassist.service.dao; +import static com.github.frimtec.android.pikettassist.util.PhoneNumberType.fromNumber; import static java.util.stream.Collectors.joining; import android.content.ContentResolver; @@ -34,9 +35,11 @@ public class ContactDao { ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }; + private final Context context; private final ContentResolver contentResolver; public ContactDao(Context context) { + this.context = context; this.contentResolver = context.getContentResolver(); } @@ -141,12 +144,12 @@ public Set getPhoneNumbers(Contact contact) { } } - // add alphanumeric short codes from contact organization field as comma separated list - phoneNumbers.addAll(getAlphanumericShortCodesFromContact(contact)); + // add short codes from contact organization field as comma separated list + phoneNumbers.addAll(getShortCodesFromContact(contact)); return phoneNumbers; } - public Set getAlphanumericShortCodesFromContact(Contact contact) { + public Set getShortCodesFromContact(Contact contact) { try (Cursor cursor = this.contentResolver.query(ContactsContract.Data.CONTENT_URI, null, ContactsContract.Data.CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?", new String[]{String.valueOf(contact.reference().id()), ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE}, null)) { @@ -159,7 +162,7 @@ public Set getAlphanumericShortCodesFromContact(Contact contact) { return Arrays.stream(company.split(",")) .filter(Objects::nonNull) .map(String::trim) - .filter(companyName -> companyName.length() > 0) + .filter(number -> fromNumber(number, this.context).isShortCode()) .collect(Collectors.toSet()); } } else { diff --git a/app/src/main/java/com/github/frimtec/android/pikettassist/ui/settings/SettingsActivity.java b/app/src/main/java/com/github/frimtec/android/pikettassist/ui/settings/SettingsActivity.java index 62852c61..7f13ce25 100644 --- a/app/src/main/java/com/github/frimtec/android/pikettassist/ui/settings/SettingsActivity.java +++ b/app/src/main/java/com/github/frimtec/android/pikettassist/ui/settings/SettingsActivity.java @@ -32,6 +32,7 @@ import com.github.frimtec.android.pikettassist.service.dao.ContactDao; import com.github.frimtec.android.pikettassist.service.system.SignalStrengthService; import com.github.frimtec.android.pikettassist.state.ApplicationPreferences; +import com.github.frimtec.android.pikettassist.util.PhoneNumberType; import com.takisoft.preferencex.EditTextPreference; import com.takisoft.preferencex.PreferenceFragmentCompat; import com.takisoft.preferencex.RingtonePreference; @@ -101,7 +102,7 @@ public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String r Contact contact = new OperationsCenterContactService(context).getOperationsCenterContact(); if (!TextUtils.isEmpty(value) && AlertConfirmMethod.valueOf(value).isSms() && contact.valid()) { ContactDao contactDao = new ContactDao(context); - if (!contactDao.getAlphanumericShortCodesFromContact(contact).isEmpty()) { + if (contactDao.getShortCodesFromContact(contact).stream().anyMatch(number -> !PhoneNumberType.fromNumber(number, context).isSendSupport())) { summary = summary + " / " + getString(R.string.pref_summary_send_confirm_sms); } } diff --git a/app/src/main/java/com/github/frimtec/android/pikettassist/util/PhoneNumberType.java b/app/src/main/java/com/github/frimtec/android/pikettassist/util/PhoneNumberType.java new file mode 100644 index 00000000..6e88d65e --- /dev/null +++ b/app/src/main/java/com/github/frimtec/android/pikettassist/util/PhoneNumberType.java @@ -0,0 +1,111 @@ +package com.github.frimtec.android.pikettassist.util; + +import android.content.Context; +import android.telephony.TelephonyManager; + +/** + * Helper to evaluate the type of a phone number. + * + * @since 3.3.0 + */ +// TODO: 20.09.2024 REPLACE when updated to com.github.frimtec:secure-sms-proxy-api version 3.3.0 +public enum PhoneNumberType { + STANDARD(true, false, true), + NUMERIC_SHORT_CODE(true, true, true), + ALPHANUMERIC_SHORT_CODE(true, true, false), + EMPTY(false, false, false); + + private final boolean valid; + private final boolean shortCode; + private final boolean sendSupport; + + PhoneNumberType(boolean valid, boolean shortCode, boolean sendSupport) { + this.valid = valid; + this.shortCode = shortCode; + this.sendSupport = sendSupport; + } + + public boolean isValid() { + return valid; + } + + public boolean isShortCode() { + return shortCode; + } + + public boolean isSendSupport() { + return sendSupport; + } + + /** + * Returns the type of the given phone number. + * + * @param phoneNumber phone number + * @param context context to get the country code of the telephony manager, used to detect numeric short codes + * @return phone number type + **/ + public static PhoneNumberType fromNumber( + String phoneNumber, + Context context + ) { + return fromNumber(phoneNumber, networkCountryIso(context)); + } + + /** + * Returns the type of the given phone number. + * + * @param phoneNumber phone number + * @param twoLetterIsoCountryCode country code, used to detect numeric short codes + * @return phone number type + **/ + public static PhoneNumberType fromNumber( + String phoneNumber, + String twoLetterIsoCountryCode + ) { + if (phoneNumber == null) { + return PhoneNumberType.EMPTY; + } + String trimmedPhoneNumber = phoneNumber.trim(); + if (trimmedPhoneNumber.isEmpty()) { + return PhoneNumberType.EMPTY; + } + if (isAlphanumericShortCode(trimmedPhoneNumber)) { + return PhoneNumberType.ALPHANUMERIC_SHORT_CODE; + } + if (twoLetterIsoCountryCode != null && isNumericShortCode(trimmedPhoneNumber, twoLetterIsoCountryCode.toUpperCase())) { + return PhoneNumberType.NUMERIC_SHORT_CODE; + } + return PhoneNumberType.STANDARD; + } + + public static String networkCountryIso(Context context) { + TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return telephonyManager != null ? telephonyManager.getNetworkCountryIso() : ""; + } + + private static boolean isAlphanumericShortCode(String phoneNumber) { + for (int i = 0; i < phoneNumber.length(); i++) { + char ch = phoneNumber.charAt(i); + if (Character.isLetter(ch)) { + return true; + } + } + return false; + } + + private static boolean isNumericShortCode(String phoneNumber, String countryCode) { + return switch (countryCode) { + case "CH" -> inLengthRange(phoneNumber, 3, 5); + case "FR" -> inLengthRange(phoneNumber, 5, 5); + case "DE" -> inLengthRange(phoneNumber, 4, 5); + case "US" -> phoneNumber.charAt(0) != '1' && inLengthRange(phoneNumber, 5, 6); + default -> false; + }; + } + + private static boolean inLengthRange(String phoneNumber, int min, int max) { + int length = phoneNumber.length(); + return length >= min && length <= max; + } + +}