Change to add new column and unique constraint to carriers db.

Includes changes to ensure APN DB updates over the air does not affect
user/app edited entries and unique contraint avoids duplicate entries.

Change-Id: I7cae3d229445607c66b6472124264588f7571097
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..85e15e3
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- persist APNs for these PLMNs on db upgrade to version 14 -->
+    <string-array name="persist_apns_for_plmn">
+        <item>20404</item>
+        <item>310120</item>
+        <item>311480</item>
+    </string-array>
+</resources>
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
index 64ab430..e604196 100644
--- a/src/com/android/providers/telephony/TelephonyProvider.java
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -35,16 +35,15 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Environment;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Telephony;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.Xml;
 
-import com.android.internal.telephony.BaseCommands;
-import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -55,14 +54,16 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.lang.NumberFormatException;
+import java.util.ArrayList;
+import java.util.Arrays;
 
 public class TelephonyProvider extends ContentProvider
 {
     private static final String DATABASE_NAME = "telephony.db";
     private static final boolean DBG = true;
-    private static final boolean VDBG = false;
+    private static final boolean VDBG = false; // STOPSHIP if true
 
-    private static final int DATABASE_VERSION = 13 << 16;
+    private static final int DATABASE_VERSION = 14 << 16;
     private static final int URL_UNKNOWN = 0;
     private static final int URL_TELEPHONY = 1;
     private static final int URL_CURRENT = 2;
@@ -77,14 +78,19 @@
     private static final int URL_PREFERAPN_USING_SUBID = 11;
     private static final int URL_PREFERAPN_NO_UPDATE_USING_SUBID = 12;
     private static final int URL_SIMINFO_USING_SUBID = 13;
+    private static final int URL_UPDATE_DB = 14;
 
     private static final String TAG = "TelephonyProvider";
     private static final String CARRIERS_TABLE = "carriers";
+    private static final String CARRIERS_TABLE_TMP = "carriers_tmp";
     private static final String SIMINFO_TABLE = "siminfo";
 
     private static final String PREF_FILE = "preferred-apn";
     private static final String COLUMN_APN_ID = "apn_id";
 
+    private static final String BUILD_ID_FILE = "build-id";
+    private static final String RO_BUILD_ID = "ro_build_id";
+
     private static final String PARTNER_APNS_PATH = "etc/apns-conf.xml";
     private static final String OEM_APNS_PATH = "telephony/apns-conf.xml";
 
@@ -93,6 +99,14 @@
     private static final ContentValues s_currentNullMap;
     private static final ContentValues s_currentSetMap;
 
+    private static final int USER_EDITED_UNTOUCHED = 0;
+    private static final int USER_EDITED_EDITED = 1;
+    private static final int USER_EDITED_DELETED = 2;
+        // DELETED_BUT_PRESENT is an intermediate value used to indicate that an entry deleted
+        // by the user is still present in the new APN database and therefore must remain tagged
+        // as user deleted rather than completely removed from the database
+    private static final int USER_EDITED_DELETED_BUT_PRESENT = 3;
+
     static {
         s_urlMatcher.addURI("telephony", "carriers", URL_TELEPHONY);
         s_urlMatcher.addURI("telephony", "carriers/current", URL_CURRENT);
@@ -110,12 +124,13 @@
         s_urlMatcher.addURI("telephony", "carriers/preferapn_no_update/subId/*",
                 URL_PREFERAPN_NO_UPDATE_USING_SUBID);
 
+        s_urlMatcher.addURI("telephony", "carriers/update_db", URL_UPDATE_DB);
 
         s_currentNullMap = new ContentValues(1);
-        s_currentNullMap.put("current", (Long) null);
+        s_currentNullMap.put(Telephony.Carriers.CURRENT, "0");
 
         s_currentSetMap = new ContentValues(1);
-        s_currentSetMap.put("current", "1");
+        s_currentSetMap.put(Telephony.Carriers.CURRENT, "1");
     }
 
     private static class DatabaseHelper extends SQLiteOpenHelper {
@@ -156,7 +171,7 @@
         public void onCreate(SQLiteDatabase db) {
             if (DBG) log("dbh.onCreate:+ db=" + db);
             createSimInfoTable(db);
-            createCarriersTable(db);
+            createCarriersTable(db, CARRIERS_TABLE);
             initDatabase(db);
             if (DBG) log("dbh.onCreate:- db=" + db);
         }
@@ -180,7 +195,7 @@
             } catch (SQLiteException e) {
                 loge("Exception " + CARRIERS_TABLE + " e=" + e);
                 if (e.getMessage().startsWith("no such table")) {
-                    createCarriersTable(db);
+                    createCarriersTable(db, CARRIERS_TABLE);
                 }
             }
             if (VDBG) log("dbh.onOpen:- db=" + db);
@@ -205,40 +220,49 @@
             if (DBG) log("dbh.createSimInfoTable:-");
         }
 
