[FBE] Introduce shadow calllog provider [2/2]

Introduce a new provider that's a clone of the calllog provider but
is EA.

The provider is hidden and requires MANAGE_USERS to access, and responsible
for storing calllog entries that are inserted when the real provider is still
encrypted.  When the real provider starts, it copies the entries from the shadow
as well as user-0's real provider, and clears the shadow.

Also fix b/26516259

Bug 26183949

Change-Id: If44b46709e2e7b1651b41c09d900e1cb2777dc56
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ea70f64..85fa07c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -54,6 +54,15 @@
             android:writePermission="android.permission.WRITE_CALL_LOG">
         </provider>
 
+        <provider android:name="ShadowCallLogProvider"
+                  android:authorities="call_log_shadow"
+                  android:syncable="false" android:multiprocess="false"
+                  android:exported="true"
+                  android:encryptionAware="true"
+                  android:readPermission="android.permission.MANAGE_USERS"
+                  android:writePermission="android.permission.MANAGE_USERS">
+        </provider>
+
         <provider android:name="VoicemailContentProvider"
             android:authorities="com.android.voicemail"
             android:syncable="false" android:multiprocess="false"
diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
index 726d99c..2c03a79 100644
--- a/src/com/android/providers/contacts/CallLogDatabaseHelper.java
+++ b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
@@ -42,8 +42,13 @@
 
     private static final String DATABASE_NAME = "calllog.db";
 
+    private static final String SHADOW_DATABASE_NAME = "calllog_shadow.db";
+
     private static CallLogDatabaseHelper sInstance;
 
+    /** Instance for the "shadow" provider. */
+    private static CallLogDatabaseHelper sInstanceForShadow;
+
     private final Context mContext;
 
     private final OpenHelper mOpenHelper;
@@ -55,6 +60,7 @@
 
     public interface DbProperties {
         String CALL_LOG_LAST_SYNCED = "call_log_last_synced";
+        String CALL_LOG_LAST_SYNCED_FOR_SHADOW = "call_log_last_synced_for_shadow";
         String DATA_MIGRATED = "migrated";
     }
 
@@ -186,6 +192,15 @@
         return sInstance;
     }
 
+    public static synchronized CallLogDatabaseHelper getInstanceForShadow(Context context) {
+        if (sInstanceForShadow == null) {
+            // Shadow provider is always encryption-aware.
+            sInstanceForShadow = new CallLogDatabaseHelper(
+                    context.createDeviceEncryptedStorageContext(), SHADOW_DATABASE_NAME);
+        }
+        return sInstanceForShadow;
+    }
+
     public SQLiteDatabase getReadableDatabase() {
         return mOpenHelper.getReadableDatabase();
     }
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index ce86cf7..d364dd3 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -24,6 +24,7 @@
 
 import android.app.AppOpsManager;
 import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -33,6 +34,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
@@ -52,6 +54,7 @@
 import com.android.providers.contacts.util.SelectionBuilder;
 import com.android.providers.contacts.util.UserUtils;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -62,6 +65,8 @@
 public class CallLogProvider extends ContentProvider {
     private static final String TAG = CallLogProvider.class.getSimpleName();
 
+    public static final boolean VERBOSE_LOGGING = false; // DO NOT SUBMIT WITH TRUE
+
     private static final int BACKGROUND_TASK_INITIALIZE = 0;
     private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
 
@@ -109,6 +114,9 @@
         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
+
+        // Shadow provider only supports "/calls".
+        sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS);
     }
 
     private static final HashMap<String, String> sCallsProjectionMap;
@@ -157,11 +165,19 @@
     private VoicemailPermissions mVoicemailPermissions;
     private CallLogInsertionHelper mCallLogInsertionHelper;
 
+    protected boolean isShadow() {
+        return false;
+    }
+
+    protected final String getProviderName() {
+        return this.getClass().getSimpleName();
+    }
+
     @Override
     public boolean onCreate() {
         setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
-            Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start");
+            Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start");
         }
         final Context context = getContext();
         mDbHelper = getDatabaseHelper(context);
@@ -171,7 +187,7 @@
         mVoicemailPermissions = new VoicemailPermissions(context);
         mCallLogInsertionHelper = createCallLogInsertionHelper(context);
 
