1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.v7.mms; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.content.res.XmlResourceParser; 23 import android.database.Cursor; 24 import android.database.sqlite.SQLiteException; 25 import android.net.Uri; 26 import android.provider.Telephony; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.util.SparseArray; 30 31 import com.android.messaging.R; 32 33 import java.net.URI; 34 import java.net.URISyntaxException; 35 import java.util.ArrayList; 36 import java.util.List; 37 38 /** 39 * Default implementation of APN settings loader 40 */ 41 class DefaultApnSettingsLoader implements ApnSettingsLoader { 42 /** 43 * The base implementation of an APN 44 */ 45 private static class BaseApn implements Apn { 46 /** 47 * Create a base APN from parameters 48 * 49 * @param typesIn the APN type field 50 * @param mmscIn the APN mmsc field 51 * @param proxyIn the APN mmsproxy field 52 * @param portIn the APN mmsport field 53 * @return an instance of base APN, or null if any of the parameter is invalid 54 */ 55 public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn, 56 final String portIn) { 57 if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) { 58 return null; 59 } 60 String mmsc = trimWithNullCheck(mmscIn); 61 if (TextUtils.isEmpty(mmsc)) { 62 return null; 63 } 64 mmsc = trimV4AddrZeros(mmsc); 65 try { 66 new URI(mmsc); 67 } catch (final URISyntaxException e) { 68 return null; 69 } 70 String mmsProxy = trimWithNullCheck(proxyIn); 71 int mmsProxyPort = 80; 72 if (!TextUtils.isEmpty(mmsProxy)) { 73 mmsProxy = trimV4AddrZeros(mmsProxy); 74 final String portString = trimWithNullCheck(portIn); 75 if (portString != null) { 76 try { 77 mmsProxyPort = Integer.parseInt(portString); 78 } catch (final NumberFormatException e) { 79 // Ignore, just use 80 to try 80 } 81 } 82 } 83 return new BaseApn(mmsc, mmsProxy, mmsProxyPort); 84 } 85 86 private final String mMmsc; 87 private final String mMmsProxy; 88 private final int mMmsProxyPort; 89 90 public BaseApn(final String mmsc, final String proxy, final int port) { 91 mMmsc = mmsc; 92 mMmsProxy = proxy; 93 mMmsProxyPort = port; 94 } 95 96 @Override 97 public String getMmsc() { 98 return mMmsc; 99 } 100 101 @Override 102 public String getMmsProxy() { 103 return mMmsProxy; 104 } 105 106 @Override 107 public int getMmsProxyPort() { 108 return mMmsProxyPort; 109 } 110 111 @Override 112 public void setSuccess() { 113 // Do nothing 114 } 115 116 public boolean equals(final BaseApn other) { 117 return TextUtils.equals(mMmsc, other.getMmsc()) && 118 TextUtils.equals(mMmsProxy, other.getMmsProxy()) && 119 mMmsProxyPort == other.getMmsProxyPort(); 120 } 121 } 122 123 /** 124 * An in-memory implementation of an APN. These APNs are organized into an in-memory list. 125 * The order of the list can be changed by the setSuccess method. 126 */ 127 private static class MemoryApn implements Apn { 128 /** 129 * Create an in-memory APN loaded from resources 130 * 131 * @param apns the in-memory APN list 132 * @param typesIn the APN type field 133 * @param mmscIn the APN mmsc field 134 * @param proxyIn the APN mmsproxy field 135 * @param portIn the APN mmsport field 136 * @return an in-memory APN instance, null if there is invalid parameter 137 */ 138 public static MemoryApn from(final List<Apn> apns, final String typesIn, 139 final String mmscIn, final String proxyIn, final String portIn) { 140 if (apns == null) { 141 return null; 142 } 143 final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn); 144 if (base == null) { 145 return null; 146 } 147 for (final Apn apn : apns) { 148 if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) { 149 return null; 150 } 151 } 152 return new MemoryApn(apns, base); 153 } 154 155 private final List<Apn> mApns; 156 private final BaseApn mBase; 157 158 public MemoryApn(final List<Apn> apns, final BaseApn base) { 159 mApns = apns; 160 mBase = base; 161 } 162 163 @Override 164 public String getMmsc() { 165 return mBase.getMmsc(); 166 } 167 168 @Override 169 public String getMmsProxy() { 170 return mBase.getMmsProxy(); 171 } 172 173 @Override 174 public int getMmsProxyPort() { 175 return mBase.getMmsProxyPort(); 176 } 177 178 @Override 179 public void setSuccess() { 180 // If this is being marked as a successful APN, move it to the top of the list so 181 // next time it will be tried first 182 boolean moved = false; 183 synchronized (mApns) { 184 if (mApns.get(0) != this) { 185 mApns.remove(this); 186 mApns.add(0, this); 187 moved = true; 188 } 189 } 190 if (moved) { 191 Log.d(MmsService.TAG, "Set APN [" 192 + "MMSC=" + getMmsc() + ", " 193 + "PROXY=" + getMmsProxy() + ", " 194 + "PORT=" + getMmsProxyPort() + "] to be first"); 195 } 196 } 197 198 public boolean equals(final BaseApn other) { 199 if (other == null) { 200 return false; 201 } 202 return mBase.equals(other); 203 } 204 } 205 206 /** 207 * APN_TYPE_ALL is a special type to indicate that this APN entry can 208 * service all data connections. 209 */ 210 public static final String APN_TYPE_ALL = "*"; 211 /** APN type for MMS traffic */ 212 public static final String APN_TYPE_MMS = "mms"; 213 214 private static final String[] APN_PROJECTION = { 215 Telephony.Carriers.TYPE, 216 Telephony.Carriers.MMSC, 217 Telephony.Carriers.MMSPROXY, 218 Telephony.Carriers.MMSPORT, 219 }; 220 private static final int COLUMN_TYPE = 0; 221 private static final int COLUMN_MMSC = 1; 222 private static final int COLUMN_MMSPROXY = 2; 223 private static final int COLUMN_MMSPORT = 3; 224 225 private static final String APN_MCC = "mcc"; 226 private static final String APN_MNC = "mnc"; 227 private static final String APN_APN = "apn"; 228 private static final String APN_TYPE = "type"; 229 private static final String APN_MMSC = "mmsc"; 230 private static final String APN_MMSPROXY = "mmsproxy"; 231 private static final String APN_MMSPORT = "mmsport"; 232 233 private final Context mContext; 234 235 // Cached APNs for subIds 236 private final SparseArray<List<Apn>> mApnsCache; 237 238 DefaultApnSettingsLoader(final Context context) { 239 mContext = context; 240 mApnsCache = new SparseArray<>(); 241 } 242 243 @Override 244 public List<Apn> get(final String apnName) { 245 final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID); 246 List<Apn> apns; 247 boolean didLoad = false; 248 synchronized (this) { 249 apns = mApnsCache.get(subId); 250 if (apns == null) { 251 apns = new ArrayList<>(); 252 mApnsCache.put(subId, apns); 253 loadLocked(subId, apnName, apns); 254 didLoad = true; 255 } 256 } 257 if (didLoad) { 258 Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs"); 259 } 260 return apns; 261 } 262 263 private void loadLocked(final int subId, final String apnName, final List<Apn> apns) { 264 // Try system APN table first 265 loadFromSystem(subId, apnName, apns); 266 if (apns.size() > 0) { 267 return; 268 } 269 // Try loading from apns.xml in resources 270 loadFromResources(subId, apnName, apns); 271 if (apns.size() > 0) { 272 return; 273 } 274 // Try resources but without APN name 275 loadFromResources(subId, null/*apnName*/, apns); 276 } 277 278 /** 279 * Load matching APNs from telephony provider. 280 * We try different combinations of the query to work around some platform quirks. 281 * 282 * @param subId the SIM subId 283 * @param apnName the APN name to match 284 * @param apns the list used to return results 285 */ 286 private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) { 287 Uri uri; 288 if (Utils.supportMSim() && subId != MmsManager.DEFAULT_SUB_ID) { 289 uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId); 290 } else { 291 uri = Telephony.Carriers.CONTENT_URI; 292 } 293 Cursor cursor = null; 294 try { 295 for (; ; ) { 296 // Try different combinations of queries. Some would work on some platforms. 297 // So we query each combination until we find one returns non-empty result. 298 cursor = querySystem(uri, true/*checkCurrent*/, apnName); 299 if (cursor != null) { 300 break; 301 } 302 cursor = querySystem(uri, false/*checkCurrent*/, apnName); 303 if (cursor != null) { 304 break; 305 } 306 cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/); 307 if (cursor != null) { 308 break; 309 } 310 cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/); 311 break; 312 } 313 } catch (final SecurityException e) { 314 // Can't access platform APN table, return directly 315 return; 316 } 317 if (cursor == null) { 318 return; 319 } 320 try { 321 if (cursor.moveToFirst()) { 322 final Apn apn = BaseApn.from( 323 cursor.getString(COLUMN_TYPE), 324 cursor.getString(COLUMN_MMSC), 325 cursor.getString(COLUMN_MMSPROXY), 326 cursor.getString(COLUMN_MMSPORT)); 327 if (apn != null) { 328 apns.add(apn); 329 } 330 } 331 } finally { 332 cursor.close(); 333 } 334 } 335 336 /** 337 * Query system APN table 338 * 339 * @param uri The APN query URL to use 340 * @param checkCurrent If add "CURRENT IS NOT NULL" condition 341 * @param apnName The optional APN name for query condition 342 * @return A cursor of the query result. If a cursor is returned as not null, it is 343 * guaranteed to contain at least one row. 344 */ 345 private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) { 346 Log.i(MmsService.TAG, "Loading APNs from system, " 347 + "checkCurrent=" + checkCurrent + " apnName=" + apnName); 348 final StringBuilder selectionBuilder = new StringBuilder(); 349 String[] selectionArgs = null; 350 if (checkCurrent) { 351 selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL"); 352 } 353 apnName = trimWithNullCheck(apnName); 354 if (!TextUtils.isEmpty(apnName)) { 355 if (selectionBuilder.length() > 0) { 356 selectionBuilder.append(" AND "); 357 } 358 selectionBuilder.append(Telephony.Carriers.APN).append("=?"); 359 selectionArgs = new String[] { apnName }; 360 } 361 try { 362 final Cursor cursor = mContext.getContentResolver().query( 363 uri, 364 APN_PROJECTION, 365 selectionBuilder.toString(), 366 selectionArgs, 367 null/*sortOrder*/); 368 if (cursor == null || cursor.getCount() < 1) { 369 if (cursor != null) { 370 cursor.close(); 371 } 372 Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and " 373 + (checkCurrent ? "checking CURRENT" : "not checking CURRENT") 374 + " returned empty"); 375 return null; 376 } 377 return cursor; 378 } catch (final SQLiteException e) { 379 Log.w(MmsService.TAG, "APN table query exception: " + e); 380 } catch (final SecurityException e) { 381 Log.w(MmsService.TAG, "Platform restricts APN table access: " + e); 382 throw e; 383 } 384 return null; 385 } 386 387 /** 388 * Find matching APNs using builtin APN list resource 389 * 390 * @param subId the SIM subId 391 * @param apnName the APN name to match 392 * @param apns the list for returning results 393 */ 394 private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) { 395 Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName); 396 final int[] mccMnc = Utils.getMccMnc(mContext, subId); 397 if (mccMnc[0] == 0 && mccMnc[0] == 0) { 398 Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system"); 399 return; 400 } 401 // MCC/MNC is good, loading/querying APNs from XML 402 XmlResourceParser xml = null; 403 try { 404 xml = mContext.getResources().getXml(R.xml.apns); 405 new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() { 406 @Override 407 public void process(ContentValues apnValues) { 408 final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC)); 409 final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC)); 410 final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN)); 411 try { 412 if (mccMnc[0] == Integer.parseInt(mcc) && 413 mccMnc[1] == Integer.parseInt(mnc) && 414 (TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) { 415 final String type = apnValues.getAsString(APN_TYPE); 416 final String mmsc = apnValues.getAsString(APN_MMSC); 417 final String mmsproxy = apnValues.getAsString(APN_MMSPROXY); 418 final String mmsport = apnValues.getAsString(APN_MMSPORT); 419 final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport); 420 if (newApn != null) { 421 apns.add(newApn); 422 } 423 } 424 } catch (final NumberFormatException e) { 425 // Ignore 426 } 427 } 428 }).parse(); 429 } catch (final Resources.NotFoundException e) { 430 Log.w(MmsService.TAG, "Can not get apns.xml " + e); 431 } finally { 432 if (xml != null) { 433 xml.close(); 434 } 435 } 436 } 437 438 private static String trimWithNullCheck(final String value) { 439 return value != null ? value.trim() : null; 440 } 441 442 /** 443 * Trim leading zeros from IPv4 address strings 444 * Our base libraries will interpret that as octel.. 445 * Must leave non v4 addresses and host names alone. 446 * For example, 192.168.000.010 -> 192.168.0.10 447 * 448 * @param addr a string representing an ip addr 449 * @return a string propertly trimmed 450 */ 451 private static String trimV4AddrZeros(final String addr) { 452 if (addr == null) { 453 return null; 454 } 455 final String[] octets = addr.split("\\."); 456 if (octets.length != 4) { 457 return addr; 458 } 459 final StringBuilder builder = new StringBuilder(16); 460 String result = null; 461 for (int i = 0; i < 4; i++) { 462 try { 463 if (octets[i].length() > 3) { 464 return addr; 465 } 466 builder.append(Integer.parseInt(octets[i])); 467 } catch (final NumberFormatException e) { 468 return addr; 469 } 470 if (i < 3) { 471 builder.append('.'); 472 } 473 } 474 result = builder.toString(); 475 return result; 476 } 477 478 /** 479 * Check if the APN contains the APN type we want 480 * 481 * @param types The string encodes a list of supported types 482 * @param requestType The type we want 483 * @return true if the input types string contains the requestType 484 */ 485 public static boolean isValidApnType(final String types, final String requestType) { 486 // If APN type is unspecified, assume APN_TYPE_ALL. 487 if (TextUtils.isEmpty(types)) { 488 return true; 489 } 490 for (final String t : types.split(",")) { 491 if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) { 492 return true; 493 } 494 } 495 return false; 496 } 497 } 498