Adding a new content value to prevent intent broadcasting during MMS
insert, and using it for MMS restore

Test: atest MmsProviderTest TelephonyBackupAgentTest
Change-Id: Ie4d4e79bb2d05f572b8abe437ec575bd25bcb250
diff --git a/.gitignore b/.gitignore
index bff2d76..7eb3721 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.iml
+*.idea
diff --git a/src/com/android/providers/telephony/MmsProvider.java b/src/com/android/providers/telephony/MmsProvider.java
index 35d6b04..04182ac 100644
--- a/src/com/android/providers/telephony/MmsProvider.java
+++ b/src/com/android/providers/telephony/MmsProvider.java
@@ -38,7 +38,6 @@
 import android.provider.Telephony.CanonicalAddressesColumns;
 import android.provider.Telephony.Mms;
 import android.provider.Telephony.Mms.Addr;
-import android.provider.Telephony.Mms.Inbox;
 import android.provider.Telephony.Mms.Part;
 import android.provider.Telephony.Mms.Rate;
 import android.provider.Telephony.MmsSms;
@@ -73,6 +72,13 @@
     // The name of parts directory. The full dir is "app_parts".
     static final String PARTS_DIR_NAME = "parts";
 
+    private ProviderUtilWrapper providerUtilWrapper = new ProviderUtilWrapper();
+
+    @VisibleForTesting
+    public void setProviderUtilWrapper(ProviderUtilWrapper providerUtilWrapper) {
+        this.providerUtilWrapper = providerUtilWrapper;
+    }
+
     @Override
     public boolean onCreate() {
         setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
@@ -81,6 +87,14 @@
         return true;
     }
 
+    // wrapper class to allow easier mocking of the static ProviderUtil in tests
+    @VisibleForTesting
+    public static class ProviderUtilWrapper {
+        public boolean isAccessRestricted(Context context, String packageName, int uid) {
+            return ProviderUtil.isAccessRestricted(context, packageName, uid);
+        }
+    }
+
     /**
      * Return the proper view of "pdu" table for the current access status.
      *
@@ -317,6 +331,15 @@
         int msgBox = Mms.MESSAGE_BOX_ALL;
         boolean notify = true;
 
+        boolean forceNoNotify = values.containsKey(TelephonyBackupAgent.NOTIFY)
+                && !values.getAsBoolean(TelephonyBackupAgent.NOTIFY);
+        values.remove(TelephonyBackupAgent.NOTIFY);
+        // check isAccessRestricted to prevent third parties from setting NOTIFY = false maliciously
+        if (forceNoNotify && !providerUtilWrapper.isAccessRestricted(
+                getContext(), getCallingPackage(), Binder.getCallingUid())) {
+            notify = false;
+        }
+
         int match = sURLMatcher.match(uri);
         if (LOCAL_LOGV) {
             Log.v(TAG, "Insert uri=" + uri + ", match=" + match);
diff --git a/src/com/android/providers/telephony/TelephonyBackupAgent.java b/src/com/android/providers/telephony/TelephonyBackupAgent.java
index 34fed99..5f75117 100644
--- a/src/com/android/providers/telephony/TelephonyBackupAgent.java
+++ b/src/com/android/providers/telephony/TelephonyBackupAgent.java
@@ -31,11 +31,11 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.database.Cursor;
-import android.database.DatabaseUtils;
 import android.net.Uri;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.PowerManager;
+import android.os.UserHandle;
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
 import android.provider.Telephony;
@@ -288,6 +288,8 @@
     private static final String QUOTA_RESET_TIME = "reset_quota_time";
     private static final long QUOTA_RESET_INTERVAL = 30 * AlarmManager.INTERVAL_DAY; // 30 days.
 
+    // Key for explicitly settings whether mms restore should notify or not
+    static final String NOTIFY = "notify";
 
     static {
         // Consider restored messages read and seen by default. The actual data can override
@@ -732,6 +734,7 @@
         jsonReader.beginArray();
         int total = 0;
         int numExceptions = 0;
+        final int notifyAfterCount = mMaxMsgPerFile;
         while (jsonReader.hasNext()) {
             final Mms mms = readMmsFromReader(jsonReader);
             if (DEBUG) {
@@ -747,17 +750,32 @@
                     continue;
                 }
                 total++;
+                mms.values.put(NOTIFY, false);
                 addMmsMessage(mms);
+                // notifying every 1000 messages to follow sms restore pattern
+                if (total % notifyAfterCount == 0) {
+                    notifyBulkMmsChange();
+                }
             } catch (Exception e) {
                 Log.e(TAG, "putMmsMessagesToProvider", e);
                 numExceptions++;
                 DeferredSmsMmsRestoreService.localLog("putMmsMessagesToProvider: Exception " + e);
             }
         }
+        // notifying for any remaining messages
+        if (total % notifyAfterCount > 0) {
+            notifyBulkMmsChange();
+        }
         Log.d(TAG, "putMmsMessagesToProvider handled " + total + " new messages.");
         incremenentSharedPref(false, total, numExceptions);
     }
 
+    private void notifyBulkMmsChange() {
+        mContentResolver.notifyChange(Telephony.MmsSms.CONTENT_URI, null,
+                ContentResolver.NOTIFY_SYNC_TO_NETWORK, UserHandle.USER_ALL);
+        ProviderUtil.notifyIfNotDefaultSmsApp(Telephony.Mms.CONTENT_URI, null, this);
+    }
+
     @VisibleForTesting
     static final String[] PROJECTION_ID = {BaseColumns._ID};
     private static final int ID_IDX = 0;
diff --git a/tests/src/com/android/providers/telephony/MmsProviderTest.java b/tests/src/com/android/providers/telephony/MmsProviderTest.java
index 5f5de75..2e618b0 100644
--- a/tests/src/com/android/providers/telephony/MmsProviderTest.java
+++ b/tests/src/com/android/providers/telephony/MmsProviderTest.java
@@ -16,8 +16,10 @@
 
 package com.android.providers.telephony;
 
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -26,6 +28,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.provider.Telephony;
@@ -36,39 +39,40 @@
 import junit.framework.TestCase;
 
 import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 
 public class MmsProviderTest extends TestCase {
     private static final String TAG = "MmsProviderTest";
 
-    @Mock private Context mContext;
     private MockContentResolver mContentResolver;
     private MmsProviderTestable mMmsProviderTestable;
-    @Mock private PackageManager mPackageManager;
 
     private int notifyChangeCount;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        MockitoAnnotations.initMocks(this);
+
         mMmsProviderTestable = new MmsProviderTestable();
 
         // setup mocks
-        when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE)))
+        Context context = mock(Context.class);
+        PackageManager packageManager = mock(PackageManager.class);
+        Resources resources = mock(Resources.class);
+        when(context.getSystemService(eq(Context.APP_OPS_SERVICE)))
                 .thenReturn(mock(AppOpsManager.class));
-        when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE)))
+        when(context.getSystemService(eq(Context.TELEPHONY_SERVICE)))
                 .thenReturn(mock(TelephonyManager.class));
 
-        when(mContext.checkCallingOrSelfPermission(anyString()))
+        when(context.checkCallingOrSelfPermission(anyString()))
                 .thenReturn(PackageManager.PERMISSION_GRANTED);
-        when(mContext.getUserId()).thenReturn(0);
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(context.getUserId()).thenReturn(0);
+        when(context.getPackageManager()).thenReturn(packageManager);
+        when(context.getResources()).thenReturn(resources);
+        when(resources.getString(anyInt())).thenReturn("");
 
         /**
          * This is used to give the MmsProviderTest a mocked context which takes a
-         * SmsProvider and attaches it to the ContentResolver with telephony authority.
+         * MmsProvider and attaches it to the ContentResolver with telephony authority.
          * The mocked context also gives WRITE_APN_SETTINGS permissions
          */
         mContentResolver = new MockContentResolver() {
@@ -78,14 +82,14 @@
                 notifyChangeCount++;
             }
         };