-        mBackgroundThread = new HandlerThread("CallLogProviderWorker",
+        mBackgroundThread = new HandlerThread(getProviderName() + "Worker",
                 Process.THREAD_PRIORITY_BACKGROUND);
         mBackgroundThread.start();
         mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
@@ -186,7 +202,7 @@
         scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
 
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
-            Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
+            Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish");
         }
         return true;
     }
@@ -196,7 +212,6 @@
         return DefaultCallLogInsertionHelper.getInstance(context);
     }
 
-    @VisibleForTesting
     protected CallLogDatabaseHelper getDatabaseHelper(final Context context) {
         return CallLogDatabaseHelper.getInstance(context);
     }
@@ -204,6 +219,12 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
+                    "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
+                    "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
+                    " User=" + UserUtils.getCurrentUserHandle(getContext()));
+        }
         waitForAccess(mReadAccessLatch);
         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         qb.setTables(Tables.CALLS);
@@ -301,6 +322,10 @@
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "insert: uri=" + uri + "  values=[" + values + "]" +
+                    " CPID=" + Binder.getCallingPid());
+        }
         waitForAccess(mReadAccessLatch);
         checkForSupportedColumns(sCallsProjectionMap, values);
         // Inserting a voicemail record through call_log requires the voicemail
@@ -328,6 +353,12 @@
 
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "update: uri=" + uri +
+                    "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
+                    "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
+                    " User=" + UserUtils.getCurrentUserHandle(getContext()));
+        }
         waitForAccess(mReadAccessLatch);
         checkForSupportedColumns(sCallsProjectionMap, values);
         // Request that involves changing record type to voicemail requires the
@@ -359,6 +390,12 @@
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "delete: uri=" + uri +
+                    "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
+                    " CPID=" + Binder.getCallingPid() +
+                    " User=" + UserUtils.getCurrentUserHandle(getContext()));
+        }
         waitForAccess(mReadAccessLatch);
         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
@@ -367,6 +404,8 @@
         final int matchedUriId = sURIMatcher.match(uri);
         switch (matchedUriId) {
             case CALLS:
+                // TODO: Special case - We may want to forward the delete request on user 0 to the
+                // shadow provider too.
                 return getDatabaseModifier(db).delete(Tables.CALLS,
                         selectionBuilder.build(), selectionArgs);
             default:
@@ -455,37 +494,76 @@
     }
 
     /**
-     * Syncs any unique call log entries that have been inserted into the primary user's call log
-     * since the last time the last sync occurred.
+     * Sync all calllog entries that were inserted
      */
-    private void syncEntriesFromPrimaryUser(UserManager userManager) {
-        final int userHandle = userManager.getUserHandle();
+    private void syncEntries() {
+        if (isShadow()) {
+            return; // It's the shadow provider itself.  No copying.
+        }
+
+        final UserManager userManager = UserUtils.getUserManager(getContext());
+
         // TODO: http://b/24944959
-        if (userHandle == UserHandle.USER_SYSTEM
-            || userManager.getUserInfo(userHandle).isManagedProfile()) {
+        if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager,
+                userManager.getUserHandle())) {
             return;
         }
 
-        final long lastSyncTime = getLastSyncTime();
-        final Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI,
-                UserHandle.USER_SYSTEM);
-        final Cursor cursor = getContext().getContentResolver().query(
+        final int myUserId = userManager.getUserHandle();
+
+        // See the comment in Calls.addCall() for the logic.
+
+        if (userManager.isSystemUser()) {
+            // If it's the system user, just copy from shadow.
+            syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true,
+                    /* forAllUsersOnly =*/ false);
+        } else {
+            // Otherwise, copy from system's real provider, as well as self's shadow.
+            syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false,
+                    /* forAllUsersOnly =*/ true);
+            syncEntriesFrom(myUserId, /* sourceIsShadow = */ true,
+                    /* forAllUsersOnly =*/ false);
+        }
+    }
+
+    private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow,
+            boolean forAllUsersOnly) {
+
+        final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI;
+
+        final long lastSyncTime = getLastSyncTime(sourceIsShadow);
+
+        final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId);
+        final long newestTimeStamp;
+        final ContentResolver cr = getContext().getContentResolver();
+
+        final StringBuilder selection = new StringBuilder();
+
+        selection.append(
+                "(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")");
+
+        if (forAllUsersOnly) {
+            selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)");
+        }
+
+        final Cursor cursor = cr.query(
                 uri,
                 CALL_LOG_SYNC_PROJECTION,
-                EXCLUDE_VOICEMAIL_SELECTION + " AND " + MORE_RECENT_THAN_SELECTION,
+                selection.toString(),
                 new String[] {String.valueOf(lastSyncTime)},
-                Calls.DATE + " DESC");
+                Calls.DATE + " ASC");
         if (cursor == null) {
             return;
         }
         try {
-            final long lastSyncedEntryTime = copyEntriesFromCursor(cursor);
-            if (lastSyncedEntryTime > lastSyncTime) {
-                setLastTimeSynced(lastSyncedEntryTime);
-            }
+            newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow);
         } finally {
             cursor.close();
         }
