File indexing completed on 2025-02-02 04:47:49

0001 /*
0002  * SPDX-FileCopyrightText: 2019 Simon Redman <simon@ergotech.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 package org.kde.kdeconnect.Helpers;
0008 
0009 import android.annotation.SuppressLint;
0010 import android.content.Context;
0011 import android.database.Cursor;
0012 import android.net.Uri;
0013 import android.os.Build;
0014 import android.provider.Telephony;
0015 import android.telephony.SubscriptionInfo;
0016 import android.telephony.SubscriptionManager;
0017 import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
0018 import android.telephony.TelephonyManager;
0019 import android.util.Log;
0020 
0021 import androidx.annotation.NonNull;
0022 import androidx.annotation.Nullable;
0023 import androidx.annotation.RequiresApi;
0024 import androidx.core.content.ContextCompat;
0025 
0026 import java.util.ArrayList;
0027 import java.util.Collections;
0028 import java.util.HashSet;
0029 import java.util.List;
0030 import java.util.stream.Collectors;
0031 
0032 public class TelephonyHelper {
0033 
0034     public static final String LOGGING_TAG = "TelephonyHelper";
0035 
0036     /**
0037      * Get all subscriptionIDs of the device
0038      * As far as I can tell, this is essentially a way of identifying particular SIM cards
0039      */
0040     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
0041     public static List<Integer> getActiveSubscriptionIDs(
0042             @NonNull Context context)
0043             throws SecurityException {
0044         SubscriptionManager subscriptionManager = ContextCompat.getSystemService(context,
0045                 SubscriptionManager.class);
0046         if (subscriptionManager == null) {
0047             // I don't know why or when this happens...
0048             Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
0049             return Collections.emptyList();
0050         }
0051         List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
0052         if (subscriptionInfos == null) {
0053             // This happens when there is no SIM card inserted
0054             Log.w(LOGGING_TAG, "Could not get SubscriptionInfos");
0055             return Collections.emptyList();
0056         }
0057         List<Integer> subscriptionIDs = new ArrayList<>(subscriptionInfos.size());
0058         for (SubscriptionInfo info : subscriptionInfos) {
0059             subscriptionIDs.add(info.getSubscriptionId());
0060         }
0061         return subscriptionIDs;
0062     }
0063 
0064     /**
0065      * Callback for `listenActiveSubscriptionIDs`
0066      */
0067     public interface SubscriptionCallback {
0068         void run(Integer subscriptionID);
0069     }
0070 
0071     /**
0072      * Registers a listener for changes in subscriptionIDs for the device.
0073      * This lets you identify additions/removals of SIM cards.
0074      * Make sure to call `cancelActiveSubscriptionIDsListener` with the return value of this once you're done.
0075      */
0076     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
0077     public static OnSubscriptionsChangedListener listenActiveSubscriptionIDs(
0078             @NonNull Context context, SubscriptionCallback onAdd, SubscriptionCallback onRemove) {
0079         SubscriptionManager sm = ContextCompat.getSystemService(context, SubscriptionManager.class);
0080         if (sm == null) {
0081             // I don't know why or when this happens...
0082             Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
0083             return null;
0084         }
0085 
0086         HashSet<Integer> activeIDs = new HashSet<>();
0087 
0088         OnSubscriptionsChangedListener listener = new OnSubscriptionsChangedListener() {
0089             @Override
0090             public void onSubscriptionsChanged() {
0091                 HashSet<Integer> nextSubs = new HashSet<>(getActiveSubscriptionIDs(context));
0092 
0093                 HashSet<Integer> addedSubs = new HashSet<>(nextSubs);
0094                 addedSubs.removeAll(activeIDs);
0095 
0096                 HashSet<Integer> removedSubs = new HashSet<>(activeIDs);
0097                 removedSubs.removeAll(nextSubs);
0098 
0099                 activeIDs.removeAll(removedSubs);
0100                 activeIDs.addAll(addedSubs);
0101 
0102                 // Delete old listeners
0103                 for (Integer subID : removedSubs) {
0104                     onRemove.run(subID);
0105                 }
0106 
0107                 // Create new listeners
0108                 for (Integer subID : addedSubs) {
0109                     onAdd.run(subID);
0110                 }
0111             }
0112         };
0113         sm.addOnSubscriptionsChangedListener(listener);
0114         return listener;
0115     }
0116 
0117     /**
0118      * Cancels a listener created by `listenActiveSubscriptionIDs`
0119      */
0120     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
0121     public static void cancelActiveSubscriptionIDsListener(@NonNull Context context, @NonNull OnSubscriptionsChangedListener listener) {
0122         SubscriptionManager sm = ContextCompat.getSystemService(context, SubscriptionManager.class);
0123         if (sm == null) {
0124             // I don't know why or when this happens...
0125             Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
0126             return;
0127         }
0128 
0129         sm.removeOnSubscriptionsChangedListener(listener);
0130     }
0131 
0132     /**
0133      * Try to get the phone number currently active on the phone
0134      *
0135      * Make sure that you have the READ_PHONE_STATE permission!
0136      *
0137      * Note that entries of the returned list might return null if the phone number is not known by the device
0138      */
0139     public static @NonNull List<LocalPhoneNumber> getAllPhoneNumbers(
0140             @NonNull Context context)
0141             throws SecurityException {
0142         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
0143             // Single-sim case
0144             // From https://stackoverflow.com/a/25131061/3723163
0145             // Android added support for multi-sim devices in Lollypop v5.1 (api 22)
0146             // See: https://developer.android.com/about/versions/android-5.1.html#multisim
0147             // There were vendor-specific implmentations before then, but those are very difficult to support
0148             // S/O Reference: https://stackoverflow.com/a/28571835/3723163
0149             TelephonyManager telephonyManager = ContextCompat.getSystemService(context,
0150                     TelephonyManager.class);
0151             if (telephonyManager == null) {
0152                 // I don't know why or when this happens...
0153                 Log.w(LOGGING_TAG, "Could not get TelephonyManager");
0154                 return Collections.emptyList();
0155             }
0156             LocalPhoneNumber phoneNumber = getPhoneNumber(telephonyManager);
0157             return Collections.singletonList(phoneNumber);
0158         } else {
0159             // Potentially multi-sim case
0160             SubscriptionManager subscriptionManager = ContextCompat.getSystemService(context,
0161                     SubscriptionManager.class);
0162             if (subscriptionManager == null) {
0163                 // I don't know why or when this happens...
0164                 Log.w(LOGGING_TAG, "Could not get SubscriptionManager");
0165                 return Collections.emptyList();
0166             }
0167             List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList();
0168             if (subscriptionInfos == null) {
0169                 // This happens when there is no SIM card inserted
0170                 Log.w(LOGGING_TAG, "Could not get SubscriptionInfos");
0171                 return Collections.emptyList();
0172             }
0173             List<LocalPhoneNumber> phoneNumbers = new ArrayList<>(subscriptionInfos.size());
0174             for (SubscriptionInfo info : subscriptionInfos) {
0175                 LocalPhoneNumber thisPhoneNumber = new LocalPhoneNumber(info.getNumber(), info.getSubscriptionId());
0176                 phoneNumbers.add(thisPhoneNumber);
0177             }
0178             return phoneNumbers.stream().filter(localPhoneNumber -> localPhoneNumber.number != null).collect(Collectors.toList());
0179         }
0180     }
0181 
0182     /**
0183      * Try to get the phone number to which the TelephonyManager is pinned
0184      */
0185     public static @Nullable LocalPhoneNumber getPhoneNumber(
0186             @NonNull TelephonyManager telephonyManager)
0187             throws SecurityException {
0188         @SuppressLint("HardwareIds")
0189         String maybeNumber = telephonyManager.getLine1Number();
0190 
0191         if (maybeNumber == null) {
0192             Log.d(LOGGING_TAG, "Got 'null' instead of a phone number");
0193             return null;
0194         }
0195         // Sometimes we will get some garbage like "Unknown" or "?????" or a variety of other things
0196         // Per https://stackoverflow.com/a/25131061/3723163, the only real solution to this is to
0197         // query the user for the proper phone number
0198         // As a quick possible check, I say if a "number" is not at least 25% digits, it is not actually
0199         // a number
0200         int digitCount = 0;
0201         for (char digit : "0123456789".toCharArray()) {
0202             // https://stackoverflow.com/a/8910767/3723163
0203             // The number of occurrences of a particular character can be counted by looking at the
0204             // total length of the string and subtracting the length of the string without the
0205             // target digit
0206             int count = maybeNumber.length() - maybeNumber.replace("" + digit, "").length();
0207             digitCount += count;
0208         }
0209         if (maybeNumber.length() > digitCount*4) {
0210             Log.d(LOGGING_TAG, "Discarding " + maybeNumber + " because it does not contain a high enough digit ratio to be a real phone number");
0211             return null;
0212         } else {
0213             return new LocalPhoneNumber(maybeNumber, -1);
0214         }
0215     }
0216 
0217     /**
0218      * Get the APN settings of the current APN for the given subscription ID
0219      *
0220      * Note that this method is broken after Android 4.2 but starts working again "at some point"
0221      * After Android 4.2, *reading* APN permissions requires a system permission (WRITE_APN_SETTINGS)
0222      * Before this, no permission is required
0223      * At some point after, the permission is not required to read non-sensitive columns (which are the
0224      * only ones we need)
0225      * If anyone has a solution to this (which doesn't involve a vendor-sepecific XML), feel free to share!
0226      *
0227      * Cobbled together from the [Android sources](https://android.googlesource.com/platform/packages/services/Mms/+/refs/heads/master/src/com/android/mms/service/ApnSettings.java)
0228      * and some StackOverflow Posts
0229      * [post 1](https://stackoverflow.com/a/18897139/3723163)
0230      * [post 2[(https://stackoverflow.com/a/7928751/3723163)
0231      *
0232      * @param context Context of the requestor
0233      * @param subscriptionId Subscription ID for which to get the preferred APN. Ignored for devices older than Lollypop
0234      * @return Null if the preferred APN can't be found or doesn't support MMS, otherwise an ApnSetting object
0235      */
0236     @SuppressLint("InlinedApi")
0237     public static ApnSetting getPreferredApn(Context context, int subscriptionId) {
0238 
0239         String[] APN_PROJECTION = {
0240                 Telephony.Carriers.TYPE,
0241                 Telephony.Carriers.MMSC,
0242                 Telephony.Carriers.MMSPROXY,
0243                 Telephony.Carriers.MMSPORT,
0244         };
0245 
0246         Uri telephonyCarriersUri = Telephony.Carriers.CONTENT_URI;
0247 
0248         Uri telephonyCarriersPreferredApnUri;
0249 
0250         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
0251             telephonyCarriersPreferredApnUri = Uri.withAppendedPath(telephonyCarriersUri, "/preferapn/subId/" + subscriptionId);
0252         } else {
0253             // Ignore subID for devices before that existed
0254             telephonyCarriersPreferredApnUri = Uri.withAppendedPath(telephonyCarriersUri, "/preferapn/");
0255         }
0256 
0257         try (Cursor cursor = context.getContentResolver().query(
0258                 telephonyCarriersPreferredApnUri,
0259                 APN_PROJECTION,
0260                 null,
0261                 null,
0262                 Telephony.Carriers.DEFAULT_SORT_ORDER
0263         )) {
0264             while (cursor != null && cursor.moveToNext()) {
0265 
0266                 String type = cursor.getString(cursor.getColumnIndex(Telephony.Carriers.TYPE));
0267                 if (!isValidApnType(type, APN_TYPE_MMS)) continue;
0268 
0269                 ApnSetting.Builder apnBuilder = new ApnSetting.Builder()
0270                         .setMmsc(Uri.parse(cursor.getString(cursor.getColumnIndex(Telephony.Carriers.MMSC))))
0271                         .setMmsProxyAddress(cursor.getString(cursor.getColumnIndex(Telephony.Carriers.MMSPROXY)));
0272 
0273                 String maybeMmsProxyPort = cursor.getString(cursor.getColumnIndex(Telephony.Carriers.MMSPORT));
0274                 try {
0275                     int mmsProxyPort = Integer.parseInt(maybeMmsProxyPort);
0276                     apnBuilder.setMmsProxyPort(mmsProxyPort);
0277                 } catch (Exception e) {
0278                     // Lots of APN settings have other values, very commonly something like "Not set"
0279                     // just cross your fingers and hope that the default in ApnSetting works...
0280                     // If someone finds some documentation which says what the default value should be,
0281                     // please share
0282                 }
0283 
0284                 return apnBuilder.build();
0285             }
0286         } catch (Exception e)
0287         {
0288             Log.e(LOGGING_TAG, "Error encountered while trying to read APNs", e);
0289         }
0290 
0291         return null;
0292     }
0293 
0294     /**
0295      * APN types for data connections.  These are usage categories for an APN
0296      * entry.  One APN entry may support multiple APN types, eg, a single APN
0297      * may service regular internet traffic ("default") as well as MMS-specific
0298      * connections.
0299      * APN_TYPE_ALL is a special type to indicate that this APN entry can
0300      * service all data connections.
0301      * Copied from Android's internal source: https://android.googlesource.com/platform/frameworks/base/+/cd92588/telephony/java/com/android/internal/telephony/PhoneConstants.java
0302      */
0303     private static final String APN_TYPE_ALL = "*";
0304     /** APN type for MMS traffic */
0305     private static final String APN_TYPE_MMS = "mms";
0306 
0307     /**
0308      * Copied directly from Android's source: https://android.googlesource.com/platform/packages/services/Mms/+/refs/heads/master/src/com/android/mms/service/ApnSettings.java
0309      * @param types Value of Telephony.Carriers.TYPE for the APN being interrogated
0310      * @param requestType Value which we would like to find in types
0311      * @return True if the APN supports the requested type, false otherwise
0312      */
0313     private static boolean isValidApnType(String types, String requestType) {
0314         // If APN type is unspecified, assume APN_TYPE_ALL.
0315         if (types.isEmpty()) {
0316             return true;
0317         }
0318         for (String type : types.split(",")) {
0319             type = type.trim();
0320             if (type.equals(requestType) || type.equals(APN_TYPE_ALL)) {
0321                 return true;
0322             }
0323         }
0324         return false;
0325     }
0326 
0327     /**
0328      * Canonicalize a phone number by removing all (valid) non-digit characters
0329      *
0330      * Should be equivalent to SmsHelper::canonicalizePhoneNumber in the C++ implementation
0331      *
0332      * @param phoneNumber The phone number to canonicalize
0333      * @return The canonicalized version of the input phone number
0334      */
0335     public static String canonicalizePhoneNumber(String phoneNumber)
0336     {
0337         String toReturn = phoneNumber;
0338         toReturn = toReturn.replace(" ", "");
0339         toReturn = toReturn.replace("-", "");
0340         toReturn = toReturn.replace("(", "");
0341         toReturn = toReturn.replace(")", "");
0342         toReturn = toReturn.replace("+", "");
0343         toReturn = toReturn.replaceFirst("^0*", "");
0344 
0345         if (toReturn.isEmpty()) {
0346             // If we have stripped away everything, assume this is a special number (and already canonicalized)
0347             return phoneNumber;
0348         }
0349         return toReturn;
0350     }
0351 
0352     /**
0353      * Light copy of https://developer.android.com/reference/android/telephony/data/ApnSetting so
0354      * that we can support older API versions. Delete this when API 28 becomes our supported version.
0355      */
0356     public static class ApnSetting
0357     {
0358         private Uri mmscUri = null;
0359         private String mmsProxyAddress = null;
0360         private int mmsProxyPort = 80; // Default port should be 80 according to code comment in Android's ApnSettings.java
0361 
0362         public static class Builder {
0363             private final org.kde.kdeconnect.Helpers.TelephonyHelper.ApnSetting internalApnSetting;
0364 
0365             public Builder() {
0366                 internalApnSetting = new ApnSetting();
0367             }
0368 
0369             public Builder setMmsc(Uri mmscUri) {
0370                 internalApnSetting.mmscUri = mmscUri;
0371                 return this;
0372             }
0373 
0374             public Builder setMmsProxyAddress(String mmsProxy) {
0375                 internalApnSetting.mmsProxyAddress = mmsProxy;
0376                 return this;
0377             }
0378 
0379             public Builder setMmsProxyPort(int mmsPort) {
0380                 internalApnSetting.mmsProxyPort = mmsPort;
0381                 return this;
0382             }
0383 
0384             public ApnSetting build() {
0385                 return internalApnSetting;
0386             }
0387         }
0388 
0389         private ApnSetting() {}
0390 
0391         public Uri getMmsc() {
0392             return mmscUri;
0393         }
0394 
0395         public String getMmsProxyAddressAsString() {
0396             return mmsProxyAddress;
0397         }
0398 
0399         public int getMmsProxyPort() {
0400             return mmsProxyPort;
0401         }
0402     }
0403 
0404     /**
0405      * Class representing a phone number which is assigned to the current device
0406      */
0407     public static class LocalPhoneNumber {
0408         /**
0409         * The phone number
0410         */
0411         public final String number;
0412 
0413         /**
0414          * The subscription ID to which this phone number belongs
0415          */
0416         public final int subscriptionID;
0417 
0418        public LocalPhoneNumber(String number, int subscriptionID) {
0419            this.number = number;
0420            this.subscriptionID = subscriptionID;
0421        }
0422 
0423         @NonNull
0424         @Override
0425         public String toString() {
0426             return number;
0427         }
0428 
0429         /**
0430          * Do some basic fuzzy matching on two phone numbers to determine whether they match
0431          *
0432          * This is roughly equivalent to SmsHelper::isPhoneNumberMatch, but might produce more false negatives
0433          *
0434          * @param potentialMatchingPhoneNumber The phone number to compare to this phone number
0435          * @return True if the phone numbers appear to be the same, false otherwise
0436          */
0437         public boolean isMatchingPhoneNumber(String potentialMatchingPhoneNumber) {
0438            String mPhoneNumber = canonicalizePhoneNumber(this.number);
0439            String oPhoneNumber = canonicalizePhoneNumber(potentialMatchingPhoneNumber);
0440 
0441             if (mPhoneNumber.isEmpty() || oPhoneNumber.isEmpty()) {
0442                 // The empty string is not a valid phone number so does not match anything
0443                 return false;
0444             }
0445 
0446             // To decide if a phone number matches:
0447             // 1. Are they similar lengths? If two numbers are very different, probably one is junk data and should be ignored
0448             // 2. Is one a superset of the other? Phone number digits get more specific the further towards the end of the string,
0449             //    so if one phone number ends with the other, it is probably just a more-complete version of the same thing
0450             String longerNumber = mPhoneNumber.length() >= oPhoneNumber.length() ? mPhoneNumber : oPhoneNumber;
0451             String shorterNumber = mPhoneNumber.length() < oPhoneNumber.length() ? mPhoneNumber : oPhoneNumber;
0452 
0453             // If the numbers are vastly different in length, assume they are not the same
0454             if (shorterNumber.length() < 0.75 * longerNumber.length()) {
0455                 return false;
0456             }
0457 
0458             boolean matchingPhoneNumber = longerNumber.endsWith(shorterNumber);
0459 
0460             return matchingPhoneNumber;
0461         }
0462     }
0463 }