-        private void createCarriersTable(SQLiteDatabase db) {
+        private void createCarriersTable(SQLiteDatabase db, String tableName) {
             // Set up the database schema
-            if (DBG) log("dbh.createCarriersTable:+");
-            db.execSQL("CREATE TABLE " + CARRIERS_TABLE +
+            if (DBG) log("dbh.createCarriersTable: " + tableName);
+            db.execSQL("CREATE TABLE " + tableName +
                 "(_id INTEGER PRIMARY KEY," +
-                    "name TEXT," +
-                    "numeric TEXT," +
-                    "mcc TEXT," +
-                    "mnc TEXT," +
-                    "apn TEXT," +
-                    "user TEXT," +
-                    "server TEXT," +
-                    "password TEXT," +
-                    "proxy TEXT," +
-                    "port TEXT," +
-                    "mmsproxy TEXT," +
-                    "mmsport TEXT," +
-                    "mmsc TEXT," +
-                    "authtype INTEGER," +
-                    "type TEXT," +
+                    "name TEXT DEFAULT ''," +
+                    "numeric TEXT DEFAULT ''," +
+                    "mcc TEXT DEFAULT ''," +
+                    "mnc TEXT DEFAULT ''," +
+                    "apn TEXT DEFAULT ''," +
+                    "user TEXT DEFAULT ''," +
+                    "server TEXT DEFAULT ''," +
+                    "password TEXT DEFAULT ''," +
+                    "proxy TEXT DEFAULT ''," +
+                    "port TEXT DEFAULT ''," +
+                    "mmsproxy TEXT DEFAULT ''," +
+                    "mmsport TEXT DEFAULT ''," +
+                    "mmsc TEXT DEFAULT ''," +
+                    "authtype INTEGER DEFAULT -1," +
+                    "type TEXT DEFAULT ''," +
                     "current INTEGER," +
-                    "protocol TEXT," +
-                    "roaming_protocol TEXT," +
-                    "carrier_enabled BOOLEAN," +
-                    "bearer INTEGER," +
-                    "mvno_type TEXT," +
-                    "mvno_match_data TEXT," +
+                    "protocol TEXT DEFAULT 'IP'," +
+                    "roaming_protocol TEXT DEFAULT 'IP'," +
+                    "carrier_enabled BOOLEAN DEFAULT 1," +
+                    "bearer INTEGER DEFAULT 0," +
+                    "mvno_type TEXT DEFAULT ''," +
+                    "mvno_match_data TEXT DEFAULT ''," +
                     "sub_id INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID + "," +
-                    "profile_id INTEGER default 0," +
-                    "modem_cognitive BOOLEAN default 0," +
-                    "max_conns INTEGER default 0," +
-                    "wait_time INTEGER default 0," +
-                    "max_conns_time INTEGER default 0," +
-                    "mtu INTEGER);");
+                    "profile_id INTEGER DEFAULT 0," +
+                    "modem_cognitive BOOLEAN DEFAULT 0," +
+                    "max_conns INTEGER DEFAULT 0," +
+                    "wait_time INTEGER DEFAULT 0," +
+                    "max_conns_time INTEGER DEFAULT 0," +
+                    "mtu INTEGER DEFAULT 0," +
+                    "user_edited INTEGER DEFAULT " + USER_EDITED_UNTOUCHED + "," +
+                    // Uniqueness collisions are used to trigger merge code so
+                    // if a field is listed
+                    // here it means we will accept both (user edited + new apn_conf definition)
+                    // Columns not included in UNIQUE constraint: name, current, user_edited,
+                    // user, server, password, authtype, type, protocol, roaming_protocol, sub_id,
+                    // modem_cognitive, max_conns, wait_time, max_conns_time, mtu
+                    "UNIQUE (numeric, mcc, mnc, apn, proxy, port, mmsproxy, mmsport, mmsc," +
+                    "carrier_enabled, bearer, mvno_type, mvno_match_data, profile_id));");
             if (DBG) log("dbh.createCarriersTable:-");
         }
 
@@ -292,6 +316,7 @@
                 // Sanity check. Force internal version and confidential versions to agree
                 int confversion = Integer.parseInt(confparser.getAttributeValue(null, "version"));
                 if (publicversion != confversion) {
+                    log("initDatabase: throwing exception due to version mismatch");
                     throw new IllegalStateException("Internal APNS file version doesn't match "
                             + confFile.getAbsolutePath());
                 }
@@ -301,9 +326,31 @@
                 // It's ok if the file isn't found. It means there isn't a confidential file
                 // Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'");
             } catch (Exception e) {
-                loge("Exception while parsing '" + confFile.getAbsolutePath() + "'" + e);
+                loge("initDatabase: Exception while parsing '" + confFile.getAbsolutePath() + "'" +
+                        e);
             } finally {
-                try { if (confreader != null) confreader.close(); } catch (IOException e) { }
+                // Get rid of user deleted entries that are not present in apn xml file. Those
+                // entries have user_edited value USER_EDITED_DELETED.
+                // Update user_edited value from USER_EDITED_DELETED_BUT_PRESENT
+                // to USER_EDITED_DELETED.
+                // USER_EDITED_DELETED_BUT_PRESENT indicates rows deleted by user but still
+                // present in the xml file. Mark them as user deleted (2).
+                if (VDBG) {
+                    log("initDatabase: deleting USER_EDITED_DELETED and replacing "
+                            + "USER_EDITED_DELETED_BUT_PRESENT with USER_EDITED_DELETED");
+                }
+                db.delete(CARRIERS_TABLE, "user_edited=" + USER_EDITED_DELETED, null);
+                ContentValues cv = new ContentValues();
+                cv.put(Telephony.Carriers.USER_EDITED, USER_EDITED_DELETED);
+                db.update(CARRIERS_TABLE, cv, "user_edited=" + USER_EDITED_DELETED_BUT_PRESENT,
+                        null);
+                if (confreader != null) {
+                    try {
+                        confreader.close();
+                    } catch (IOException e) {
+                        // do nothing
+                    }
+                }
             }
             if (VDBG) log("dbh.initDatabase:- db=" + db);
 
