Merge "address API council feedbacks for Telephony.Carriers"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2b4c6f8..59fa7bd 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -119,6 +119,13 @@
                   android:singleUser="true"
                   android:multiprocess="false" />
 
+        <provider android:name="RcsProvider"
+                  android:authorities="rcs"
+                  android:multiprocess="false"
+                  android:exported="true"
+                  android:singleUser="true"
+                  android:readPermission="android.permission.READ_SMS" />
+
         <service
             android:name=".TelephonyBackupAgent$DeferredSmsMmsRestoreService"
             android:exported="false" />
diff --git a/OWNERS b/OWNERS
index be3c684..92458db 100644
--- a/OWNERS
+++ b/OWNERS
@@ -9,3 +9,4 @@
 hallliu@google.com
 tgunn@google.com
 breadley@google.com
+nazaninb@google.com
diff --git a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
index fa823ff..592ecbe 100644
--- a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
+++ b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
@@ -262,6 +262,11 @@
     // cache for INITIAL_CREATE_DONE shared pref so access to it can be avoided when possible
     private static AtomicBoolean sInitialCreateDone = new AtomicBoolean(false);
 
+    // TODO(sahinc): Turn this to true once the schema finalizes, so that people can update their
+    // messaging databases. NOTE: move the switch/case update to the latest version of the database
+    // before turning this flag to true.
+    private static final boolean IS_RCS_TABLE_SCHEMA_CODE_COMPLETE = false;
+
     /**
      * The primary purpose of this DatabaseErrorHandler is to broadcast an intent on corruption and
      * print a Slog.wtf so database corruption can be caught earlier.
@@ -546,6 +551,11 @@
         createMmsTables(db);
         createSmsTables(db);
         createCommonTables(db);
+
+        if (IS_RCS_TABLE_SCHEMA_CODE_COMPLETE) {
+            RcsProviderHelper.createRcsTables(db);
+        }
+
         createCommonTriggers(db);
         createMmsTriggers(db);
         createWordsTables(db);
@@ -1649,6 +1659,12 @@
             } finally {
                 db.endTransaction();
             }
+            // fall through
+        case 67:
+            if (currentVersion <= 67 || !IS_RCS_TABLE_SCHEMA_CODE_COMPLETE) {
+                return;
+            }
+            RcsProviderHelper.createRcsTables(db);
             return;
         }
 
diff --git a/src/com/android/providers/telephony/RcsProvider.java b/src/com/android/providers/telephony/RcsProvider.java
new file mode 100644
index 0000000..83ff130
--- /dev/null
+++ b/src/com/android/providers/telephony/RcsProvider.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.telephony;
+
+import static com.android.providers.telephony.RcsProviderHelper.ID;
+import static com.android.providers.telephony.RcsProviderHelper.THREAD_TABLE;
+
+import android.app.AppOpsManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Content provider to handle RCS messages. The functionality here is similar to SmsProvider,
+ * MmsProvider etc. This is not meant to be public.
+ * @hide
+ */
+public class RcsProvider extends ContentProvider {
+    private final static String TAG = "RcsProvider";
+    static final String AUTHORITY = "rcs";
+    private static final UriMatcher URL_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final int THREAD = 1;
+
+    SQLiteOpenHelper mDbOpenHelper;
+
+    static {
+        URL_MATCHER.addURI(AUTHORITY, "thread", THREAD);
+    }
+
+    @Override
+    public boolean onCreate() {
+        setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
+        // Use the credential encrypted mmssms.db for RCS messages.
+        mDbOpenHelper = MmsSmsDatabaseHelper.getInstanceForCe(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        int match = URL_MATCHER.match(uri);
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        SQLiteDatabase readableDatabase = mDbOpenHelper.getReadableDatabase();
+
+        switch (match) {
+            case THREAD:
+                RcsProviderHelper.buildThreadQuery(qb);
+                break;
+            default:
+                Log.e(TAG, "Invalid query: " + uri);
+        }
+
+        return qb.query(
+                readableDatabase, projection, selection, selectionArgs, null, null, sortOrder);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        int match = URL_MATCHER.match(uri);
+        SQLiteDatabase writableDatabase = mDbOpenHelper.getWritableDatabase();
+
+        switch (match) {
+            case THREAD:
+                writableDatabase.insert(THREAD_TABLE, ID, values);
+                break;
+            default:
+                Log.e(TAG, "Invalid insert: " + uri);
+        }
+
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        int match = URL_MATCHER.match(uri);
+        int deletedCount = 0;
+        SQLiteDatabase writableDatabase = mDbOpenHelper.getWritableDatabase();
+
+        switch (match) {
+            case THREAD:
+                deletedCount = writableDatabase.delete(THREAD_TABLE, selection, selectionArgs);
+                break;
+            default:
+                Log.e(TAG, "Invalid delete: " + uri);
+        }
+
+        return deletedCount;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        int match = URL_MATCHER.match(uri);
+        int updatedCount = 0;
+        SQLiteDatabase writableDatabase = mDbOpenHelper.getWritableDatabase();
+
+        switch (match) {
+            case THREAD:
+                updatedCount = writableDatabase.update(
+                        THREAD_TABLE, values, selection, selectionArgs);
+                break;
+            default:
+                Log.e(TAG, "Invalid update: " + uri);
+        }
+
+        return updatedCount;
+    }
+}
diff --git a/src/com/android/providers/telephony/RcsProviderHelper.java b/src/com/android/providers/telephony/RcsProviderHelper.java
new file mode 100644
index 0000000..3966f53
--- /dev/null
+++ b/src/com/android/providers/telephony/RcsProviderHelper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.telephony;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Constants and helpers for RcsProvider to keep the code clean.
+ * @hide
+ */
+class RcsProviderHelper {
+    static final String ID = "_id";
+    static final String THREAD_TABLE = "rcs_thread";
+    static final String OWNER_PARTICIPANT = "owner_participant";
+
+    @VisibleForTesting
+    public static void createRcsTables(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + THREAD_TABLE + " (" +
+                ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                OWNER_PARTICIPANT + " INTEGER " +
+                ");");
+    }
+
+    static void buildThreadQuery(SQLiteQueryBuilder qb) {
+        qb.setTables(THREAD_TABLE);
+    }
+}
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
index a80fa12..1261218 100644
--- a/src/com/android/providers/telephony/TelephonyProvider.java
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -143,7 +143,7 @@
     private static final boolean DBG = true;
     private static final boolean VDBG = false; // STOPSHIP if true
 