-        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        when(context.getContentResolver()).thenReturn(mContentResolver);
 
         // Add authority="mms" to given mmsProvider
         ProviderInfo providerInfo = new ProviderInfo();
         providerInfo.authority = "mms";
 
         // Add context to given mmsProvider
-        mMmsProviderTestable.attachInfoForTesting(mContext, providerInfo);
+        mMmsProviderTestable.attachInfoForTesting(context, providerInfo);
         Log.d(TAG, "MockContextWithProvider: mmsProvider.getContext(): "
                 + mMmsProviderTestable.getContext());
 
@@ -104,6 +108,35 @@
 
     @Test
     public void testInsertMms() {
+        final ContentValues values = getTestContentValues();
+
+        Uri expected = Uri.parse("content://mms/1");
+        Uri actual = mContentResolver.insert(Telephony.Mms.CONTENT_URI, values);
+
+        assertEquals(expected, actual);
+        assertEquals(1, notifyChangeCount);
+    }
+
+    @Test
+    public void testInsertMmsWithoutNotify() {
+
+        MmsProvider.ProviderUtilWrapper providerUtilWrapper =
+                mock(MmsProvider.ProviderUtilWrapper.class);
+        when(providerUtilWrapper.isAccessRestricted(
+                any(Context.class), anyString(), anyInt())).thenReturn(false);
+        mMmsProviderTestable.setProviderUtilWrapper(providerUtilWrapper);
+
+        final ContentValues values = getTestContentValues();
+        values.put(TelephonyBackupAgent.NOTIFY, false);
+
+        Uri expected = Uri.parse("content://mms/1");
+        Uri actual = mContentResolver.insert(Telephony.Mms.CONTENT_URI, values);
+
+        assertEquals(expected, actual);
+        assertEquals(0, notifyChangeCount);
+    }
+
+    private ContentValues getTestContentValues() {
         final ContentValues values = new ContentValues();
         values.put(Telephony.Mms.READ, 1);
         values.put(Telephony.Mms.SEEN, 1);
@@ -111,11 +144,6 @@
         values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL);
         values.put(Telephony.Mms.TEXT_ONLY, 1);
         values.put(Telephony.Mms.THREAD_ID, 1);
