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 com.android.messaging.sms; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteException; 24 import android.net.Uri; 25 import android.provider.Telephony; 26 import android.support.v7.mms.ApnSettingsLoader; 27 import android.support.v7.mms.MmsManager; 28 import android.text.TextUtils; 29 import android.util.SparseArray; 30 31 import com.android.messaging.datamodel.data.ParticipantData; 32 import com.android.messaging.mmslib.SqliteWrapper; 33 import com.android.messaging.util.BugleGservices; 34 import com.android.messaging.util.BugleGservicesKeys; 35 import com.android.messaging.util.LogUtil; 36 import com.android.messaging.util.OsUtil; 37 import com.android.messaging.util.PhoneUtils; 38 39 import java.net.URI; 40 import java.net.URISyntaxException; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * APN loader for default SMS SIM 46 * 47 * This loader tries to load APNs from 3 sources in order: 48 * 1. Gservices setting 49 * 2. System APN table 50 * 3. Local APN table 51 */ 52 public class BugleApnSettingsLoader implements ApnSettingsLoader { 53 /** 54 * The base implementation of an APN 55 */ 56 private static class BaseApn implements Apn { 57 /** 58 * Create a base APN from parameters 59 * 60 * @param typesIn the APN type field 61 * @param mmscIn the APN mmsc field 62 * @param proxyIn the APN mmsproxy field 63 * @param portIn the APN mmsport field 64 * @return an instance of base APN, or null if any of the parameter is invalid 65 */ 66 public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn, 67 final String portIn) { 68 if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) { 69 return null; 70 } 71 String mmsc = trimWithNullCheck(mmscIn); 72 if (TextUtils.isEmpty(mmsc)) { 73 return null; 74 } 75 mmsc = trimV4AddrZeros(mmsc); 76 try { 77 new URI(mmsc); 78 } catch (final URISyntaxException e) { 79 return null; 80 } 81 String mmsProxy = trimWithNullCheck(proxyIn); 82 int mmsProxyPort = 80; 83 if (!TextUtils.isEmpty(mmsProxy)) { 84 mmsProxy = trimV4AddrZeros(mmsProxy); 85 final String portString = trimWithNullCheck(portIn); 86 if (portString != null) { 87 try { 88 mmsProxyPort = Integer.parseInt(portString); 89 } catch (final NumberFormatException e) { 90 // Ignore, just use 80 to try 91 } 92 } 93 } 94 return new BaseApn(mmsc, mmsProxy, mmsProxyPort); 95 } 96 97 private final String mMmsc; 98 private final String mMmsProxy; 99 private final int mMmsProxyPort; 100 101 public BaseApn(final String mmsc, final String proxy, final int port) { 102 mMmsc = mmsc; 103 mMmsProxy = proxy; 104 mMmsProxyPort = port; 105 } 106 107 @Override 108 public String getMmsc() { 109 return mMmsc; 110 } 111 112 @Override 113 public String getMmsProxy() { 114 return mMmsProxy; 115 } 116 117 @Override 118 public int getMmsProxyPort() { 119 return mMmsProxyPort; 120 } 121 122 @Override 123 public void setSuccess() { 124 // Do nothing 125 } 126 127 public boolean equals(final BaseApn other) { 128 return TextUtils.equals(mMmsc, other.getMmsc()) && 129 TextUtils.equals(mMmsProxy, other.getMmsProxy()) && 130 mMmsProxyPort == other.getMmsProxyPort(); 131 } 132 } 133 134 /** 135 * The APN represented by the local APN table row 136 */ 137 private static class DatabaseApn implements Apn { 138 private static final ContentValues CURRENT_NULL_VALUE; 139 private static final ContentValues CURRENT_SET_VALUE; 140 static { 141 CURRENT_NULL_VALUE = new ContentValues(1); 142 CURRENT_NULL_VALUE.putNull(Telephony.Carriers.CURRENT); 143 CURRENT_SET_VALUE = new ContentValues(1); 144 CURRENT_SET_VALUE.put(Telephony.Carriers.CURRENT, "1"); // 1 for auto selected APN 145 } 146 private static final String CLEAR_UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?"; 147 private static final String[] CLEAR_UPDATE_SELECTION_ARGS = new String[] { "1" }; 148 private static final String SET_UPDATE_SELECTION = Telephony.Carriers._ID + " =?"; 149 150 /** 151 * Create an APN loaded from local database 152 * 153 * @param apns the in-memory APN list 154 * @param typesIn the APN type field 155 * @param mmscIn the APN mmsc field 156 * @param proxyIn the APN mmsproxy field 157 * @param portIn the APN mmsport field 158 * @param rowId the APN's row ID in database 159 * @param current the value of CURRENT column in database 160 * @return an in-memory APN instance for database APN row, null if parameter invalid 161 */ 162 public static DatabaseApn from(final List<Apn> apns, final String typesIn, 163 final String mmscIn, final String proxyIn, final String portIn, 164 final long rowId, final int current) { 165 if (apns == null) { 166 return null; 167 } 168 final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn); 169 if (base == null) { 170 return null; 171 } 172 for (final ApnSettingsLoader.Apn apn : apns) { 173 if (apn instanceof DatabaseApn && ((DatabaseApn) apn).equals(base)) { 174 return null; 175 } 176 } 177 return new DatabaseApn(apns, base, rowId, current); 178 } 179 180 private final List<Apn> mApns; 181 private final BaseApn mBase; 182 private final long mRowId; 183 private int mCurrent; 184 185 public DatabaseApn(final List<Apn> apns, final BaseApn base, final long rowId, 186 final int current) { 187 mApns = apns; 188 mBase = base; 189 mRowId = rowId; 190 mCurrent = current; 191 } 192 193 @Override 194 public String getMmsc() { 195 return mBase.getMmsc(); 196 } 197 198 @Override 199 public String getMmsProxy() { 200 return mBase.getMmsProxy(); 201 } 202 203 @Override 204 public int getMmsProxyPort() { 205 return mBase.getMmsProxyPort(); 206 } 207 208 @Override 209 public void setSuccess() { 210 moveToListHead(); 211 setCurrentInDatabase(); 212 } 213 214 /** 215 * Try to move this APN to the head of in-memory list 216 */ 217 private void moveToListHead() { 218 // If this is being marked as a successful APN, move it to the top of the list so 219 // next time it will be tried first 220 boolean moved = false; 221 synchronized (mApns) { 222 if (mApns.get(0) != this) { 223 mApns.remove(this); 224 mApns.add(0, this); 225 moved = true; 226 } 227 } 228 if (moved) { 229 LogUtil.d(LogUtil.BUGLE_TAG, "Set APN [" 230 + "MMSC=" + getMmsc() + ", " 231 + "PROXY=" + getMmsProxy() + ", " 232 + "PORT=" + getMmsProxyPort() + "] to be first"); 233 } 234 } 235 236 /** 237 * Try to set the APN to be CURRENT in its database table 238 */ 239 private void setCurrentInDatabase() { 240 synchronized (this) { 241 if (mCurrent > 0) { 242 // Already current 243 return; 244 } 245 mCurrent = 1; 246 } 247 LogUtil.d(LogUtil.BUGLE_TAG, "Set APN @" + mRowId + " to be CURRENT in local db"); 248 final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); 249 database.beginTransaction(); 250 try { 251 // clear the previous current=1 apn 252 // we don't clear current=2 apn since it is manually selected by user 253 // and we should not override it. 254 database.update(ApnDatabase.APN_TABLE, CURRENT_NULL_VALUE, 255 CLEAR_UPDATE_SELECTION, CLEAR_UPDATE_SELECTION_ARGS); 256 // set this one to be current (1) 257 database.update(ApnDatabase.APN_TABLE, CURRENT_SET_VALUE, SET_UPDATE_SELECTION, 258 new String[] { Long.toString(mRowId) }); 259 database.setTransactionSuccessful(); 260 } finally { 261 database.endTransaction(); 262 } 263 } 264 265 public boolean equals(final BaseApn other) { 266 if (other == null) { 267 return false; 268 } 269 return mBase.equals(other); 270 } 271 } 272 273 /** 274 * APN_TYPE_ALL is a special type to indicate that this APN entry can 275 * service all data connections. 276 */ 277 public static final String APN_TYPE_ALL = "*"; 278 /** APN type for MMS traffic */ 279 public static final String APN_TYPE_MMS = "mms"; 280 281 private static final String[] APN_PROJECTION_SYSTEM = { 282 Telephony.Carriers.TYPE, 283 Telephony.Carriers.MMSC, 284 Telephony.Carriers.MMSPROXY, 285 Telephony.Carriers.MMSPORT, 286 }; 287 private static final String[] APN_PROJECTION_LOCAL = { 288 Telephony.Carriers.TYPE, 289 Telephony.Carriers.MMSC, 290 Telephony.Carriers.MMSPROXY, 291 Telephony.Carriers.MMSPORT, 292 Telephony.Carriers.CURRENT, 293 Telephony.Carriers._ID, 294 }; 295 private static final int COLUMN_TYPE = 0; 296 private static final int COLUMN_MMSC = 1; 297 private static final int COLUMN_MMSPROXY = 2; 298 private static final int COLUMN_MMSPORT = 3; 299 private static final int COLUMN_CURRENT = 4; 300 private static final int COLUMN_ID = 5; 301 302 private static final String SELECTION_APN = Telephony.Carriers.APN + "=?"; 303 private static final String SELECTION_CURRENT = Telephony.Carriers.CURRENT + " IS NOT NULL"; 304 private static final String SELECTION_NUMERIC = Telephony.Carriers.NUMERIC + "=?"; 305 private static final String ORDER_BY = Telephony.Carriers.CURRENT + " DESC"; 306 307 private final Context mContext; 308 309 // Cached APNs for subIds 310 private final SparseArray<List<ApnSettingsLoader.Apn>> mApnsCache; 311 312 public BugleApnSettingsLoader(final Context context) { 313 mContext = context; 314 mApnsCache = new SparseArray<>(); 315 } 316 317 @Override 318 public List<ApnSettingsLoader.Apn> get(final String apnName) { 319 final int subId = PhoneUtils.getDefault().getEffectiveSubId( 320 ParticipantData.DEFAULT_SELF_SUB_ID); 321 List<ApnSettingsLoader.Apn> apns; 322 boolean didLoad = false; 323 synchronized (this) { 324 apns = mApnsCache.get(subId); 325 if (apns == null) { 326 apns = new ArrayList<>(); 327 mApnsCache.put(subId, apns); 328 loadLocked(subId, apnName, apns); 329 didLoad = true; 330 } 331 } 332 if (didLoad) { 333 LogUtil.i(LogUtil.BUGLE_TAG, "Loaded " + apns.size() + " APNs"); 334 } 335 return apns; 336 } 337 338 private void loadLocked(final int subId, final String apnName, final List<Apn> apns) { 339 // Try Gservices first 340 loadFromGservices(apns); 341 if (apns.size() > 0) { 342 return; 343 } 344 // Try system APN table 345 loadFromSystem(subId, apnName, apns); 346 if (apns.size() > 0) { 347 return; 348 } 349 // Try local APN table 350 loadFromLocalDatabase(apnName, apns); 351 if (apns.size() <= 0) { 352 LogUtil.w(LogUtil.BUGLE_TAG, "Failed to load any APN"); 353 } 354 } 355 356 /** 357 * Load from Gservices if APN setting is set in Gservices 358 * 359 * @param apns the list used to return results 360 */ 361 private void loadFromGservices(final List<Apn> apns) { 362 final BugleGservices gservices = BugleGservices.get(); 363 final String mmsc = gservices.getString(BugleGservicesKeys.MMS_MMSC, null); 364 if (TextUtils.isEmpty(mmsc)) { 365 return; 366 } 367 LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from gservices"); 368 final String proxy = gservices.getString(BugleGservicesKeys.MMS_PROXY_ADDRESS, null); 369 final int port = gservices.getInt(BugleGservicesKeys.MMS_PROXY_PORT, -1); 370 final Apn apn = BaseApn.from("mms", mmsc, proxy, Integer.toString(port)); 371 if (apn != null) { 372 apns.add(apn); 373 } 374 } 375 376 /** 377 * Load matching APNs from telephony provider. 378 * We try different combinations of the query to work around some platform quirks. 379 * 380 * @param subId the SIM subId 381 * @param apnName the APN name to match 382 * @param apns the list used to return results 383 */ 384 private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) { 385 Uri uri; 386 if (OsUtil.isAtLeastL_MR1() && subId != MmsManager.DEFAULT_SUB_ID) { 387 uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId); 388 } else { 389 uri = Telephony.Carriers.CONTENT_URI; 390 } 391 Cursor cursor = null; 392 try { 393 for (; ; ) { 394 // Try different combinations of queries. Some would work on some platforms. 395 // So we query each combination until we find one returns non-empty result. 396 cursor = querySystem(uri, true/*checkCurrent*/, apnName); 397 if (cursor != null) { 398 break; 399 } 400 cursor = querySystem(uri, false/*checkCurrent*/, apnName); 401 if (cursor != null) { 402 break; 403 } 404 cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/); 405 if (cursor != null) { 406 break; 407 } 408 cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/); 409 break; 410 } 411 } catch (final SecurityException e) { 412 // Can't access platform APN table, return directly 413 return; 414 } 415 if (cursor == null) { 416 return; 417 } 418 try { 419 if (cursor.moveToFirst()) { 420 final ApnSettingsLoader.Apn apn = BaseApn.from( 421 cursor.getString(COLUMN_TYPE), 422 cursor.getString(COLUMN_MMSC), 423 cursor.getString(COLUMN_MMSPROXY), 424 cursor.getString(COLUMN_MMSPORT)); 425 if (apn != null) { 426 apns.add(apn); 427 } 428 } 429 } finally { 430 cursor.close(); 431 } 432 } 433 434 /** 435 * Query system APN table 436 * 437 * @param uri The APN query URL to use 438 * @param checkCurrent If add "CURRENT IS NOT NULL" condition 439 * @param apnName The optional APN name for query condition 440 * @return A cursor of the query result. If a cursor is returned as not null, it is 441 * guaranteed to contain at least one row. 442 */ 443 private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) { 444 LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from system, " 445 + "checkCurrent=" + checkCurrent + " apnName=" + apnName); 446 final StringBuilder selectionBuilder = new StringBuilder(); 447 String[] selectionArgs = null; 448 if (checkCurrent) { 449 selectionBuilder.append(SELECTION_CURRENT); 450 } 451 apnName = trimWithNullCheck(apnName); 452 if (!TextUtils.isEmpty(apnName)) { 453 if (selectionBuilder.length() > 0) { 454 selectionBuilder.append(" AND "); 455 } 456 selectionBuilder.append(SELECTION_APN); 457 selectionArgs = new String[] { apnName }; 458 } 459 try { 460 final Cursor cursor = SqliteWrapper.query( 461 mContext, 462 mContext.getContentResolver(), 463 uri, 464 APN_PROJECTION_SYSTEM, 465 selectionBuilder.toString(), 466 selectionArgs, 467 null/*sortOrder*/); 468 if (cursor == null || cursor.getCount() < 1) { 469 if (cursor != null) { 470 cursor.close(); 471 } 472 LogUtil.w(LogUtil.BUGLE_TAG, "Query " + uri + " with apn " + apnName + " and " 473 + (checkCurrent ? "checking CURRENT" : "not checking CURRENT") 474 + " returned empty"); 475 return null; 476 } 477 return cursor; 478 } catch (final SQLiteException e) { 479 LogUtil.w(LogUtil.BUGLE_TAG, "APN table query exception: " + e); 480 } catch (final SecurityException e) { 481 LogUtil.w(LogUtil.BUGLE_TAG, "Platform restricts APN table access: " + e); 482 throw e; 483 } 484 return null; 485 } 486 487 /** 488 * Load matching APNs from local APN table. 489 * We try both using the APN name and not using the APN name. 490 * 491 * @param apnName the APN name 492 * @param apns the list of results to return 493 */ 494 private void loadFromLocalDatabase(final String apnName, final List<Apn> apns) { 495 LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from local APN table"); 496 final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); 497 final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.getDefault().getMccMnc()); 498 Cursor cursor = null; 499 cursor = queryLocalDatabase(database, mccMnc, apnName); 500 if (cursor == null) { 501 cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/); 502 } 503 if (cursor == null) { 504 LogUtil.w(LogUtil.BUGLE_TAG, "Could not find any APN in local table"); 505 return; 506 } 507 try { 508 while (cursor.moveToNext()) { 509 final Apn apn = DatabaseApn.from(apns, 510 cursor.getString(COLUMN_TYPE), 511 cursor.getString(COLUMN_MMSC), 512 cursor.getString(COLUMN_MMSPROXY), 513 cursor.getString(COLUMN_MMSPORT), 514 cursor.getLong(COLUMN_ID), 515 cursor.getInt(COLUMN_CURRENT)); 516 if (apn != null) { 517 apns.add(apn); 518 } 519 } 520 } finally { 521 cursor.close(); 522 } 523 } 524 525 /** 526 * Make a query of local APN table based on MCC/MNC and APN name, sorted by CURRENT 527 * column in descending order 528 * 529 * @param db the local database 530 * @param numeric the MCC/MNC string 531 * @param apnName the optional APN name to match 532 * @return the cursor of the query, null if no result 533 */ 534 private static Cursor queryLocalDatabase(final SQLiteDatabase db, final String numeric, 535 final String apnName) { 536 final String selection; 537 final String[] selectionArgs; 538 if (TextUtils.isEmpty(apnName)) { 539 selection = SELECTION_NUMERIC; 540 selectionArgs = new String[] { numeric }; 541 } else { 542 selection = SELECTION_NUMERIC + " AND " + SELECTION_APN; 543 selectionArgs = new String[] { numeric, apnName }; 544 } 545 Cursor cursor = null; 546 try { 547 cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs, 548 null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/); 549 } catch (final SQLiteException e) { 550 LogUtil.w(LogUtil.BUGLE_TAG, "Local APN table does not exist. Try rebuilding.", e); 551 ApnDatabase.forceBuildAndLoadApnTables(); 552 cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs, 553 null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/); 554 } 555 if (cursor == null || cursor.getCount() < 1) { 556 if (cursor != null) { 557 cursor.close(); 558 } 559 LogUtil.w(LogUtil.BUGLE_TAG, "Query local APNs with apn " + apnName 560 + " returned empty"); 561 return null; 562 } 563 return cursor; 564 } 565 566 private static String trimWithNullCheck(final String value) { 567 return value != null ? value.trim() : null; 568 } 569 570 /** 571 * Trim leading zeros from IPv4 address strings 572 * Our base libraries will interpret that as octel.. 573 * Must leave non v4 addresses and host names alone. 574 * For example, 192.168.000.010 -> 192.168.0.10 575 * 576 * @param addr a string representing an ip addr 577 * @return a string propertly trimmed 578 */ 579 private static String trimV4AddrZeros(final String addr) { 580 if (addr == null) { 581 return null; 582 } 583 final String[] octets = addr.split("\\."); 584 if (octets.length != 4) { 585 return addr; 586 } 587 final StringBuilder builder = new StringBuilder(16); 588 String result = null; 589 for (int i = 0; i < 4; i++) { 590 try { 591 if (octets[i].length() > 3) { 592 return addr; 593 } 594 builder.append(Integer.parseInt(octets[i])); 595 } catch (final NumberFormatException e) { 596 return addr; 597 } 598 if (i < 3) { 599 builder.append('.'); 600 } 601 } 602 result = builder.toString(); 603 return result; 604 } 605 606 /** 607 * Check if the APN contains the APN type we want 608 * 609 * @param types The string encodes a list of supported types 610 * @param requestType The type we want 611 * @return true if the input types string contains the requestType 612 */ 613 public static boolean isValidApnType(final String types, final String requestType) { 614 // If APN type is unspecified, assume APN_TYPE_ALL. 615 if (TextUtils.isEmpty(types)) { 616 return true; 617 } 618 for (final String t : types.split(",")) { 619 if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) { 620 return true; 621 } 622 } 623 return false; 624 } 625 626 /** 627 * Get the ID of first APN to try 628 */ 629 public static String getFirstTryApn(final SQLiteDatabase database, final String mccMnc) { 630 String key = null; 631 Cursor cursor = null; 632 try { 633 cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/); 634 if (cursor.moveToFirst()) { 635 key = cursor.getString(ApnDatabase.COLUMN_ID); 636 } 637 } catch (final Exception e) { 638 // Nothing to do 639 } finally { 640 if (cursor != null) { 641 cursor.close(); 642 } 643 } 644 return key; 645 } 646 } 647