-    private static final int DATABASE_VERSION = 30 << 16;
+    private static final int DATABASE_VERSION = 32 << 16;
     private static final int URL_UNKNOWN = 0;
     private static final int URL_TELEPHONY = 1;
     private static final int URL_CURRENT = 2;
@@ -380,8 +380,9 @@
                 + SubscriptionManager.WFC_IMS_ROAMING_MODE + " INTEGER DEFAULT -1,"
                 + SubscriptionManager.WFC_IMS_ROAMING_ENABLED + " INTEGER DEFAULT -1,"
                 + SubscriptionManager.IS_OPPORTUNISTIC + " INTEGER DEFAULT 0,"
-                + SubscriptionManager.PARENT_SUB_ID + " INTEGER DEFAULT -1,"
-                + SubscriptionManager.GROUP_UUID + " TEXT"
+                + SubscriptionManager.GROUP_UUID + " TEXT,"
+                + SubscriptionManager.IS_METERED + " INTEGER DEFAULT 1,"
+                + SubscriptionManager.ISO_COUNTRY_CODE + " TEXT"
                 + ");";
     }
 
@@ -1120,8 +1121,6 @@
                     // Try to update the siminfo table. It might not be there.
                     db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN "
                             + SubscriptionManager.IS_OPPORTUNISTIC + " INTEGER DEFAULT 0;");