@@ -400,8 +447,8 @@
             if (oldVersion < (13 << 16 | 6)) {
                 try {
                     // Try to update the siminfo table. It might not be there.
-                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE +
-                            " ADD COLUMN " + SubscriptionManager.CARRIER_NAME + " TEXT DEFAULT '';");
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN " +
+                            SubscriptionManager.CARRIER_NAME + " TEXT DEFAULT '';");
                 } catch (SQLiteException e) {
                     if (DBG) {
                         log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
@@ -410,11 +457,218 @@
                 }
                 oldVersion = 13 << 16 | 6;
             }
+            if (oldVersion < (14 << 16 | 6)) {
+                Cursor c = db.query(CARRIERS_TABLE, null, null, null, null, null, null);
+                if (VDBG) {
+                    log("dbh.onUpgrade:- before upgrading total number of rows: " + c.getCount());
+                }
+
+                createCarriersTable(db, CARRIERS_TABLE_TMP);
+
+                // Move entries from CARRIERS_TABLE to CARRIERS_TABLE_TMP
+                if (c != null) {
+                    String[] persistApnsForPlmns = mContext.getResources().getStringArray(
+                            R.array.persist_apns_for_plmn);
+                    while (c.moveToNext()) {
+                        ContentValues cv = new ContentValues();
+                        String val;
+
+                        // Include only non-null values in cv so that null values can be replaced
+                        // with default if there's a default value for the field
+
+                        // String vals
+                        val = getValueFromCursor(c, Telephony.Carriers.NAME);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.NAME, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.NUMERIC);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.NUMERIC, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MCC);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MCC, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MNC);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MNC, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.APN);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.APN, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.USER);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.USER, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.SERVER);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.SERVER, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.PASSWORD);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.PASSWORD, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.PROXY);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.PROXY, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.PORT);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.PORT, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MMSPROXY);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MMSPROXY, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MMSPORT);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MMSPORT, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MMSC);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MMSC, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.TYPE);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.TYPE, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.PROTOCOL);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.PROTOCOL, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.ROAMING_PROTOCOL);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.ROAMING_PROTOCOL, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MVNO_TYPE);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MVNO_TYPE, val);
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MVNO_MATCH_DATA);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MVNO_MATCH_DATA, val);
+                        }
+
+                        // bool/int vals
+                        val = getValueFromCursor(c, Telephony.Carriers.AUTH_TYPE);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.AUTH_TYPE, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.CURRENT);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.CURRENT, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.CARRIER_ENABLED);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.CARRIER_ENABLED, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.BEARER);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.BEARER, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.SUBSCRIPTION_ID);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.SUBSCRIPTION_ID, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.PROFILE_ID);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.PROFILE_ID, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MODEM_COGNITIVE);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MODEM_COGNITIVE, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MAX_CONNS);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MAX_CONNS, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.WAIT_TIME);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.WAIT_TIME, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MAX_CONNS_TIME);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MAX_CONNS_TIME, new Integer(val));
+                        }
+                        val = getValueFromCursor(c, Telephony.Carriers.MTU);
+                        if (val != null) {
+                            cv.put(Telephony.Carriers.MTU, new Integer(val));
+                        }
+
+                        // New USER_EDITED column. Default value (USER_EDITED_UNTOUCHED) will
+                        // be used for all rows except for non-mvno entries for plmns indicated
+                        // by resource: those will be set to USER_EDITED_EDITED to preserve
+                        // their current values
+                        val = c.getString(c.getColumnIndex(Telephony.Carriers.NUMERIC));
+                        for (String s : persistApnsForPlmns) {
+                            if (!TextUtils.isEmpty(val) && val.equals(s) &&
+                                    (!cv.containsKey(Telephony.Carriers.MVNO_TYPE) ||
+                                            TextUtils.isEmpty(cv.getAsString(Telephony.Carriers.
+                                                    MVNO_TYPE)))) {
+                                cv.put(Telephony.Carriers.USER_EDITED, USER_EDITED_EDITED);
+                                break;
+                            }
+                        }
+
+                        try {
+                            db.insertWithOnConflict(CARRIERS_TABLE_TMP, null, cv,
+                                    SQLiteDatabase.CONFLICT_ABORT);
+                            if (VDBG) {
+                                log("dbh.onUpgrade: db.insert returned >= 0; insert "
+                                        + "successful for cv " + cv);
+                            }
+                        } catch (SQLException e) {
+                            if (VDBG) log("dbh.onUpgrade insertWithOnConflict exception " + e);
+                            // Insertion failed which could be due to a conflict. Check if that is
+                            // the case and merge the entries
+                            Cursor oldRow = DatabaseHelper.selectConflictingRow(db,
+                                    CARRIERS_TABLE_TMP, cv);
+                            if (oldRow != null) {
+                                ContentValues mergedValues = new ContentValues();
+                                mergeFieldsAndUpdateDb(db, CARRIERS_TABLE_TMP, oldRow, cv,
+                                        mergedValues, true);
+                                oldRow.close();
+                            }
+                        }
+                    }
+                    c.close();
+                }
+
+                db.execSQL("DROP TABLE IF EXISTS " + CARRIERS_TABLE);
+
+                db.execSQL("ALTER TABLE " + CARRIERS_TABLE_TMP + " rename to " + CARRIERS_TABLE +
+                        ";");
+
+                oldVersion = 14 << 16 | 6;
+
+                if (VDBG) {
+                    String[] proj = {"_id"};
+                    c = db.query(CARRIERS_TABLE, proj, null, null, null, null, null);
+                    log("dbh.onUpgrade:- after upgrading total number of rows: " + c.getCount());
+                    c.close();
+                    c = db.query(CARRIERS_TABLE, proj, "user_edited=" + USER_EDITED_UNTOUCHED, null,
+                            null, null, null);
+                    log("dbh.onUpgrade:- after upgrading total number of rows with user_edited="
+                            + USER_EDITED_UNTOUCHED + ": " + c.getCount());
+                    c.close();
+                    c = db.query(CARRIERS_TABLE, proj, "user_edited!=" + USER_EDITED_UNTOUCHED,
+                            null, null, null, null);
+                    log("dbh.onUpgrade:- after upgrading total number of rows with user_edited!="
+                            + USER_EDITED_UNTOUCHED + ": " + c.getCount());
+                    c.close();
+                }
+            }
             if (DBG) {
                 log("dbh.onUpgrade:- db=" + db + " oldV=" + oldVersion + " newV=" + newVersion);
             }
         }
 