+        if (sourceIsShadow) {
+            // delete all entries in shadow.
+            cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)});
+        }
     }
 
     /**
@@ -532,12 +610,10 @@
 
     /**
      * @param cursor to copy call log entries from
-     *
-     * @return the timestamp of the last synced entry.
      */
     @VisibleForTesting
-    long copyEntriesFromCursor(Cursor cursor) {
-        long lastSynced = 0;
+    long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) {
+        long latestTimestamp = 0;
         final ContentValues values = new ContentValues();
         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
         db.beginTransaction();
@@ -548,11 +624,6 @@
                 values.clear();
                 DatabaseUtils.cursorRowToContentValues(cursor, values);
 
-                final boolean addForAllUsers = values.getAsInteger(Calls.ADD_FOR_ALL_USERS) == 1;
-                if (!addForAllUsers) {
-                    continue;
-                }
-
                 final String startTime = values.getAsString(Calls.DATE);
                 final String number = values.getAsString(Calls.NUMBER);
 
@@ -562,7 +633,7 @@
 
                 if (cursor.isLast()) {
                     try {
-                        lastSynced = Long.valueOf(startTime);
+                        latestTimestamp = Long.valueOf(startTime);
                     } catch (NumberFormatException e) {
                         Log.e(TAG, "Call log entry does not contain valid start time: "
                                 + startTime);
@@ -580,23 +651,35 @@
 
                 db.insert(Tables.CALLS, null, values);
             }
+
+            if (latestTimestamp > lastSyncTime) {
+                setLastTimeSynced(latestTimestamp, forShadow);
+            }
+
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
         }
-        return lastSynced;
+        return latestTimestamp;
     }
 
-    private long getLastSyncTime() {
+    private static String getLastSyncTimePropertyName(boolean forShadow) {
+        return forShadow
+                ? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW
+                : DbProperties.CALL_LOG_LAST_SYNCED;
+    }
+
+    @VisibleForTesting
+    long getLastSyncTime(boolean forShadow) {
         try {
-            return Long.valueOf(mDbHelper.getProperty(DbProperties.CALL_LOG_LAST_SYNCED, "0"));
+            return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0"));
         } catch (NumberFormatException e) {
             return 0;
         }
     }
 
-    private void setLastTimeSynced(long time) {
-        mDbHelper.setProperty(DbProperties.CALL_LOG_LAST_SYNCED, String.valueOf(time));
+    private void setLastTimeSynced(long time, boolean forShadow) {
+        mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time));
     }
 
     private static void waitForAccess(CountDownLatch latch) {
@@ -621,14 +704,7 @@
     private void performBackgroundTask(int task, Object arg) {
         if (task == BACKGROUND_TASK_INITIALIZE) {
             try {
-                final Context context = getContext();
-                if (context != null) {
-                    final UserManager userManager = UserUtils.getUserManager(context);
-                    if (userManager != null &&
-                            !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)) {
-                        syncEntriesFromPrimaryUser(userManager);
-                    }
-                }
+                syncEntries();
             } finally {
                 mReadAccessLatch.countDown();
                 mReadAccessLatch = null;
@@ -636,6 +712,5 @@
         } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
             adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
         }
-
     }
 }