-                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN "
-                            + SubscriptionManager.PARENT_SUB_ID + " INTEGER DEFAULT -1;");
                 } catch (SQLiteException e) {
                     if (DBG) {
                         log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
@@ -1151,15 +1150,44 @@
                 try {
                     // Try to update the siminfo table. It might not be there.
                     db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN "
-                            + SubscriptionManager.GROUP_UUID + " TEXT;");
+                        + SubscriptionManager.GROUP_UUID + " TEXT;");
+                } catch (SQLiteException e) {
+                    if (DBG) {
+                        log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
+                            "The table will get created in onOpen.");
+                    }
+                }
+                oldVersion = 30 << 16 | 6;
+            }
+            
+            if (oldVersion < (31 << 16 | 6)) {
+                try {
+                    // Try to update the siminfo table. It might not be there.
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN "
+                            + SubscriptionManager.IS_METERED + " INTEGER DEFAULT 1;");
                 } catch (SQLiteException e) {
                     if (DBG) {
                         log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
                                 "The table will get created in onOpen.");
                     }
                 }
-                oldVersion = 30 << 16 | 6;
+                oldVersion = 31 << 16 | 6;
             }
+
+            if (oldVersion < (32 << 16 | 6)) {
+                try {
+                    // Try to update the siminfo table. It might not be there.
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN "
+                            + SubscriptionManager.ISO_COUNTRY_CODE + " TEXT;");
+                } catch (SQLiteException e) {
+                    if (DBG) {
+                        log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
+                                "The table will get created in onOpen.");
+                    }
+                }
+                oldVersion = 32 << 16 | 6;
+            }
+
             if (DBG) {
                 log("dbh.onUpgrade:- db=" + db + " oldV=" + oldVersion + " newV=" + newVersion);
             }
@@ -2788,7 +2816,6 @@
      */
     private Cursor getSubscriptionMatchingAPNList(SQLiteQueryBuilder qb, String[] projectionIn,
             String selection, String[] selectionArgs, String sort, int subId) {
-
         Cursor ret;
         final TelephonyManager tm = ((TelephonyManager)
                 getContext().getSystemService(Context.TELEPHONY_SERVICE))
@@ -2802,6 +2829,10 @@
         qb.appendWhereStandalone(NUMERIC + " = '" + mccmnc + "' ");
 
         ret = qb.query(db, null, selection, selectionArgs, null, null, sort);
+        if (ret == null) {
+            loge("query current APN but cursor is null.");
+            return null;
+        }
 
         if (DBG) log("match current APN size:  " + ret.getCount());
 
@@ -2834,6 +2865,7 @@
                 parentCursor.addRow(data);
             }
         }
