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 }