diff --git a/src/com/android/providers/contacts/ShadowCallLogProvider.java b/src/com/android/providers/contacts/ShadowCallLogProvider.java
new file mode 100644
index 0000000..2cacdc2
--- /dev/null
+++ b/src/com/android/providers/contacts/ShadowCallLogProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.contacts;
+
+import android.content.Context;
+
+public class ShadowCallLogProvider extends CallLogProvider {
+    protected CallLogDatabaseHelper getDatabaseHelper(final Context context) {
+        return CallLogDatabaseHelper.getInstanceForShadow(context);
+    }
+
+    @Override
+    protected boolean isShadow() {
+        return true;
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index fb771a3..590a0a3 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -18,6 +18,7 @@
 
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.PhoneConstants;
+import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
 import com.android.providers.contacts.testutil.CommonDatabaseUtils;
 
 import android.content.ComponentName;
@@ -187,6 +188,8 @@
         Uri uri = Calls.addCall(ci, getMockContext(), "1-800-263-7643",
                 PhoneConstants.PRESENTATION_ALLOWED, Calls.OUTGOING_TYPE, 0, subscription, 2000,
                 40, null);
+        assertNotNull(uri);
+        assertEquals("0@" + CallLog.AUTHORITY, uri.getAuthority());
 
         ContentValues values = new ContentValues();
         values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
@@ -398,27 +401,33 @@
         mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
     }
 
-    public void testCopyEntriesFromCursor_ReturnsMostRecentEntryTimestamp() {
-        assertEquals(10, mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor()));
-    }
-
     public void testCopyEntriesFromCursor_AllEntriesSyncedWithoutDuplicatesPresent() {
         assertStoredValues(Calls.CONTENT_URI);
-        mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
+
+        assertEquals(10, mCallLogProvider.copyEntriesFromCursor(
+                getTestCallLogCursor(), 5, /* forShadow =*/ true));
+
         assertStoredValues(Calls.CONTENT_URI,
                 getTestCallLogValues(2),
                 getTestCallLogValues(1),
                 getTestCallLogValues(0));
+        assertEquals(10, mCallLogProvider.getLastSyncTime(/* forShadow =*/ true));
+        assertEquals(0, mCallLogProvider.getLastSyncTime(/* forShadow =*/ false));
     }
 
     public void testCopyEntriesFromCursor_DuplicatesIgnoredCorrectly() {
         mResolver.insert(Calls.CONTENT_URI, getTestCallLogValues(1));
         assertStoredValues(Calls.CONTENT_URI, getTestCallLogValues(1));
-        mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
+
+        assertEquals(10, mCallLogProvider.copyEntriesFromCursor(
+                getTestCallLogCursor(), 5, /* forShadow =*/ false));
+
         assertStoredValues(Calls.CONTENT_URI,
                 getTestCallLogValues(2),
                 getTestCallLogValues(1),
                 getTestCallLogValues(0));
+        assertEquals(0, mCallLogProvider.getLastSyncTime(/* forShadow =*/ true));
+        assertEquals(10, mCallLogProvider.getLastSyncTime(/* forShadow =*/ false));
     }
 
     private ContentValues getDefaultValues(int callType) {
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 1d7931e..fd75e45 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -275,7 +275,28 @@
 
         resolver = new MockContentResolver();
         context = new RestrictionMockContext(overallContext, packageName, resolver,
-                mGrantedPermissions, mGrantedUriPermissions);
+                mGrantedPermissions, mGrantedUriPermissions) {
+            @Override
+            public Object getSystemService(String name) {
+                if (Context.COUNTRY_DETECTOR.equals(name)) {
+                    return mMockCountryDetector;
+                }
+                if (Context.ACCOUNT_SERVICE.equals(name)) {
+                    return mMockAccountManager;
+                }
+                if (Context.USER_SERVICE.equals(name)) {
+                    return mockUserManager;
+                }
+                // Use overallContext here; super.getSystemService() somehow won't return
+                // DevicePolicyManager.
+                return overallContext.getSystemService(name);
+            }
+
+            @Override
+            public String getSystemServiceName(Class<?> serviceClass) {
+                return overallContext.getSystemServiceName(serviceClass);
+            }
+        };
         this.packageName = packageName;
 
         // Let the Secure class initialize the settings provider, which is done when we first
@@ -357,12 +378,12 @@
         // info shouldn't have it.
         info.authority = stripOutUserIdFromAuthority(authority);
         provider.attachInfoForTesting(providerContext, info);
-        resolver.addProvider(authority, provider);
 
         // In case of LegacyTest, "authority" here is actually multiple authorities.
         // Register all authority here.
         for (String a : authority.split(";")) {
             resolver.addProvider(a, provider);
+            resolver.addProvider("0@" + a, provider);
         }
         return provider;
     }