+        ret.close();
 
         if (currentCursor.getCount() > 0) {
             if (DBG) log("match Carrier Id APN: " + currentCursor.getCount());
diff --git a/tests/src/com/android/providers/telephony/RcsProviderTest.java b/tests/src/com/android/providers/telephony/RcsProviderTest.java
new file mode 100644
index 0000000..708078f
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/RcsProviderTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.telephony;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.app.AppOpsManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class RcsProviderTest {
+    private MockContentResolver mContentResolver;
+
+    @Before
+    public void setUp() {
+        RcsProvider rcsProvider = new RcsProviderTestable();
+        MockContextWithProvider context = new MockContextWithProvider(rcsProvider);
+        mContentResolver = context.getContentResolver();
+    }
+
+    // TODO(sahinc): This test isn't that useful for now as it only checks the return value. Revisit
+    // once we have more of the implementation in place.
+    @Test
+    public void testInsertThread() {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(RcsProviderHelper.OWNER_PARTICIPANT, 5);
+
+        Uri uri = mContentResolver.insert(Uri.parse("content://rcs/thread"), contentValues);
+        assertNull(uri);
+    }
+
+    @Test
+    public void testUpdateThread() {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(RcsProviderHelper.OWNER_PARTICIPANT, 5);
+        mContentResolver.insert(Uri.parse("content://rcs/thread"), contentValues);
+
+        contentValues.put(RcsProviderHelper.OWNER_PARTICIPANT, 12);
+        int updateCount = mContentResolver.update(Uri.parse("content://rcs/thread"),
+                contentValues, "owner_participant=5", null);
+
+        assertEquals(1, updateCount);
+    }
+
+    class MockContextWithProvider extends MockContext {
+        private final MockContentResolver mResolver;
+
+        MockContextWithProvider(RcsProvider rcsProvider) {
+            mResolver = new MockContentResolver();
+
+            // Add authority="rcs" to given smsProvider
+            ProviderInfo providerInfo = new ProviderInfo();
+            providerInfo.authority = RcsProvider.AUTHORITY;
+            rcsProvider.attachInfoForTesting(this, providerInfo);
+            mResolver.addProvider(RcsProvider.AUTHORITY, rcsProvider);
+        }
+
+        @Override
+        public MockContentResolver getContentResolver() {
+            return mResolver;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            switch (name) {
+                case Context.APP_OPS_SERVICE:
+                    return Mockito.mock(AppOpsManager.class);
+                case Context.TELEPHONY_SERVICE:
+                    return Mockito.mock(TelephonyManager.class);
+                default:
+                    return null;
+            }
+        }
+    }
+
+}
diff --git a/tests/src/com/android/providers/telephony/RcsProviderTestable.java b/tests/src/com/android/providers/telephony/RcsProviderTestable.java
new file mode 100644
index 0000000..1e4da11
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/RcsProviderTestable.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.telephony;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * A subclass of RcsProvider used for testing on an in-memory database
+ */
+public class RcsProviderTestable extends RcsProvider {
+
+    @Override
+    public boolean onCreate() {
+        mDbOpenHelper = new InMemoryRcsDatabase();
+        return true;
+    }
+
+    static class InMemoryRcsDatabase extends SQLiteOpenHelper {
+        InMemoryRcsDatabase() {
+            super(null,      // no context is needed for in-memory db
+                    null,      // db file name is null for in-memory db
+                    null,      // CursorFactory is null by default
+                    1);        // db version is no-op for tests
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            RcsProviderHelper.createRcsTables(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // no-op
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/telephony/TelephonyDatabaseHelperTest.java b/tests/src/com/android/providers/telephony/TelephonyDatabaseHelperTest.java
index d72d2a4..6892673 100644
--- a/tests/src/com/android/providers/telephony/TelephonyDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/telephony/TelephonyDatabaseHelperTest.java
@@ -87,6 +87,19 @@
     }
 
     @Test
+    public void databaseHelperOnUpgrade_hasCountryIsoField() {
+        Log.d(TAG, "databaseHelperOnUpgrade_hasCountryIsoField");
+        SQLiteDatabase db = mInMemoryDbHelper.getWritableDatabase();
+        mHelper.onUpgrade(db, (4 << 16), TelephonyProvider.DatabaseHelper.getVersion(mContext));
+
+        // the upgraded db must have the Telephony.Carriers.CARRIER_ID field
+        Cursor cursor = db.query("simInfo", null, null, null, null, null, null);
+        String[] upgradedColumns = cursor.getColumnNames();
+        Log.d(TAG, "iso columns: " + Arrays.toString(upgradedColumns));
+        assertTrue(Arrays.asList(upgradedColumns).contains(SubscriptionManager.ISO_COUNTRY_CODE));
+    }
+
+    @Test
     public void databaseHelperOnUpgrade_columnsMatchNewlyCreatedDb() {
         Log.d(TAG, "databaseHelperOnUpgrade_columnsMatchNewlyCreatedDb");
         // (5 << 16 | 6) is the first upgrade trigger in onUpgrade