+        private String getValueFromCursor(Cursor c, String key) {
+            String fromCursor = c.getString(c.getColumnIndex(key));
+            return !TextUtils.isEmpty(fromCursor) ? fromCursor : null;
+        }
+
         /**
          * Gets the next row of apn values.
          *
@@ -432,33 +686,57 @@
             String mnc = parser.getAttributeValue(null, "mnc");
             String numeric = mcc + mnc;
 
-            map.put(Telephony.Carriers.NUMERIC,numeric);
+            map.put(Telephony.Carriers.NUMERIC, numeric);
             map.put(Telephony.Carriers.MCC, mcc);
             map.put(Telephony.Carriers.MNC, mnc);
             map.put(Telephony.Carriers.NAME, parser.getAttributeValue(null, "carrier"));
-            map.put(Telephony.Carriers.APN, parser.getAttributeValue(null, "apn"));
-            map.put(Telephony.Carriers.USER, parser.getAttributeValue(null, "user"));
-            map.put(Telephony.Carriers.SERVER, parser.getAttributeValue(null, "server"));
-            map.put(Telephony.Carriers.PASSWORD, parser.getAttributeValue(null, "password"));
 
-            // do not add NULL to the map so that insert() will set the default value
+            // do not add NULL to the map so that default values can be inserted in db
+            String apn = parser.getAttributeValue(null, "apn");
+            if (apn != null) {
+                map.put(Telephony.Carriers.APN, apn);
+            }
+
+            String user = parser.getAttributeValue(null, "user");
+            if (user != null) {
+                map.put(Telephony.Carriers.USER, user);
+            }
+
+            String server = parser.getAttributeValue(null, "server");
+            if (server != null) {
+                map.put(Telephony.Carriers.SERVER, server);
+            }
+
+            String password = parser.getAttributeValue(null, "password");
+            if (password != null) {
+                map.put(Telephony.Carriers.PASSWORD, password);
+            }
+
             String proxy = parser.getAttributeValue(null, "proxy");
             if (proxy != null) {
                 map.put(Telephony.Carriers.PROXY, proxy);
             }
+
             String port = parser.getAttributeValue(null, "port");
             if (port != null) {
                 map.put(Telephony.Carriers.PORT, port);
             }
+
             String mmsproxy = parser.getAttributeValue(null, "mmsproxy");
             if (mmsproxy != null) {
                 map.put(Telephony.Carriers.MMSPROXY, mmsproxy);
             }
+
             String mmsport = parser.getAttributeValue(null, "mmsport");
             if (mmsport != null) {
                 map.put(Telephony.Carriers.MMSPORT, mmsport);
             }
-            map.put(Telephony.Carriers.MMSC, parser.getAttributeValue(null, "mmsc"));
+
+            String mmsc = parser.getAttributeValue(null, "mmsc");
+            if (mmsc != null) {
+                map.put(Telephony.Carriers.MMSC, mmsc);
+            }
+
             String type = parser.getAttributeValue(null, "type");
             if (type != null) {
                 map.put(Telephony.Carriers.TYPE, type);
@@ -548,7 +826,7 @@
                         if (row == null) {
                             throw new XmlPullParserException("Expected 'apn' tag", parser, null);
                         }
-                        insertAddingDefaults(db, CARRIERS_TABLE, row);
+                        insertAddingDefaults(db, row);
                         XmlUtils.nextElement(parser);
                     }
                     db.setTransactionSuccessful();
@@ -565,117 +843,232 @@
         }
 
         static public ContentValues setDefaultValue(ContentValues values) {
-            if (!values.containsKey(Telephony.Carriers.NAME)) {
-                values.put(Telephony.Carriers.NAME, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.APN)) {
-                values.put(Telephony.Carriers.APN, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.PORT)) {
-                values.put(Telephony.Carriers.PORT, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.PROXY)) {
-                values.put(Telephony.Carriers.PROXY, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.USER)) {
-                values.put(Telephony.Carriers.USER, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.SERVER)) {
-                values.put(Telephony.Carriers.SERVER, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.PASSWORD)) {
-                values.put(Telephony.Carriers.PASSWORD, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.MMSPORT)) {
-                values.put(Telephony.Carriers.MMSPORT, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.MMSPROXY)) {
-                values.put(Telephony.Carriers.MMSPROXY, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.AUTH_TYPE)) {
-                values.put(Telephony.Carriers.AUTH_TYPE, -1);
-            }
-            if (!values.containsKey(Telephony.Carriers.PROTOCOL)) {
-                values.put(Telephony.Carriers.PROTOCOL, "IP");
-            }
-            if (!values.containsKey(Telephony.Carriers.ROAMING_PROTOCOL)) {
-                values.put(Telephony.Carriers.ROAMING_PROTOCOL, "IP");
-            }
-            if (!values.containsKey(Telephony.Carriers.CARRIER_ENABLED)) {
-                values.put(Telephony.Carriers.CARRIER_ENABLED, true);
-            }
-            if (!values.containsKey(Telephony.Carriers.BEARER)) {
-                values.put(Telephony.Carriers.BEARER, 0);
-            }
-            if (!values.containsKey(Telephony.Carriers.MVNO_TYPE)) {
-                values.put(Telephony.Carriers.MVNO_TYPE, "");
-            }
-            if (!values.containsKey(Telephony.Carriers.MVNO_MATCH_DATA)) {
-                values.put(Telephony.Carriers.MVNO_MATCH_DATA, "");
-            }
-
-            int subId = SubscriptionManager.getDefaultSubId();
             if (!values.containsKey(Telephony.Carriers.SUBSCRIPTION_ID)) {
+                int subId = SubscriptionManager.getDefaultSubId();
                 values.put(Telephony.Carriers.SUBSCRIPTION_ID, subId);
             }
 
-            if (!values.containsKey(Telephony.Carriers.PROFILE_ID)) {
-                values.put(Telephony.Carriers.PROFILE_ID, 0);
-            }
-            if (!values.containsKey(Telephony.Carriers.MODEM_COGNITIVE)) {
-                values.put(Telephony.Carriers.MODEM_COGNITIVE, false);
-            }
-            if (!values.containsKey(Telephony.Carriers.MAX_CONNS)) {
-                values.put(Telephony.Carriers.MAX_CONNS, 0);
-            }
-            if (!values.containsKey(Telephony.Carriers.WAIT_TIME)) {
-                values.put(Telephony.Carriers.WAIT_TIME, 0);
-            }
-            if (!values.containsKey(Telephony.Carriers.MAX_CONNS_TIME)) {
-                values.put(Telephony.Carriers.MAX_CONNS_TIME, 0);
-            }
-
             return values;
         }
 
-        private void insertAddingDefaults(SQLiteDatabase db, String table, ContentValues row) {
+        private void insertAddingDefaults(SQLiteDatabase db, ContentValues row) {
             row = setDefaultValue(row);
-            db.insert(CARRIERS_TABLE, null, row);
+            try {
+                db.insertWithOnConflict(CARRIERS_TABLE, null, row,
+                        SQLiteDatabase.CONFLICT_ABORT);
+                if (VDBG) log("dbh.insertAddingDefaults: db.insert returned >= 0; insert " +
+                        "successful for cv " + row);
+            } catch (SQLException e) {
+                if (VDBG) log("dbh.insertAddingDefaults: exception " + e);
+                // Insertion failed which could be due to a conflict. Check if that is the case and
+                // update user_edited field accordingly.
+                // Search for the exact same entry and update user_edited field.
+                // If it is USER_EDITED_EDITED change it to USER_EDITED_UNTOUCHED,
+                // and if USER_EDITED_DELETED change it to USER_EDITED_DELETED_BUT_PRESENT.
+                Cursor oldRow = selectConflictingRow(db, CARRIERS_TABLE, row);
+                if (oldRow != null) {
+                    // Update the row
+                    ContentValues mergedValues = new ContentValues();
+                    int user_edited = oldRow.getInt(oldRow.getColumnIndex(
+                            Telephony.Carriers.USER_EDITED));
+                    int old_user_edited = user_edited;
+                    if (user_edited != USER_EDITED_UNTOUCHED) {
+                        if (user_edited == USER_EDITED_EDITED) {
+                            user_edited = USER_EDITED_UNTOUCHED;
+                        } else if (user_edited == USER_EDITED_DELETED) {
+                            // user_edited 3 indicates entry has been deleted by user but present in
+                            // apn xml file.
+                            user_edited = USER_EDITED_DELETED_BUT_PRESENT;
+                        }
+                        mergedValues.put(Telephony.Carriers.USER_EDITED, user_edited);
+                    }
+
+                    mergeFieldsAndUpdateDb(db, CARRIERS_TABLE, oldRow, row, mergedValues, false);
+
+                    if (VDBG) log("dbh.insertAddingDefaults: old user_edited  = " + old_user_edited
+                            + " new user_edited = " + user_edited);
+
+                    oldRow.close();
+                }
+            }
+        }
+
+        static public void mergeFieldsAndUpdateDb(SQLiteDatabase db, String table, Cursor oldRow,
+                                                  ContentValues newRow, ContentValues mergedValues,
+                                                  boolean useOld) {
+            if (newRow.containsKey(Telephony.Carriers.TYPE)) {
+                // Merge the types
+                String oldType = oldRow.getString(oldRow.getColumnIndex(Telephony.Carriers.TYPE));
+                String newType = newRow.getAsString(Telephony.Carriers.TYPE);
+                if (!oldType.equalsIgnoreCase(newType)) {
+                    if (oldType.equals("") || newType.equals("")) {
+                        newRow.put(Telephony.Carriers.TYPE, "");
+                    } else {
+                        // Merge the 2 types
+                        String[] oldTypes = oldType.toLowerCase().split(",");
+                        String[] newTypes = newType.toLowerCase().split(",");
+                        ArrayList<String> mergedTypes = new ArrayList<String>();
+                        mergedTypes.addAll(Arrays.asList(oldTypes));
+                        for (String s : newTypes) {
+                            if (!mergedTypes.contains(s.trim())) {
+                                mergedTypes.add(s);
+                            }
+                        }
+                        StringBuilder mergedType = new StringBuilder();
+                        for (int i = 0; i < mergedTypes.size(); i++) {
+                            mergedType.append((i == 0 ? "" : ",") + mergedTypes.get(i));
+                        }
+                        newRow.put(Telephony.Carriers.TYPE, mergedType.toString());
+                    }
+                }
+                mergedValues.put(Telephony.Carriers.TYPE, newRow.getAsString(
+                        Telephony.Carriers.TYPE));
+            }
+
+            if (!useOld) {
+                mergedValues.putAll(newRow);
+            }
+
+            db.update(table, mergedValues, "_id=" + oldRow.getInt(oldRow.getColumnIndex("_id")),
+                    null);
+        }
+
+        static public Cursor selectConflictingRow(SQLiteDatabase db, String table,
+                                                  ContentValues row) {
+            // Conflict is possible only when numeric, mnnc, mnc (fields without any default value)
+            // are set in the new row
+            if (row.containsKey(Telephony.Carriers.NUMERIC) ||
+                    row.containsKey(Telephony.Carriers.MCC) ||
+                    row.containsKey(Telephony.Carriers.MNC)) {
+                loge("dbh.selectConflictingRow: called for non-conflicting row: " + row);
+                return null;
+            }
+
+            String[] columns = { "_id", Telephony.Carriers.TYPE, Telephony.Carriers.USER_EDITED };
+            String selection = "numeric=? AND mcc=? AND mnc=? AND apn=? AND proxy=? AND port=? "
+                    + "AND mmsproxy=? AND mmsport=? AND mmsc=? AND carrier_enabled=? AND bearer=? "
+                    + "AND mvno_type=? AND mvno_match_data=? AND profile_id=?";
+            int i = 0;
+            String[] selectionArgs = new String[14];
+            selectionArgs[i++] = row.getAsString(Telephony.Carriers.NUMERIC);
+            selectionArgs[i++] = row.getAsString(Telephony.Carriers.MCC);
+            selectionArgs[i++] = row.getAsString(Telephony.Carriers.MNC);
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.APN) ?
+                    row.getAsString(Telephony.Carriers.APN) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.PROXY) ?
+                    row.getAsString(Telephony.Carriers.PROXY) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.PORT) ?
+                    row.getAsString(Telephony.Carriers.PORT) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.MMSPROXY) ?
+                    row.getAsString(Telephony.Carriers.MMSPROXY) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.MMSPORT) ?
+                    row.getAsString(Telephony.Carriers.MMSPORT) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.MMSC) ?
+                    row.getAsString(Telephony.Carriers.MMSC) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.CARRIER_ENABLED) &&
+                    (row.getAsString(Telephony.Carriers.CARRIER_ENABLED).equals("0") ||
+                            row.getAsString(Telephony.Carriers.CARRIER_ENABLED).equals("false")) ?
+                    "0" : "1";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.BEARER) ?
+                    row.getAsString(Telephony.Carriers.BEARER) : "0";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.MVNO_TYPE) ?
+                    row.getAsString(Telephony.Carriers.MVNO_TYPE) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.MVNO_MATCH_DATA) ?
+                    row.getAsString(Telephony.Carriers.MVNO_MATCH_DATA) : "";
+            selectionArgs[i++] = row.containsKey(Telephony.Carriers.PROFILE_ID) ?
+                    row.getAsString(Telephony.Carriers.PROFILE_ID) : "0";
+
+            Cursor c = db.query(table, columns, selection, selectionArgs, null, null, null);
+
+            if (c != null) {
+                if (c.getCount() > 0) {
+                    if (VDBG) log("dbh.selectConflictingRow: " + c.getCount() + " conflicting " +
+                            "row(s) found");
+                    if (c.moveToFirst()) {
+                        return c;
+                    } else {
+                        loge("dbh.selectConflictingRow: moveToFirst() failed");
+                    }
+                } else {
+                    loge("dbh.selectConflictingRow: " + c.getCount() + " matching row found for " +
+                            "cv " + row);
+                }
+                c.close();
+            } else {
+                loge("dbh.selectConflictingRow: Error - c is null; no matching row found for " +
+                        "cv " + row);
+            }
+
+            return null;
         }
     }
 
     @Override
     public boolean onCreate() {
-        if (VDBG) log("onCreate:+");
         mOpenHelper = new DatabaseHelper(getContext());
+
+        // Call getReadableDatabase() to make sure onUpgrade is called
+        if (VDBG) log("onCreate: calling getReadableDatabase to trigger onUpgrade");
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+        // Update APN db on build update
+        String newBuildId = SystemProperties.get("ro.build.id", null);
+        if (!TextUtils.isEmpty(newBuildId)) {
+            // Check if build id has changed
+            SharedPreferences sp = getContext().getSharedPreferences(BUILD_ID_FILE,
+                    Context.MODE_PRIVATE);
+            String oldBuildId = sp.getString(RO_BUILD_ID, "");
+            if (!newBuildId.equals(oldBuildId)) {
+                if (DBG) log("onCreate: build id changed from " + oldBuildId + " to " +
+                        newBuildId);
+                updateApnDb();
+            } else {
+                if (VDBG) log("onCreate: build id did not change: " + oldBuildId);
+            }
+            sp.edit().putString(RO_BUILD_ID, newBuildId).apply();
+        } else {
+            if (VDBG) log("onCreate: newBuildId is empty");
+        }
+
         if (VDBG) log("onCreate:- ret true");
         return true;
     }
 
     private void setPreferredApnId(Long id, int subId) {
+        //todo: remove old PREF_FILE+subId SharedPreferences
         SharedPreferences sp = getContext().getSharedPreferences(
-                PREF_FILE + subId, Context.MODE_PRIVATE);
+                PREF_FILE, Context.MODE_PRIVATE);
         SharedPreferences.Editor editor = sp.edit();
-        editor.putLong(COLUMN_APN_ID, id != null ? id.longValue() : -1);
+        editor.putLong(COLUMN_APN_ID + subId, id != null ? id.longValue() : -1);
         editor.apply();
     }
 
     private long getPreferredApnId(int subId) {
         SharedPreferences sp = getContext().getSharedPreferences(
-                PREF_FILE + subId, Context.MODE_PRIVATE);
-        return sp.getLong(COLUMN_APN_ID, -1);
+                PREF_FILE, Context.MODE_PRIVATE);
+        return sp.getLong(COLUMN_APN_ID + subId, -1);
+    }
+
+    private void deletePreferredApnId() {
+        SharedPreferences sp = getContext().getSharedPreferences(
+                PREF_FILE, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sp.edit();
+        editor.clear();
+        editor.apply();
     }
 
     @Override
     public Cursor query(Uri url, String[] projectionIn, String selection,
             String[] selectionArgs, String sort) {
+        if (VDBG) log("query: url=" + url + ", projectionIn=" + projectionIn + ", selection="
+            + selection + "selectionArgs=" + selectionArgs + ", sort=" + sort);
         TelephonyManager mTelephonyManager =
                 (TelephonyManager)getContext().getSystemService(Context.TELEPHONY_SERVICE);
         int subId = SubscriptionManager.getDefaultSubId();
         String subIdString;
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         qb.setStrict(true); // a little protection from injection attacks
-        qb.setTables("carriers");
+        qb.setTables(CARRIERS_TABLE);
 
         int match = s_urlMatcher.match(url);
         switch (match) {
@@ -692,7 +1085,7 @@
                 // FIXME alter the selection to pass subId
                 // selection = selection + "and subId = "
             }
-            //intentional fall through from above case
+            // intentional fall through from above case
             // do nothing
             case URL_TELEPHONY: {
                 break;
@@ -774,6 +1167,17 @@
         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
         Cursor ret = null;
         try {
+            // Exclude entries marked deleted
+            if (CARRIERS_TABLE.equals(qb.getTables())) {
+                if (TextUtils.isEmpty(selection)) {
+                    selection = "";
+                } else {
+                    selection += " and ";
+                }
+                selection += "user_edited!=" + USER_EDITED_DELETED + " and user_edited!="
+                        + USER_EDITED_DELETED_BUT_PRESENT;
+                if (VDBG) log("query: selection modified to " + selection);
+            }
             ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
         } catch (SQLException e) {
             loge("got exception when querying: " + e);
@@ -841,15 +1245,32 @@
                 }
 
                 values = DatabaseHelper.setDefaultValue(values);
+                values.put(Telephony.Carriers.USER_EDITED, USER_EDITED_EDITED);
 
-                long rowID = db.insert(CARRIERS_TABLE, null, values);
-                if (rowID > 0)
-                {
-                    result = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, rowID);
-                    notify = true;
+                try {
+                    // Replace on conflict so that if same APN is present in db with user_edited
+                    // as USER_EDITED_UNTOUCHED or USER_EDITED_DELETED, it is replaced with
+                    // user_edited USER_EDITED_EDITED
+                    long rowID = db.insertWithOnConflict(CARRIERS_TABLE, null, values,
+                            SQLiteDatabase.CONFLICT_REPLACE);
+                    if (rowID >= 0) {
+                        result = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, rowID);
+                        notify = true;
+                    }
+                    if (VDBG) log("insert: inserted " + values.toString() + " rowID = " + rowID);
+                } catch (SQLException e) {
+                    log("insert: exception " + e);
+                    // Insertion failed which could be due to a conflict. Check if that is the case
+                    // and merge the entries
+                    Cursor oldRow = DatabaseHelper.selectConflictingRow(db, CARRIERS_TABLE, values);
+                    if (oldRow != null) {
+                        ContentValues mergedValues = new ContentValues();
+                        DatabaseHelper.mergeFieldsAndUpdateDb(db, CARRIERS_TABLE, oldRow, values,
+                                mergedValues, false);
+                        oldRow.close();
+                    }
                 }
 
-                if (VDBG) log("inserted " + values.toString() + " rowID = " + rowID);
                 break;
             }
 
@@ -869,11 +1290,11 @@
 
             case URL_CURRENT:
             {
-                // null out the previous operator
-                db.update("carriers", s_currentNullMap, "current IS NOT NULL", null);
+                // zero out the previous operator
+                db.update(CARRIERS_TABLE, s_currentNullMap, "current!=0", null);
 
-                String numeric = initialValues.getAsString("numeric");
-                int updated = db.update("carriers", s_currentSetMap,
+                String numeric = initialValues.getAsString(Telephony.Carriers.NUMERIC);
+                int updated = db.update(CARRIERS_TABLE, s_currentSetMap,
                         "numeric = '" + numeric + "'", null);
 
                 if (updated > 0)
@@ -932,6 +1353,8 @@
     {
         int count = 0;
         int subId = SubscriptionManager.getDefaultSubId();
+        ContentValues cv = new ContentValues();
+        cv.put(Telephony.Carriers.USER_EDITED, USER_EDITED_DELETED);
 
         checkPermission();
 
@@ -955,7 +1378,8 @@
 
             case URL_TELEPHONY:
             {
-                count = db.delete(CARRIERS_TABLE, where, whereArgs);
+                // Mark as user deleted instead of deleting
+                count = db.update(CARRIERS_TABLE, cv, where, whereArgs);
                 break;
             }
 
@@ -974,13 +1398,15 @@
 
             case URL_CURRENT:
             {
-                count = db.delete(CARRIERS_TABLE, where, whereArgs);
+                // Mark as user deleted instead of deleting
+                count = db.update(CARRIERS_TABLE, cv, where, whereArgs);
                 break;
             }
 
             case URL_ID:
             {
-                count = db.delete(CARRIERS_TABLE, Telephony.Carriers._ID + "=?",
+                // Mark as user deleted instead of deleting
+                count = db.update(CARRIERS_TABLE, cv, Telephony.Carriers._ID + "=?",
                         new String[] { url.getLastPathSegment() });
                 break;
             }
@@ -1028,6 +1454,12 @@
                 break;
             }
 
+            case URL_UPDATE_DB: {
+                updateApnDb();
+                count = 1;
+                break;
+            }
+
             default: {
                 throw new UnsupportedOperationException("Cannot delete that URL: " + url);
             }
@@ -1070,7 +1502,13 @@
 
             case URL_TELEPHONY:
             {
-                count = db.update(CARRIERS_TABLE, values, where, whereArgs);
+                values.put(Telephony.Carriers.USER_EDITED, USER_EDITED_EDITED);
+
+                // Replace on conflict so that if same APN is present in db with user_edited
+                // as USER_EDITED_UNTOUCHED or USER_EDITED_DELETED, it is replaced with
+                // user_edited USER_EDITED_EDITED
+                count = db.updateWithOnConflict(CARRIERS_TABLE, values, where, whereArgs,
+                        SQLiteDatabase.CONFLICT_REPLACE);
                 break;
             }
 
@@ -1090,7 +1528,12 @@
 
             case URL_CURRENT:
             {
-                count = db.update(CARRIERS_TABLE, values, where, whereArgs);
+                values.put(Telephony.Carriers.USER_EDITED, USER_EDITED_EDITED);
+                // Replace on conflict so that if same APN is present in db with user_edited
+                // as USER_EDITED_UNTOUCHED or USER_EDITED_DELETED, it is replaced with
+                // user_edited USER_EDITED_EDITED
+                count = db.updateWithOnConflict(CARRIERS_TABLE, values, where, whereArgs,
+                        SQLiteDatabase.CONFLICT_REPLACE);
                 break;
             }
 
@@ -1100,8 +1543,13 @@
                     throw new UnsupportedOperationException(
                             "Cannot update URL " + url + " with a where clause");
                 }
-                count = db.update(CARRIERS_TABLE, values, Telephony.Carriers._ID + "=?",
-                        new String[] { url.getLastPathSegment() });
+                values.put(Telephony.Carriers.USER_EDITED, USER_EDITED_EDITED);
+                // Replace on conflict so that if same APN is present in db with user_edited
+                // as USER_EDITED_UNTOUCHED or USER_EDITED_DELETED, it is replaced with
+                // user_edited USER_EDITED_EDITED
+                count = db.updateWithOnConflict(CARRIERS_TABLE, values,
+                        Telephony.Carriers._ID + "=?", new String[] { url.getLastPathSegment() },
+                        SQLiteDatabase.CONFLICT_REPLACE);
                 break;
             }
 
@@ -1194,6 +1642,35 @@
         mOpenHelper.initDatabase(db);
     }
 
+    private void updateApnDb() {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        try {
+            if (VDBG) log("updateApnDb: deleting user_edited=USER_EDITED_UNTOUCHED entries");
+            db.delete(CARRIERS_TABLE, "user_edited=" + USER_EDITED_UNTOUCHED, null);
+        } catch (SQLException e) {
+            loge("got exception when deleting to update: " + e);
+        }
+
+        // Delete preferred APN for all subIds
+        long currentPreferredApnId = getPreferredApnId(SubscriptionManager.getDefaultSubId());
+        Cursor c = db.query(CARRIERS_TABLE, null, "_id=" + currentPreferredApnId, null, null, null,
+                null);
+        if (VDBG) {
+            log("updateApnDb: currentPreferredApnId = " + currentPreferredApnId);
+            if (c != null && c.getCount() != 0) {
+                c.moveToNext();
+                log("updateApnDb: NAME=" + c.getString(c.getColumnIndex(Telephony.Carriers.NAME)));
+            }
+        }
+        deletePreferredApnId();
+
+        mOpenHelper.initDatabase(db);
+
+        // TODO: restore preference APN ID
+        c.close();
+    }
+
     /**
      * Log with debug
      *