-
-        Uri expected = Uri.parse("content://mms/1");
-        Uri actual = mContentResolver.insert(Telephony.Mms.CONTENT_URI, values);
-
-        assertEquals(expected, actual);
-        assertEquals(1, notifyChangeCount);
+        return values;
     }
 }
diff --git a/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
index f6a9c7f..bf0f49b 100644
--- a/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
+++ b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertArrayEquals;
 
 import android.annotation.TargetApi;
-import android.app.backup.BackupDataOutput;
 import android.app.backup.FullBackupDataOutput;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -43,6 +42,7 @@
 import android.util.JsonWriter;
 import android.util.SparseArray;
 
+import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.internal.telephony.PhoneFactory;
 
 import libcore.io.IoUtils;
@@ -54,13 +54,11 @@
 import org.json.JSONObject;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -652,13 +650,21 @@
 
     /**
      * Test restore mms with the empty json array "[]".
-     * @throws Exception
      */
-    public void testRestoreMms_NoMms() throws Exception {
+    public void testRestoreMms_NoMms() {
         JsonReader jsonReader = new JsonReader(new StringReader(EMPTY_JSON_ARRAY));
         FakeMmsProvider mmsProvider = new FakeMmsProvider(null);
         mMockContentResolver.addProvider("mms", mmsProvider);
-        mTelephonyBackupAgent.putMmsMessagesToProvider(jsonReader);
+        ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyBackupAgent, (agent) -> {
+                    try {
+                        agent.putMmsMessagesToProvider(jsonReader);
+                    } catch (IOException e) {
+                        fail("Encountered exception: " + e);
+                    }
+                    return null;
+                }
+        );
         assertEquals(0, mmsProvider.getRowsAdded());
     }
 
@@ -670,7 +676,16 @@
         JsonReader jsonReader = new JsonReader(new StringReader(addRandomDataToJson(mAllMmsJson)));
         FakeMmsProvider mmsProvider = new FakeMmsProvider(mMmsAllContentValues);
         mMockContentResolver.addProvider("mms", mmsProvider);
-        mTelephonyBackupAgent.putMmsMessagesToProvider(jsonReader);
+        ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyBackupAgent, (agent) -> {
+                    try {
+                        agent.putMmsMessagesToProvider(jsonReader);
+                    } catch (IOException e) {
+                        fail("Encountered exception: " + e);
+                    }
+                    return null;
+                }
+        );
         assertEquals(18, mmsProvider.getRowsAdded());
         assertEquals(mThreadProvider.mIsThreadArchived, mThreadProvider.mUpdateThreadsArchived);
     }
@@ -684,7 +699,16 @@
                 (new StringReader(addRandomDataToJson(mMmsAllAttachmentJson)));
         FakeMmsProvider mmsProvider = new FakeMmsProvider(mMmsAllContentValues);
         mMockContentResolver.addProvider("mms", mmsProvider);
-        mTelephonyBackupAgent.putMmsMessagesToProvider(jsonReader);
+        ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyBackupAgent, (agent) -> {
+                    try {
+                        agent.putMmsMessagesToProvider(jsonReader);
+                    } catch (IOException e) {
+                        fail("Encountered exception: " + e);
+                    }
+                    return null;
+                }
+        );
         assertEquals(7, mmsProvider.getRowsAdded());
     }
 
@@ -694,7 +718,16 @@
         FakeMmsProvider mmsProvider = new FakeMmsProvider(mMmsNullBodyContentValues);
         mMockContentResolver.addProvider("mms", mmsProvider);
 
-        mTelephonyBackupAgent.putMmsMessagesToProvider(jsonReader);
+        ShellIdentityUtils.invokeMethodWithShellPermissions(
+                mTelephonyBackupAgent, (agent) -> {
+                    try {
+                        agent.putMmsMessagesToProvider(jsonReader);
+                    } catch (IOException e) {
+                        fail("Encountered exception: " + e);
+                    }
+                    return null;
+                }
+        );
 
         assertEquals(3, mmsProvider.getRowsAdded());
     }
@@ -943,6 +976,8 @@
             for (String key : modifiedValues.keySet()) {
                 assertEquals("Key:"+key, modifiedValues.get(key), values.get(key));
             }
+            values.remove(TelephonyBackupAgent.NOTIFY); // notify gets removed before final values
+
             assertEquals(modifiedValues.size(), values.size());
             return retUri;
         }