1 /* //device/content/providers/telephony/TelephonyProvider.java 2 ** 3 ** Copyright 2006, The Android Open Source Project 4 ** 5 ** Licensed under the Apache License, Version 2.0 (the "License"); 6 ** you may not use this file except in compliance with the License. 7 ** You may obtain a copy of the License at 8 ** 9 ** http://www.apache.org/licenses/LICENSE-2.0 10 ** 11 ** Unless required by applicable law or agreed to in writing, software 12 ** distributed under the License is distributed on an "AS IS" BASIS, 13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 ** See the License for the specific language governing permissions and 15 ** limitations under the License. 16 */ 17 18 package com.android.providers.telephony; 19 20 import android.content.ContentProvider; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.SharedPreferences; 25 import android.content.UriMatcher; 26 import android.content.pm.PackageManager; 27 import android.content.res.Resources; 28 import android.content.res.XmlResourceParser; 29 import android.database.Cursor; 30 import android.database.SQLException; 31 import android.database.sqlite.SQLiteDatabase; 32 import android.database.sqlite.SQLiteException; 33 import android.database.sqlite.SQLiteOpenHelper; 34 import android.database.sqlite.SQLiteQueryBuilder; 35 import android.net.Uri; 36 import android.os.Binder; 37 import android.os.Environment; 38 import android.os.UserHandle; 39 import android.provider.Telephony; 40 import android.telephony.SubscriptionManager; 41 import android.telephony.TelephonyManager; 42 import android.util.Log; 43 import android.util.Xml; 44 45 import com.android.internal.telephony.BaseCommands; 46 import com.android.internal.telephony.Phone; 47 import com.android.internal.telephony.PhoneConstants; 48 import com.android.internal.util.XmlUtils; 49 50 import org.xmlpull.v1.XmlPullParser; 51 import org.xmlpull.v1.XmlPullParserException; 52 53 import java.io.File; 54 import java.io.FileNotFoundException; 55 import java.io.FileReader; 56 import java.io.IOException; 57 import java.lang.NumberFormatException; 58 59 public class TelephonyProvider extends ContentProvider 60 { 61 private static final String DATABASE_NAME = "telephony.db"; 62 private static final boolean DBG = true; 63 private static final boolean VDBG = false; 64 65 private static final int DATABASE_VERSION = 13 << 16; 66 private static final int URL_UNKNOWN = 0; 67 private static final int URL_TELEPHONY = 1; 68 private static final int URL_CURRENT = 2; 69 private static final int URL_ID = 3; 70 private static final int URL_RESTOREAPN = 4; 71 private static final int URL_PREFERAPN = 5; 72 private static final int URL_PREFERAPN_NO_UPDATE = 6; 73 private static final int URL_SIMINFO = 7; 74 private static final int URL_TELEPHONY_USING_SUBID = 8; 75 private static final int URL_CURRENT_USING_SUBID = 9; 76 private static final int URL_RESTOREAPN_USING_SUBID = 10; 77 private static final int URL_PREFERAPN_USING_SUBID = 11; 78 private static final int URL_PREFERAPN_NO_UPDATE_USING_SUBID = 12; 79 private static final int URL_SIMINFO_USING_SUBID = 13; 80 81 private static final String TAG = "TelephonyProvider"; 82 private static final String CARRIERS_TABLE = "carriers"; 83 private static final String SIMINFO_TABLE = "siminfo"; 84 85 private static final String PREF_FILE = "preferred-apn"; 86 private static final String COLUMN_APN_ID = "apn_id"; 87 88 private static final String PARTNER_APNS_PATH = "etc/apns-conf.xml"; 89 private static final String OEM_APNS_PATH = "telephony/apns-conf.xml"; 90 91 private static final UriMatcher s_urlMatcher = new UriMatcher(UriMatcher.NO_MATCH); 92 93 private static final ContentValues s_currentNullMap; 94 private static final ContentValues s_currentSetMap; 95 96 static { 97 s_urlMatcher.addURI("telephony", "carriers", URL_TELEPHONY); 98 s_urlMatcher.addURI("telephony", "carriers/current", URL_CURRENT); 99 s_urlMatcher.addURI("telephony", "carriers/#", URL_ID); 100 s_urlMatcher.addURI("telephony", "carriers/restore", URL_RESTOREAPN); 101 s_urlMatcher.addURI("telephony", "carriers/preferapn", URL_PREFERAPN); 102 s_urlMatcher.addURI("telephony", "carriers/preferapn_no_update", URL_PREFERAPN_NO_UPDATE); 103 104 s_urlMatcher.addURI("telephony", "siminfo", URL_SIMINFO); 105 106 s_urlMatcher.addURI("telephony", "carriers/subId/*", URL_TELEPHONY_USING_SUBID); 107 s_urlMatcher.addURI("telephony", "carriers/current/subId/*", URL_CURRENT_USING_SUBID); 108 s_urlMatcher.addURI("telephony", "carriers/restore/subId/*", URL_RESTOREAPN_USING_SUBID); 109 s_urlMatcher.addURI("telephony", "carriers/preferapn/subId/*", URL_PREFERAPN_USING_SUBID); 110 s_urlMatcher.addURI("telephony", "carriers/preferapn_no_update/subId/*", 111 URL_PREFERAPN_NO_UPDATE_USING_SUBID); 112 113 114 s_currentNullMap = new ContentValues(1); 115 s_currentNullMap.put("current", (Long) null); 116 117 s_currentSetMap = new ContentValues(1); 118 s_currentSetMap.put("current", "1"); 119 } 120 121 private static class DatabaseHelper extends SQLiteOpenHelper { 122 // Context to access resources with 123 private Context mContext; 124 125 /** 126 * DatabaseHelper helper class for loading apns into a database. 127 * 128 * @param context of the user. 129 */ DatabaseHelper(Context context)130 public DatabaseHelper(Context context) { 131 super(context, DATABASE_NAME, null, getVersion(context)); 132 mContext = context; 133 } 134 getVersion(Context context)135 private static int getVersion(Context context) { 136 if (VDBG) log("getVersion:+"); 137 // Get the database version, combining a static schema version and the XML version 138 Resources r = context.getResources(); 139 XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns); 140 try { 141 XmlUtils.beginDocument(parser, "apns"); 142 int publicversion = Integer.parseInt(parser.getAttributeValue(null, "version")); 143 int version = DATABASE_VERSION | publicversion; 144 if (VDBG) log("getVersion:- version=0x" + Integer.toHexString(version)); 145 return version; 146 } catch (Exception e) { 147 loge("Can't get version of APN database" + e + " return version=" + 148 Integer.toHexString(DATABASE_VERSION)); 149 return DATABASE_VERSION; 150 } finally { 151 parser.close(); 152 } 153 } 154 155 @Override onCreate(SQLiteDatabase db)156 public void onCreate(SQLiteDatabase db) { 157 if (DBG) log("dbh.onCreate:+ db=" + db); 158 createSimInfoTable(db); 159 createCarriersTable(db); 160 initDatabase(db); 161 if (DBG) log("dbh.onCreate:- db=" + db); 162 } 163 164 @Override onOpen(SQLiteDatabase db)165 public void onOpen(SQLiteDatabase db) { 166 if (VDBG) log("dbh.onOpen:+ db=" + db); 167 try { 168 // Try to access the table and create it if "no such table" 169 db.query(SIMINFO_TABLE, null, null, null, null, null, null); 170 if (DBG) log("dbh.onOpen: ok, queried table=" + SIMINFO_TABLE); 171 } catch (SQLiteException e) { 172 loge("Exception " + SIMINFO_TABLE + "e=" + e); 173 if (e.getMessage().startsWith("no such table")) { 174 createSimInfoTable(db); 175 } 176 } 177 try { 178 db.query(CARRIERS_TABLE, null, null, null, null, null, null); 179 if (DBG) log("dbh.onOpen: ok, queried table=" + CARRIERS_TABLE); 180 } catch (SQLiteException e) { 181 loge("Exception " + CARRIERS_TABLE + " e=" + e); 182 if (e.getMessage().startsWith("no such table")) { 183 createCarriersTable(db); 184 } 185 } 186 if (VDBG) log("dbh.onOpen:- db=" + db); 187 } 188 createSimInfoTable(SQLiteDatabase db)189 private void createSimInfoTable(SQLiteDatabase db) { 190 if (DBG) log("dbh.createSimInfoTable:+"); 191 db.execSQL("CREATE TABLE " + SIMINFO_TABLE + "(" 192 + SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 193 + SubscriptionManager.ICC_ID + " TEXT NOT NULL," 194 + SubscriptionManager.SIM_SLOT_INDEX + " INTEGER DEFAULT " + SubscriptionManager.SIM_NOT_INSERTED + "," 195 + SubscriptionManager.DISPLAY_NAME + " TEXT," 196 + SubscriptionManager.CARRIER_NAME + " TEXT," 197 + SubscriptionManager.NAME_SOURCE + " INTEGER DEFAULT " + SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE + "," 198 + SubscriptionManager.COLOR + " INTEGER DEFAULT " + SubscriptionManager.COLOR_DEFAULT + "," 199 + SubscriptionManager.NUMBER + " TEXT," 200 + SubscriptionManager.DISPLAY_NUMBER_FORMAT + " INTEGER NOT NULL DEFAULT " + SubscriptionManager.DISPLAY_NUMBER_DEFAULT + "," 201 + SubscriptionManager.DATA_ROAMING + " INTEGER DEFAULT " + SubscriptionManager.DATA_ROAMING_DEFAULT + "," 202 + SubscriptionManager.MCC + " INTEGER DEFAULT 0," 203 + SubscriptionManager.MNC + " INTEGER DEFAULT 0" 204 + ");"); 205 if (DBG) log("dbh.createSimInfoTable:-"); 206 } 207 createCarriersTable(SQLiteDatabase db)208 private void createCarriersTable(SQLiteDatabase db) { 209 // Set up the database schema 210 if (DBG) log("dbh.createCarriersTable:+"); 211 db.execSQL("CREATE TABLE " + CARRIERS_TABLE + 212 "(_id INTEGER PRIMARY KEY," + 213 "name TEXT," + 214 "numeric TEXT," + 215 "mcc TEXT," + 216 "mnc TEXT," + 217 "apn TEXT," + 218 "user TEXT," + 219 "server TEXT," + 220 "password TEXT," + 221 "proxy TEXT," + 222 "port TEXT," + 223 "mmsproxy TEXT," + 224 "mmsport TEXT," + 225 "mmsc TEXT," + 226 "authtype INTEGER," + 227 "type TEXT," + 228 "current INTEGER," + 229 "protocol TEXT," + 230 "roaming_protocol TEXT," + 231 "carrier_enabled BOOLEAN," + 232 "bearer INTEGER," + 233 "mvno_type TEXT," + 234 "mvno_match_data TEXT," + 235 "sub_id INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID + "," + 236 "profile_id INTEGER default 0," + 237 "modem_cognitive BOOLEAN default 0," + 238 "max_conns INTEGER default 0," + 239 "wait_time INTEGER default 0," + 240 "max_conns_time INTEGER default 0," + 241 "mtu INTEGER);"); 242 if (DBG) log("dbh.createCarriersTable:-"); 243 } 244 initDatabase(SQLiteDatabase db)245 private void initDatabase(SQLiteDatabase db) { 246 if (VDBG) log("dbh.initDatabase:+ db=" + db); 247 // Read internal APNS data 248 Resources r = mContext.getResources(); 249 XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns); 250 int publicversion = -1; 251 try { 252 XmlUtils.beginDocument(parser, "apns"); 253 publicversion = Integer.parseInt(parser.getAttributeValue(null, "version")); 254 loadApns(db, parser); 255 } catch (Exception e) { 256 loge("Got exception while loading APN database." + e); 257 } finally { 258 parser.close(); 259 } 260 261 // Read external APNS data (partner-provided) 262 XmlPullParser confparser = null; 263 // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system". 264 File confFile = new File(Environment.getRootDirectory(), PARTNER_APNS_PATH); 265 File oemConfFile = new File(Environment.getOemDirectory(), OEM_APNS_PATH); 266 if (oemConfFile.exists()) { 267 // OEM image exist APN xml, get the timestamp from OEM & System image for comparison 268 long oemApnTime = oemConfFile.lastModified(); 269 long sysApnTime = confFile.lastModified(); 270 if (DBG) log("APNs Timestamp: oemTime = " + oemApnTime + " sysTime = " 271 + sysApnTime); 272 273 // To get the latest version from OEM or System image 274 if (oemApnTime > sysApnTime) { 275 if (DBG) log("APNs Timestamp: OEM image is greater than System image"); 276 confFile = oemConfFile; 277 } 278 } else { 279 // No Apn in OEM image, so load it from system image. 280 if (DBG) log("No APNs in OEM image = " + oemConfFile.getPath() + 281 " Load APNs from system image"); 282 } 283 284 FileReader confreader = null; 285 if (DBG) log("confFile = " + confFile); 286 try { 287 confreader = new FileReader(confFile); 288 confparser = Xml.newPullParser(); 289 confparser.setInput(confreader); 290 XmlUtils.beginDocument(confparser, "apns"); 291 292 // Sanity check. Force internal version and confidential versions to agree 293 int confversion = Integer.parseInt(confparser.getAttributeValue(null, "version")); 294 if (publicversion != confversion) { 295 throw new IllegalStateException("Internal APNS file version doesn't match " 296 + confFile.getAbsolutePath()); 297 } 298 299 loadApns(db, confparser); 300 } catch (FileNotFoundException e) { 301 // It's ok if the file isn't found. It means there isn't a confidential file 302 // Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'"); 303 } catch (Exception e) { 304 loge("Exception while parsing '" + confFile.getAbsolutePath() + "'" + e); 305 } finally { 306 try { if (confreader != null) confreader.close(); } catch (IOException e) { } 307 } 308 if (VDBG) log("dbh.initDatabase:- db=" + db); 309 310 } 311 312 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)313 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 314 if (DBG) { 315 log("dbh.onUpgrade:+ db=" + db + " oldV=" + oldVersion + " newV=" + newVersion); 316 } 317 318 if (oldVersion < (5 << 16 | 6)) { 319 // 5 << 16 is the Database version and 6 in the xml version. 320 321 // This change adds a new authtype column to the database. 322 // The auth type column can have 4 values: 0 (None), 1 (PAP), 2 (CHAP) 323 // 3 (PAP or CHAP). To avoid breaking compatibility, with already working 324 // APNs, the unset value (-1) will be used. If the value is -1. 325 // the authentication will default to 0 (if no user / password) is specified 326 // or to 3. Currently, there have been no reported problems with 327 // pre-configured APNs and hence it is set to -1 for them. Similarly, 328 // if the user, has added a new APN, we set the authentication type 329 // to -1. 330 331 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 332 " ADD COLUMN authtype INTEGER DEFAULT -1;"); 333 334 oldVersion = 5 << 16 | 6; 335 } 336 if (oldVersion < (6 << 16 | 6)) { 337 // Add protcol fields to the APN. The XML file does not change. 338 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 339 " ADD COLUMN protocol TEXT DEFAULT IP;"); 340 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 341 " ADD COLUMN roaming_protocol TEXT DEFAULT IP;"); 342 oldVersion = 6 << 16 | 6; 343 } 344 if (oldVersion < (7 << 16 | 6)) { 345 // Add carrier_enabled, bearer fields to the APN. The XML file does not change. 346 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 347 " ADD COLUMN carrier_enabled BOOLEAN DEFAULT 1;"); 348 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 349 " ADD COLUMN bearer INTEGER DEFAULT 0;"); 350 oldVersion = 7 << 16 | 6; 351 } 352 if (oldVersion < (8 << 16 | 6)) { 353 // Add mvno_type, mvno_match_data fields to the APN. 354 // The XML file does not change. 355 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 356 " ADD COLUMN mvno_type TEXT DEFAULT '';"); 357 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 358 " ADD COLUMN mvno_match_data TEXT DEFAULT '';"); 359 oldVersion = 8 << 16 | 6; 360 } 361 if (oldVersion < (9 << 16 | 6)) { 362 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 363 " ADD COLUMN sub_id INTEGER DEFAULT " + 364 SubscriptionManager.INVALID_SUBSCRIPTION_ID + ";"); 365 oldVersion = 9 << 16 | 6; 366 } 367 if (oldVersion < (10 << 16 | 6)) { 368 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 369 " ADD COLUMN profile_id INTEGER DEFAULT 0;"); 370 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 371 " ADD COLUMN modem_cognitive BOOLEAN DEFAULT 0;"); 372 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 373 " ADD COLUMN max_conns INTEGER DEFAULT 0;"); 374 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 375 " ADD COLUMN wait_time INTEGER DEFAULT 0;"); 376 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 377 " ADD COLUMN max_conns_time INTEGER DEFAULT 0;"); 378 oldVersion = 10 << 16 | 6; 379 } 380 if (oldVersion < (11 << 16 | 6)) { 381 db.execSQL("ALTER TABLE " + CARRIERS_TABLE + 382 " ADD COLUMN mtu INTEGER DEFAULT 0;"); 383 oldVersion = 11 << 16 | 6; 384 } 385 if (oldVersion < (12 << 16 | 6)) { 386 try { 387 // Try to update the siminfo table. It might not be there. 388 db.execSQL("ALTER TABLE " + SIMINFO_TABLE + 389 " ADD COLUMN " + SubscriptionManager.MCC + " INTEGER DEFAULT 0;"); 390 db.execSQL("ALTER TABLE " + SIMINFO_TABLE + 391 " ADD COLUMN " + SubscriptionManager.MNC + " INTEGER DEFAULT 0;"); 392 } catch (SQLiteException e) { 393 if (DBG) { 394 log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " + 395 " The table will get created in onOpen."); 396 } 397 } 398 oldVersion = 12 << 16 | 6; 399 } 400 if (oldVersion < (13 << 16 | 6)) { 401 try { 402 // Try to update the siminfo table. It might not be there. 403 db.execSQL("ALTER TABLE " + SIMINFO_TABLE + 404 " ADD COLUMN " + SubscriptionManager.CARRIER_NAME + " TEXT DEFAULT '';"); 405 } catch (SQLiteException e) { 406 if (DBG) { 407 log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " + 408 " The table will get created in onOpen."); 409 } 410 } 411 oldVersion = 13 << 16 | 6; 412 } 413 if (DBG) { 414 log("dbh.onUpgrade:- db=" + db + " oldV=" + oldVersion + " newV=" + newVersion); 415 } 416 } 417 418 /** 419 * Gets the next row of apn values. 420 * 421 * @param parser the parser 422 * @return the row or null if it's not an apn 423 */ getRow(XmlPullParser parser)424 private ContentValues getRow(XmlPullParser parser) { 425 if (!"apn".equals(parser.getName())) { 426 return null; 427 } 428 429 ContentValues map = new ContentValues(); 430 431 String mcc = parser.getAttributeValue(null, "mcc"); 432 String mnc = parser.getAttributeValue(null, "mnc"); 433 String numeric = mcc + mnc; 434 435 map.put(Telephony.Carriers.NUMERIC,numeric); 436 map.put(Telephony.Carriers.MCC, mcc); 437 map.put(Telephony.Carriers.MNC, mnc); 438 map.put(Telephony.Carriers.NAME, parser.getAttributeValue(null, "carrier")); 439 map.put(Telephony.Carriers.APN, parser.getAttributeValue(null, "apn")); 440 map.put(Telephony.Carriers.USER, parser.getAttributeValue(null, "user")); 441 map.put(Telephony.Carriers.SERVER, parser.getAttributeValue(null, "server")); 442 map.put(Telephony.Carriers.PASSWORD, parser.getAttributeValue(null, "password")); 443 444 // do not add NULL to the map so that insert() will set the default value 445 String proxy = parser.getAttributeValue(null, "proxy"); 446 if (proxy != null) { 447 map.put(Telephony.Carriers.PROXY, proxy); 448 } 449 String port = parser.getAttributeValue(null, "port"); 450 if (port != null) { 451 map.put(Telephony.Carriers.PORT, port); 452 } 453 String mmsproxy = parser.getAttributeValue(null, "mmsproxy"); 454 if (mmsproxy != null) { 455 map.put(Telephony.Carriers.MMSPROXY, mmsproxy); 456 } 457 String mmsport = parser.getAttributeValue(null, "mmsport"); 458 if (mmsport != null) { 459 map.put(Telephony.Carriers.MMSPORT, mmsport); 460 } 461 map.put(Telephony.Carriers.MMSC, parser.getAttributeValue(null, "mmsc")); 462 String type = parser.getAttributeValue(null, "type"); 463 if (type != null) { 464 map.put(Telephony.Carriers.TYPE, type); 465 } 466 467 String auth = parser.getAttributeValue(null, "authtype"); 468 if (auth != null) { 469 map.put(Telephony.Carriers.AUTH_TYPE, Integer.parseInt(auth)); 470 } 471 472 String protocol = parser.getAttributeValue(null, "protocol"); 473 if (protocol != null) { 474 map.put(Telephony.Carriers.PROTOCOL, protocol); 475 } 476 477 String roamingProtocol = parser.getAttributeValue(null, "roaming_protocol"); 478 if (roamingProtocol != null) { 479 map.put(Telephony.Carriers.ROAMING_PROTOCOL, roamingProtocol); 480 } 481 482 String carrierEnabled = parser.getAttributeValue(null, "carrier_enabled"); 483 if (carrierEnabled != null) { 484 map.put(Telephony.Carriers.CARRIER_ENABLED, Boolean.parseBoolean(carrierEnabled)); 485 } 486 487 String bearer = parser.getAttributeValue(null, "bearer"); 488 if (bearer != null) { 489 map.put(Telephony.Carriers.BEARER, Integer.parseInt(bearer)); 490 } 491 492 String mvno_type = parser.getAttributeValue(null, "mvno_type"); 493 if (mvno_type != null) { 494 String mvno_match_data = parser.getAttributeValue(null, "mvno_match_data"); 495 if (mvno_match_data != null) { 496 map.put(Telephony.Carriers.MVNO_TYPE, mvno_type); 497 map.put(Telephony.Carriers.MVNO_MATCH_DATA, mvno_match_data); 498 } 499 } 500 501 String profileId = parser.getAttributeValue(null, "profile_id"); 502 if (profileId != null) { 503 map.put(Telephony.Carriers.PROFILE_ID, Integer.parseInt(profileId)); 504 } 505 506 String modemCognitive = parser.getAttributeValue(null, "modem_cognitive"); 507 if (modemCognitive != null) { 508 map.put(Telephony.Carriers.MODEM_COGNITIVE, Boolean.parseBoolean(modemCognitive)); 509 } 510 511 String maxConns = parser.getAttributeValue(null, "max_conns"); 512 if (maxConns != null) { 513 map.put(Telephony.Carriers.MAX_CONNS, Integer.parseInt(maxConns)); 514 } 515 516 String waitTime = parser.getAttributeValue(null, "wait_time"); 517 if (waitTime != null) { 518 map.put(Telephony.Carriers.WAIT_TIME, Integer.parseInt(waitTime)); 519 } 520 521 String maxConnsTime = parser.getAttributeValue(null, "max_conns_time"); 522 if (maxConnsTime != null) { 523 map.put(Telephony.Carriers.MAX_CONNS_TIME, Integer.parseInt(maxConnsTime)); 524 } 525 526 String mtu = parser.getAttributeValue(null, "mtu"); 527 if (mtu != null) { 528 map.put(Telephony.Carriers.MTU, Integer.parseInt(mtu)); 529 } 530 531 return map; 532 } 533 534 /* 535 * Loads apns from xml file into the database 536 * 537 * @param db the sqlite database to write to 538 * @param parser the xml parser 539 * 540 */ loadApns(SQLiteDatabase db, XmlPullParser parser)541 private void loadApns(SQLiteDatabase db, XmlPullParser parser) { 542 if (parser != null) { 543 try { 544 db.beginTransaction(); 545 XmlUtils.nextElement(parser); 546 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { 547 ContentValues row = getRow(parser); 548 if (row == null) { 549 throw new XmlPullParserException("Expected 'apn' tag", parser, null); 550 } 551 insertAddingDefaults(db, CARRIERS_TABLE, row); 552 XmlUtils.nextElement(parser); 553 } 554 db.setTransactionSuccessful(); 555 } catch (XmlPullParserException e) { 556 loge("Got XmlPullParserException while loading apns." + e); 557 } catch (IOException e) { 558 loge("Got IOException while loading apns." + e); 559 } catch (SQLException e) { 560 loge("Got SQLException while loading apns." + e); 561 } finally { 562 db.endTransaction(); 563 } 564 } 565 } 566 setDefaultValue(ContentValues values)567 static public ContentValues setDefaultValue(ContentValues values) { 568 if (!values.containsKey(Telephony.Carriers.NAME)) { 569 values.put(Telephony.Carriers.NAME, ""); 570 } 571 if (!values.containsKey(Telephony.Carriers.APN)) { 572 values.put(Telephony.Carriers.APN, ""); 573 } 574 if (!values.containsKey(Telephony.Carriers.PORT)) { 575 values.put(Telephony.Carriers.PORT, ""); 576 } 577 if (!values.containsKey(Telephony.Carriers.PROXY)) { 578 values.put(Telephony.Carriers.PROXY, ""); 579 } 580 if (!values.containsKey(Telephony.Carriers.USER)) { 581 values.put(Telephony.Carriers.USER, ""); 582 } 583 if (!values.containsKey(Telephony.Carriers.SERVER)) { 584 values.put(Telephony.Carriers.SERVER, ""); 585 } 586 if (!values.containsKey(Telephony.Carriers.PASSWORD)) { 587 values.put(Telephony.Carriers.PASSWORD, ""); 588 } 589 if (!values.containsKey(Telephony.Carriers.MMSPORT)) { 590 values.put(Telephony.Carriers.MMSPORT, ""); 591 } 592 if (!values.containsKey(Telephony.Carriers.MMSPROXY)) { 593 values.put(Telephony.Carriers.MMSPROXY, ""); 594 } 595 if (!values.containsKey(Telephony.Carriers.AUTH_TYPE)) { 596 values.put(Telephony.Carriers.AUTH_TYPE, -1); 597 } 598 if (!values.containsKey(Telephony.Carriers.PROTOCOL)) { 599 values.put(Telephony.Carriers.PROTOCOL, "IP"); 600 } 601 if (!values.containsKey(Telephony.Carriers.ROAMING_PROTOCOL)) { 602 values.put(Telephony.Carriers.ROAMING_PROTOCOL, "IP"); 603 } 604 if (!values.containsKey(Telephony.Carriers.CARRIER_ENABLED)) { 605 values.put(Telephony.Carriers.CARRIER_ENABLED, true); 606 } 607 if (!values.containsKey(Telephony.Carriers.BEARER)) { 608 values.put(Telephony.Carriers.BEARER, 0); 609 } 610 if (!values.containsKey(Telephony.Carriers.MVNO_TYPE)) { 611 values.put(Telephony.Carriers.MVNO_TYPE, ""); 612 } 613 if (!values.containsKey(Telephony.Carriers.MVNO_MATCH_DATA)) { 614 values.put(Telephony.Carriers.MVNO_MATCH_DATA, ""); 615 } 616 617 int subId = SubscriptionManager.getDefaultSubId(); 618 if (!values.containsKey(Telephony.Carriers.SUBSCRIPTION_ID)) { 619 values.put(Telephony.Carriers.SUBSCRIPTION_ID, subId); 620 } 621 622 if (!values.containsKey(Telephony.Carriers.PROFILE_ID)) { 623 values.put(Telephony.Carriers.PROFILE_ID, 0); 624 } 625 if (!values.containsKey(Telephony.Carriers.MODEM_COGNITIVE)) { 626 values.put(Telephony.Carriers.MODEM_COGNITIVE, false); 627 } 628 if (!values.containsKey(Telephony.Carriers.MAX_CONNS)) { 629 values.put(Telephony.Carriers.MAX_CONNS, 0); 630 } 631 if (!values.containsKey(Telephony.Carriers.WAIT_TIME)) { 632 values.put(Telephony.Carriers.WAIT_TIME, 0); 633 } 634 if (!values.containsKey(Telephony.Carriers.MAX_CONNS_TIME)) { 635 values.put(Telephony.Carriers.MAX_CONNS_TIME, 0); 636 } 637 638 return values; 639 } 640 insertAddingDefaults(SQLiteDatabase db, String table, ContentValues row)641 private void insertAddingDefaults(SQLiteDatabase db, String table, ContentValues row) { 642 row = setDefaultValue(row); 643 db.insert(CARRIERS_TABLE, null, row); 644 } 645 } 646 647 @Override onCreate()648 public boolean onCreate() { 649 if (VDBG) log("onCreate:+"); 650 mOpenHelper = new DatabaseHelper(getContext()); 651 if (VDBG) log("onCreate:- ret true"); 652 return true; 653 } 654 setPreferredApnId(Long id, int subId)655 private void setPreferredApnId(Long id, int subId) { 656 SharedPreferences sp = getContext().getSharedPreferences( 657 PREF_FILE + subId, Context.MODE_PRIVATE); 658 SharedPreferences.Editor editor = sp.edit(); 659 editor.putLong(COLUMN_APN_ID, id != null ? id.longValue() : -1); 660 editor.apply(); 661 } 662 getPreferredApnId(int subId)663 private long getPreferredApnId(int subId) { 664 SharedPreferences sp = getContext().getSharedPreferences( 665 PREF_FILE + subId, Context.MODE_PRIVATE); 666 return sp.getLong(COLUMN_APN_ID, -1); 667 } 668 669 @Override query(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sort)670 public Cursor query(Uri url, String[] projectionIn, String selection, 671 String[] selectionArgs, String sort) { 672 TelephonyManager mTelephonyManager = 673 (TelephonyManager)getContext().getSystemService(Context.TELEPHONY_SERVICE); 674 int subId = SubscriptionManager.getDefaultSubId(); 675 String subIdString; 676 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 677 qb.setStrict(true); // a little protection from injection attacks 678 qb.setTables("carriers"); 679 680 int match = s_urlMatcher.match(url); 681 switch (match) { 682 case URL_TELEPHONY_USING_SUBID: { 683 subIdString = url.getLastPathSegment(); 684 try { 685 subId = Integer.parseInt(subIdString); 686 } catch (NumberFormatException e) { 687 loge("NumberFormatException" + e); 688 return null; 689 } 690 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 691 qb.appendWhere("numeric = '" + mTelephonyManager.getSimOperator(subId)+"'"); 692 // FIXME alter the selection to pass subId 693 // selection = selection + "and subId = " 694 } 695 //intentional fall through from above case 696 // do nothing 697 case URL_TELEPHONY: { 698 break; 699 } 700 701 case URL_CURRENT_USING_SUBID: { 702 subIdString = url.getLastPathSegment(); 703 try { 704 subId = Integer.parseInt(subIdString); 705 } catch (NumberFormatException e) { 706 loge("NumberFormatException" + e); 707 return null; 708 } 709 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 710 // FIXME alter the selection to pass subId 711 // selection = selection + "and subId = " 712 } 713 //intentional fall through from above case 714 case URL_CURRENT: { 715 qb.appendWhere("current IS NOT NULL"); 716 // do not ignore the selection since MMS may use it. 717 //selection = null; 718 break; 719 } 720 721 case URL_ID: { 722 qb.appendWhere("_id = " + url.getPathSegments().get(1)); 723 break; 724 } 725 726 case URL_PREFERAPN_USING_SUBID: 727 case URL_PREFERAPN_NO_UPDATE_USING_SUBID: { 728 subIdString = url.getLastPathSegment(); 729 try { 730 subId = Integer.parseInt(subIdString); 731 } catch (NumberFormatException e) { 732 loge("NumberFormatException" + e); 733 return null; 734 } 735 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 736 } 737 //intentional fall through from above case 738 case URL_PREFERAPN: 739 case URL_PREFERAPN_NO_UPDATE: { 740 qb.appendWhere("_id = " + getPreferredApnId(subId)); 741 break; 742 } 743 744 case URL_SIMINFO: { 745 qb.setTables(SIMINFO_TABLE); 746 break; 747 } 748 749 default: { 750 return null; 751 } 752 } 753 754 if (match != URL_SIMINFO) { 755 if (projectionIn != null) { 756 for (String column : projectionIn) { 757 if (Telephony.Carriers.TYPE.equals(column) || 758 Telephony.Carriers.MMSC.equals(column) || 759 Telephony.Carriers.MMSPROXY.equals(column) || 760 Telephony.Carriers.MMSPORT.equals(column) || 761 Telephony.Carriers.APN.equals(column)) { 762 // noop 763 } else { 764 checkPermission(); 765 break; 766 } 767 } 768 } else { 769 // null returns all columns, so need permission check 770 checkPermission(); 771 } 772 } 773 774 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 775 Cursor ret = null; 776 try { 777 ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort); 778 } catch (SQLException e) { 779 loge("got exception when querying: " + e); 780 } 781 if (ret != null) 782 ret.setNotificationUri(getContext().getContentResolver(), url); 783 return ret; 784 } 785 786 @Override getType(Uri url)787 public String getType(Uri url) 788 { 789 switch (s_urlMatcher.match(url)) { 790 case URL_TELEPHONY: 791 case URL_TELEPHONY_USING_SUBID: 792 return "vnd.android.cursor.dir/telephony-carrier"; 793 794 case URL_ID: 795 return "vnd.android.cursor.item/telephony-carrier"; 796 797 case URL_PREFERAPN_USING_SUBID: 798 case URL_PREFERAPN_NO_UPDATE_USING_SUBID: 799 case URL_PREFERAPN: 800 case URL_PREFERAPN_NO_UPDATE: 801 return "vnd.android.cursor.item/telephony-carrier"; 802 803 default: 804 throw new IllegalArgumentException("Unknown URL " + url); 805 } 806 } 807 808 @Override insert(Uri url, ContentValues initialValues)809 public Uri insert(Uri url, ContentValues initialValues) 810 { 811 Uri result = null; 812 int subId = SubscriptionManager.getDefaultSubId(); 813 814 checkPermission(); 815 816 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 817 int match = s_urlMatcher.match(url); 818 boolean notify = false; 819 switch (match) 820 { 821 case URL_TELEPHONY_USING_SUBID: 822 { 823 String subIdString = url.getLastPathSegment(); 824 try { 825 subId = Integer.parseInt(subIdString); 826 } catch (NumberFormatException e) { 827 loge("NumberFormatException" + e); 828 return result; 829 } 830 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 831 } 832 //intentional fall through from above case 833 834 case URL_TELEPHONY: 835 { 836 ContentValues values; 837 if (initialValues != null) { 838 values = new ContentValues(initialValues); 839 } else { 840 values = new ContentValues(); 841 } 842 843 values = DatabaseHelper.setDefaultValue(values); 844 845 long rowID = db.insert(CARRIERS_TABLE, null, values); 846 if (rowID > 0) 847 { 848 result = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, rowID); 849 notify = true; 850 } 851 852 if (VDBG) log("inserted " + values.toString() + " rowID = " + rowID); 853 break; 854 } 855 856 case URL_CURRENT_USING_SUBID: 857 { 858 String subIdString = url.getLastPathSegment(); 859 try { 860 subId = Integer.parseInt(subIdString); 861 } catch (NumberFormatException e) { 862 loge("NumberFormatException" + e); 863 return result; 864 } 865 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 866 // FIXME use subId in the query 867 } 868 //intentional fall through from above case 869 870 case URL_CURRENT: 871 { 872 // null out the previous operator 873 db.update("carriers", s_currentNullMap, "current IS NOT NULL", null); 874 875 String numeric = initialValues.getAsString("numeric"); 876 int updated = db.update("carriers", s_currentSetMap, 877 "numeric = '" + numeric + "'", null); 878 879 if (updated > 0) 880 { 881 if (VDBG) log("Setting numeric '" + numeric + "' to be the current operator"); 882 } 883 else 884 { 885 loge("Failed setting numeric '" + numeric + "' to the current operator"); 886 } 887 break; 888 } 889 890 case URL_PREFERAPN_USING_SUBID: 891 case URL_PREFERAPN_NO_UPDATE_USING_SUBID: 892 { 893 String subIdString = url.getLastPathSegment(); 894 try { 895 subId = Integer.parseInt(subIdString); 896 } catch (NumberFormatException e) { 897 loge("NumberFormatException" + e); 898 return result; 899 } 900 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 901 } 902 //intentional fall through from above case 903 904 case URL_PREFERAPN: 905 case URL_PREFERAPN_NO_UPDATE: 906 { 907 if (initialValues != null) { 908 if(initialValues.containsKey(COLUMN_APN_ID)) { 909 setPreferredApnId(initialValues.getAsLong(COLUMN_APN_ID), subId); 910 } 911 } 912 break; 913 } 914 915 case URL_SIMINFO: { 916 long id = db.insert(SIMINFO_TABLE, null, initialValues); 917 result = ContentUris.withAppendedId(SubscriptionManager.CONTENT_URI, id); 918 break; 919 } 920 } 921 922 if (notify) { 923 getContext().getContentResolver().notifyChange(Telephony.Carriers.CONTENT_URI, null, 924 true, UserHandle.USER_ALL); 925 } 926 927 return result; 928 } 929 930 @Override delete(Uri url, String where, String[] whereArgs)931 public int delete(Uri url, String where, String[] whereArgs) 932 { 933 int count = 0; 934 int subId = SubscriptionManager.getDefaultSubId(); 935 936 checkPermission(); 937 938 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 939 int match = s_urlMatcher.match(url); 940 switch (match) 941 { 942 case URL_TELEPHONY_USING_SUBID: 943 { 944 String subIdString = url.getLastPathSegment(); 945 try { 946 subId = Integer.parseInt(subIdString); 947 } catch (NumberFormatException e) { 948 loge("NumberFormatException" + e); 949 throw new IllegalArgumentException("Invalid subId " + url); 950 } 951 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 952 // FIXME use subId in query 953 } 954 //intentional fall through from above case 955 956 case URL_TELEPHONY: 957 { 958 count = db.delete(CARRIERS_TABLE, where, whereArgs); 959 break; 960 } 961 962 case URL_CURRENT_USING_SUBID: { 963 String subIdString = url.getLastPathSegment(); 964 try { 965 subId = Integer.parseInt(subIdString); 966 } catch (NumberFormatException e) { 967 loge("NumberFormatException" + e); 968 throw new IllegalArgumentException("Invalid subId " + url); 969 } 970 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 971 // FIXME use subId in query 972 } 973 //intentional fall through from above case 974 975 case URL_CURRENT: 976 { 977 count = db.delete(CARRIERS_TABLE, where, whereArgs); 978 break; 979 } 980 981 case URL_ID: 982 { 983 count = db.delete(CARRIERS_TABLE, Telephony.Carriers._ID + "=?", 984 new String[] { url.getLastPathSegment() }); 985 break; 986 } 987 988 case URL_RESTOREAPN_USING_SUBID: { 989 String subIdString = url.getLastPathSegment(); 990 try { 991 subId = Integer.parseInt(subIdString); 992 } catch (NumberFormatException e) { 993 loge("NumberFormatException" + e); 994 throw new IllegalArgumentException("Invalid subId " + url); 995 } 996 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 997 // FIXME use subId in query 998 } 999 case URL_RESTOREAPN: { 1000 count = 1; 1001 restoreDefaultAPN(subId); 1002 break; 1003 } 1004 1005 case URL_PREFERAPN_USING_SUBID: 1006 case URL_PREFERAPN_NO_UPDATE_USING_SUBID: { 1007 String subIdString = url.getLastPathSegment(); 1008 try { 1009 subId = Integer.parseInt(subIdString); 1010 } catch (NumberFormatException e) { 1011 loge("NumberFormatException" + e); 1012 throw new IllegalArgumentException("Invalid subId " + url); 1013 } 1014 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 1015 } 1016 //intentional fall through from above case 1017 1018 case URL_PREFERAPN: 1019 case URL_PREFERAPN_NO_UPDATE: 1020 { 1021 setPreferredApnId((long)-1, subId); 1022 if ((match == URL_PREFERAPN) || (match == URL_PREFERAPN_USING_SUBID)) count = 1; 1023 break; 1024 } 1025 1026 case URL_SIMINFO: { 1027 count = db.delete(SIMINFO_TABLE, where, whereArgs); 1028 break; 1029 } 1030 1031 default: { 1032 throw new UnsupportedOperationException("Cannot delete that URL: " + url); 1033 } 1034 } 1035 1036 if (count > 0) { 1037 getContext().getContentResolver().notifyChange(Telephony.Carriers.CONTENT_URI, null, 1038 true, UserHandle.USER_ALL); 1039 } 1040 1041 return count; 1042 } 1043 1044 @Override update(Uri url, ContentValues values, String where, String[] whereArgs)1045 public int update(Uri url, ContentValues values, String where, String[] whereArgs) 1046 { 1047 int count = 0; 1048 int uriType = URL_UNKNOWN; 1049 int subId = SubscriptionManager.getDefaultSubId(); 1050 1051 checkPermission(); 1052 1053 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1054 int match = s_urlMatcher.match(url); 1055 switch (match) 1056 { 1057 case URL_TELEPHONY_USING_SUBID: 1058 { 1059 String subIdString = url.getLastPathSegment(); 1060 try { 1061 subId = Integer.parseInt(subIdString); 1062 } catch (NumberFormatException e) { 1063 loge("NumberFormatException" + e); 1064 throw new IllegalArgumentException("Invalid subId " + url); 1065 } 1066 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 1067 //FIXME use subId in the query 1068 } 1069 //intentional fall through from above case 1070 1071 case URL_TELEPHONY: 1072 { 1073 count = db.update(CARRIERS_TABLE, values, where, whereArgs); 1074 break; 1075 } 1076 1077 case URL_CURRENT_USING_SUBID: 1078 { 1079 String subIdString = url.getLastPathSegment(); 1080 try { 1081 subId = Integer.parseInt(subIdString); 1082 } catch (NumberFormatException e) { 1083 loge("NumberFormatException" + e); 1084 throw new IllegalArgumentException("Invalid subId " + url); 1085 } 1086 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 1087 //FIXME use subId in the query 1088 } 1089 //intentional fall through from above case 1090 1091 case URL_CURRENT: 1092 { 1093 count = db.update(CARRIERS_TABLE, values, where, whereArgs); 1094 break; 1095 } 1096 1097 case URL_ID: 1098 { 1099 if (where != null || whereArgs != null) { 1100 throw new UnsupportedOperationException( 1101 "Cannot update URL " + url + " with a where clause"); 1102 } 1103 count = db.update(CARRIERS_TABLE, values, Telephony.Carriers._ID + "=?", 1104 new String[] { url.getLastPathSegment() }); 1105 break; 1106 } 1107 1108 case URL_PREFERAPN_USING_SUBID: 1109 case URL_PREFERAPN_NO_UPDATE_USING_SUBID: 1110 { 1111 String subIdString = url.getLastPathSegment(); 1112 try { 1113 subId = Integer.parseInt(subIdString); 1114 } catch (NumberFormatException e) { 1115 loge("NumberFormatException" + e); 1116 throw new IllegalArgumentException("Invalid subId " + url); 1117 } 1118 if (DBG) log("subIdString = " + subIdString + " subId = " + subId); 1119 } 1120 1121 case URL_PREFERAPN: 1122 case URL_PREFERAPN_NO_UPDATE: 1123 { 1124 if (values != null) { 1125 if (values.containsKey(COLUMN_APN_ID)) { 1126 setPreferredApnId(values.getAsLong(COLUMN_APN_ID), subId); 1127 if ((match == URL_PREFERAPN) || 1128 (match == URL_PREFERAPN_USING_SUBID)) { 1129 count = 1; 1130 } 1131 } 1132 } 1133 break; 1134 } 1135 1136 case URL_SIMINFO: { 1137 count = db.update(SIMINFO_TABLE, values, where, whereArgs); 1138 uriType = URL_SIMINFO; 1139 break; 1140 } 1141 1142 default: { 1143 throw new UnsupportedOperationException("Cannot update that URL: " + url); 1144 } 1145 } 1146 1147 if (count > 0) { 1148 switch (uriType) { 1149 case URL_SIMINFO: 1150 getContext().getContentResolver().notifyChange( 1151 SubscriptionManager.CONTENT_URI, null, true, UserHandle.USER_ALL); 1152 break; 1153 default: 1154 getContext().getContentResolver().notifyChange( 1155 Telephony.Carriers.CONTENT_URI, null, true, UserHandle.USER_ALL); 1156 } 1157 } 1158 1159 return count; 1160 } 1161 checkPermission()1162 private void checkPermission() { 1163 int status = getContext().checkCallingOrSelfPermission( 1164 "android.permission.WRITE_APN_SETTINGS"); 1165 if (status == PackageManager.PERMISSION_GRANTED) { 1166 return; 1167 } 1168 1169 PackageManager packageManager = getContext().getPackageManager(); 1170 String[] packages = packageManager.getPackagesForUid(Binder.getCallingUid()); 1171 1172 TelephonyManager telephonyManager = 1173 (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE); 1174 for (String pkg : packages) { 1175 if (telephonyManager.checkCarrierPrivilegesForPackage(pkg) == 1176 TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { 1177 return; 1178 } 1179 } 1180 throw new SecurityException("No permission to write APN settings"); 1181 } 1182 1183 private DatabaseHelper mOpenHelper; 1184 restoreDefaultAPN(int subId)1185 private void restoreDefaultAPN(int subId) { 1186 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1187 1188 try { 1189 db.delete(CARRIERS_TABLE, null, null); 1190 } catch (SQLException e) { 1191 loge("got exception when deleting to restore: " + e); 1192 } 1193 setPreferredApnId((long)-1, subId); 1194 mOpenHelper.initDatabase(db); 1195 } 1196 1197 /** 1198 * Log with debug 1199 * 1200 * @param s is string log 1201 */ log(String s)1202 private static void log(String s) { 1203 Log.d(TAG, s); 1204 } 1205 loge(String s)1206 private static void loge(String s) { 1207 Log.e(TAG, s); 1208 } 1209 } 1210