Merge "Convert Android.mk file to Android.bp" am: 8dddb7086d am: 8414dcc9ab
am: cc7dc6d20b

Change-Id: I68351cb8df69da083bfa29cdd8a43afd6ba1f219
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9816d4f..501c1ae 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -37,6 +37,8 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application android:label="@string/calendar_storage"
                  android:allowBackup="false"
@@ -50,13 +52,6 @@
                 android:readPermission="android.permission.READ_CALENDAR"
                 android:writePermission="android.permission.WRITE_CALENDAR" />
 
-        <activity android:name="CalendarContentProviderTests" android:label="Calendar Content Provider"
-                android:exported="false">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.UNIT_TEST" />
-            </intent-filter>
-        </activity>
         <receiver android:name="CalendarReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index c7f1f7b..9f5c87c 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -35,8 +35,10 @@
 import android.content.OperationApplicationException;
 import android.content.UriMatcher;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
+import android.database.MatrixCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
@@ -44,6 +46,8 @@
 import android.os.Binder;
 import android.os.Process;
 import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.BaseColumns;
 import android.provider.CalendarContract;
 import android.provider.CalendarContract.Attendees;
@@ -69,6 +73,8 @@
 import com.android.internal.util.ProviderAccessStats;
 import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
 import com.android.providers.calendar.CalendarDatabaseHelper.Views;
+import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper;
+
 import com.google.android.collect.Sets;
 import com.google.common.annotations.VisibleForTesting;
 
@@ -187,6 +193,8 @@
     private CalendarDatabaseHelper mDbHelper;
     private CalendarInstancesHelper mInstancesHelper;
 
+    protected CrossProfileCalendarHelper mCrossProfileCalendarHelper;
+
     private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " +
             CalendarContract.EventsRawTimes.EVENT_ID + ", " +
             CalendarContract.EventsRawTimes.DTSTART_2445 + ", " +
@@ -531,6 +539,9 @@
 
         mCalendarCache = new CalendarCache(mDbHelper);
 
+        // Unit test overrides this method to get a mock helper.
+        initCrossProfileCalendarHelper();
+
         // This is pulled out for testing
         initCalendarAlarm();
 
@@ -539,6 +550,11 @@
         return true;
     }
 
+    @VisibleForTesting
+    protected void initCrossProfileCalendarHelper() {
+        mCrossProfileCalendarHelper = new CrossProfileCalendarHelper(mContext);
+    }
+
     protected void initCalendarAlarm() {
         mCalendarAlarm = getOrCreateCalendarAlarmManager();
     }
@@ -816,6 +832,14 @@
             String sortOrder) {
         CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
 
+        // Check if cross profile calendar is enabled in settings if caller does not come from
+        // the same profile.
+        if (isCallerCrossProfile()) {
+                if (!mCrossProfileCalendarHelper.isCrossProfileCalendarEnabledInSettings()) {
+                    return createEmptyCursor(projection);
+                }
+        }
+
         // Note don't use mCallingUid here. That's only used by mutation functions.
         final int callingUid = Binder.getCallingUid();
 
@@ -829,6 +853,91 @@
         }
     }
 
+    /**
+     * @return {@link UserInfo} of the work profile user that is linked to the current user,
+     * if any. {@code null} if there is no such user.
+     */
+    private UserInfo getWorkProfileUserInfo(Context context) {
+        final UserManager userManager = context.getSystemService(UserManager.class);
+        final int currentUserId = userManager.getUserHandle();
+
+        // Check each user.
+        for (UserInfo userInfo : userManager.getUsers()) {
+            if (!userInfo.isManagedProfile()) {
+                continue; // Not a managed user.
+            }
+            final UserInfo parent = userManager.getProfileParent(userInfo.id);
+            if (parent == null) {
+                continue; // No parent.
+            }
+            // Check if it's linked to the current user.
+            if (parent.id == currentUserId) {
+                return userInfo;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return the user ID of the work profile user that is linked to the current user
+     * if any. {@link UserHandle#USER_NULL} if there's no such user.
+     *
+     * @VisibleForTesting
+     */
+    protected int getWorkProfileUserId() {
+        final UserInfo ui = getWorkProfileUserInfo(getContext());
+        return ui == null ? UserHandle.USER_NULL : ui.id;
+    }
+
+    private static Cursor createEmptyCursor(String[] projection) {
+        return new MatrixCursor(projection);
+    }
+
+    /**
+     * @VisibleForTesting
+     */
+    protected boolean isCallerCrossProfile() {
+        return Binder.getCallingUserHandle().getIdentifier() != UserHandle.myUserId();
+    }
+
+    /**
+     * @return {@code true} if the calling package can access cross profile calendar. {@code false}
+     * otherwise.
+     */
+    private boolean canAccessCrossProfileCalendar(int workProfileUserId) {
+        // The criteria include:
+        // 1. There exists a work profile linked to the current user.
+        // 2. Profile owner of the work profile has whitelisted the calling package for cross
+        //    profile calendar.
+        return workProfileUserId != UserHandle.USER_NULL
+                && mCrossProfileCalendarHelper.isPackageWhitelisted(
+                        getCallingPackageName(), workProfileUserId);
+    }
+
+    private Cursor queryWorkProfileProvider(Uri localUri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder,
+            List<String> additionalPathSegments) {
+        // If projection is not empty, check if it's valid. Otherwise fill it with all
+        // allowed columns.
+        projection = mCrossProfileCalendarHelper.getCalibratedProjection(
+                projection, localUri);
+        // Return empty cursor if cross profile calendar is currently not available.
+        final int workProfileUserId = getWorkProfileUserId();
+        if (!canAccessCrossProfileCalendar(workProfileUserId)) {
+            return createEmptyCursor(projection);
+        }
+
+        Uri remoteUri = maybeAddUserId(
+                localUri, workProfileUserId).buildUpon().build();
+        if (additionalPathSegments != null) {
+            for (String segment : additionalPathSegments) {
+                remoteUri = Uri.withAppendedPath(remoteUri, segment);
+            }
+        }
+        return getContext().getContentResolver().query(remoteUri, projection, selection,
+                selectionArgs, sortOrder);
+    }
+
     private Cursor queryInternal(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
@@ -842,6 +951,9 @@
         String limit = null; // Not currently implemented
         String instancesTimezone;
 
+        List<String> corpAdditionalPathSegments = null;
+        final List<String> uriPathSegments = uri.getPathSegments();
+
         final int match = sUriMatcher.match(uri);
         switch (match) {
             case SYNCSTATE:
@@ -856,6 +968,13 @@
                 return mDbHelper.getSyncState().query(db, projection, selectionWithId,
                         selectionArgs, sortOrder);
 
+            case ENTERPRISE_EVENTS_ID:
+                corpAdditionalPathSegments = uriPathSegments.subList(2, uriPathSegments.size());
+            // Intentional fall from the above case.
+            case ENTERPRISE_EVENTS:
+                return queryWorkProfileProvider(Events.CONTENT_URI, projection, selection,
+                        selectionArgs, sortOrder, corpAdditionalPathSegments);
+
             case EVENTS:
                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
                 qb.setProjectionMap(sEventsProjectionMap);
@@ -891,6 +1010,13 @@
                         Calendars.ACCOUNT_TYPE);
                 break;
 
+            case ENTERPRISE_CALENDARS_ID:
+                corpAdditionalPathSegments = uriPathSegments.subList(2, uriPathSegments.size());
+                // Intentional fall from the above case.
+            case ENTERPRISE_CALENDARS:
+                return queryWorkProfileProvider(Calendars.CONTENT_URI, projection, selection,
+                        selectionArgs, sortOrder, corpAdditionalPathSegments);
+
             case CALENDARS:
             case CALENDAR_ENTITIES:
                 qb.setTables(Tables.CALENDARS);
@@ -945,6 +1071,22 @@
                 return handleInstanceSearchQuery(qb, begin, end, query, projection, selection,
                         selectionArgs, sortOrder, match == INSTANCES_SEARCH_BY_DAY,
                         instancesTimezone, isHomeTimezone());
+            case ENTERPRISE_INSTANCES:
+                corpAdditionalPathSegments = uriPathSegments.subList(3, uriPathSegments.size());
+                return queryWorkProfileProvider(Instances.CONTENT_URI, projection, selection,
+                        selectionArgs, sortOrder, corpAdditionalPathSegments);
+            case ENTERPRISE_INSTANCES_BY_DAY:
+                corpAdditionalPathSegments = uriPathSegments.subList(3, uriPathSegments.size());
+                return queryWorkProfileProvider(Instances.CONTENT_BY_DAY_URI, projection, selection,
+                        selectionArgs, sortOrder, corpAdditionalPathSegments);
+            case ENTERPRISE_INSTANCES_SEARCH:
+                corpAdditionalPathSegments = uriPathSegments.subList(3, uriPathSegments.size());
+                return queryWorkProfileProvider(Instances.CONTENT_SEARCH_URI, projection, selection,
+                        selectionArgs, sortOrder, corpAdditionalPathSegments);
+            case ENTERPRISE_INSTANCES_SEARCH_BY_DAY:
+                corpAdditionalPathSegments = uriPathSegments.subList(3, uriPathSegments.size());
+                return queryWorkProfileProvider(Instances.CONTENT_SEARCH_BY_DAY_URI, projection,
+                        selection, selectionArgs, sortOrder, corpAdditionalPathSegments);
             case EVENT_DAYS:
                 int startDay;
                 int endDay;
@@ -4700,6 +4842,14 @@
     private static final int EXCEPTION_ID2 = 30;
     private static final int EMMA = 31;
     private static final int COLORS = 32;
+    private static final int ENTERPRISE_EVENTS = 33;
+    private static final int ENTERPRISE_EVENTS_ID = 34;
+    private static final int ENTERPRISE_CALENDARS = 35;
+    private static final int ENTERPRISE_CALENDARS_ID = 36;
+    private static final int ENTERPRISE_INSTANCES = 37;
+    private static final int ENTERPRISE_INSTANCES_BY_DAY = 38;
+    private static final int ENTERPRISE_INSTANCES_SEARCH = 39;
+    private static final int ENTERPRISE_INSTANCES_SEARCH_BY_DAY = 40;
 
     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     private static final HashMap<String, String> sInstancesProjectionMap;
@@ -4750,6 +4900,21 @@
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#/#", EXCEPTION_ID2);
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "emma", EMMA);
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "colors", COLORS);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/events", ENTERPRISE_EVENTS);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/events/#",
+                ENTERPRISE_EVENTS_ID);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/calendars",
+                ENTERPRISE_CALENDARS);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/calendars/#",
+                ENTERPRISE_CALENDARS_ID);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/instances/when/*/*",
+                ENTERPRISE_INSTANCES);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/instances/whenbyday/*/*",
+                ENTERPRISE_INSTANCES_BY_DAY);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/instances/search/*/*/*",
+                ENTERPRISE_INSTANCES_SEARCH);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "enterprise/instances/searchbyday/*/*/*",
+                ENTERPRISE_INSTANCES_SEARCH_BY_DAY);
 
         /** Contains just BaseColumns._COUNT */
         sCountProjectionMap = new HashMap<String, String>();
@@ -5144,7 +5309,8 @@
         }
     }
 
-    private String getCallingPackageName() {
+    @VisibleForTesting
+    protected String getCallingPackageName() {
         if (getCachedCallingPackage() != null) {
             // If the calling package is null, use the best available as a fallback.
             return getCachedCallingPackage();
diff --git a/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelper.java b/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelper.java
new file mode 100644
index 0000000..1c969bf
--- /dev/null
+++ b/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelper.java
@@ -0,0 +1,188 @@
+/*
+ * 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.calendar.enterprise;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.CalendarContract;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.Set;
+
+/**
+ * Helper class for cross profile calendar related policies.
+ */
+public class CrossProfileCalendarHelper {
+
+    private static final String LOG_TAG = "CrossProfileCalendarHelper";
+
+    final private Context mContext;
+
+    public static final Set<String> EVENTS_TABLE_WHITELIST;
+    public static final Set<String> CALENDARS_TABLE_WHITELIST;
+    public static final Set<String> INSTANCES_TABLE_WHITELIST;
+
+    static {
+        EVENTS_TABLE_WHITELIST = new ArraySet<>();
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events._ID);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.CALENDAR_ID);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.TITLE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_LOCATION);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_COLOR);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.STATUS);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTSTART);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTEND);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_TIMEZONE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_END_TIMEZONE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DURATION);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.ALL_DAY);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.AVAILABILITY);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RRULE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RDATE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXRULE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXDATE);
+
+        CALENDARS_TABLE_WHITELIST = new ArraySet<>();
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars._ID);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.NAME);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_LOCATION);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE);
+        CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY);
+
+        INSTANCES_TABLE_WHITELIST = new ArraySet<>();
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances._ID);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.EVENT_ID);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.BEGIN);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_DAY);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_DAY);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_MINUTE);
+        INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_MINUTE);
+
+        // Add calendar columns.
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE);
+        EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE);
+
+        ((ArraySet<String>) INSTANCES_TABLE_WHITELIST).addAll(EVENTS_TABLE_WHITELIST);
+    }
+
+    public CrossProfileCalendarHelper(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * @return a context created from the given context for the given user, or null if it fails.
+     */
+    private Context createPackageContextAsUser(Context context, int userId) {
+        try {
+            return context.createPackageContextAsUser(
+                    context.getPackageName(), 0 /* flags */, UserHandle.of(userId));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(LOG_TAG, "Failed to create user context", e);
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether a packaged is whitelisted by the profile owner of a work profile to access
+     * the work calendar provider.
+     *
+     * @param packageName  the name of the package
+     * @param managedProfileUserId the user id of the work profile
+     * @return {@code true} if the package is whitelisted, {@false} otherwise.
+     */
+    public boolean isPackageWhitelisted(String packageName, int managedProfileUserId) {
+        final Context managedProfileUserContext = createPackageContextAsUser(
+                mContext, managedProfileUserId);
+        final DevicePolicyManager mDpm = managedProfileUserContext.getSystemService(
+                DevicePolicyManager.class);
+        return mDpm.isPackageAllowedToAccessCalendar(packageName);
+    }
+
+    /**
+     * Returns if cross profile calendar is allowed for a work profile in Settings.
+     *
+     * @return {@code true} if cross profile calendar is allowed in Settings, {@false} otherwise.
+     */
+    public boolean isCrossProfileCalendarEnabledInSettings() {
+        final int enabled = Settings.Secure.getInt(mContext.getContentResolver(),
+                Settings.Secure.CROSS_PROFILE_CALENDAR_ENABLED, /* default = */0);
+        return enabled == 1;
+    }
+
+    private static void ensureProjectionAllowed(String[] projection, Set<String> validColumnsSet) {
+        for (String column : projection) {
+            if (!validColumnsSet.contains(column)) {
+                throw new IllegalArgumentException(String.format("Column %s is not "
+                        + "allowed to be accessed from cross profile Uris", column));
+            }
+        }
+    }
+
+    /**
+     * Returns the calibrated version of projection for a given table.
+     *
+     * If the input projection is empty, return an array of all the whitelisted columns for a
+     * given table. Table is determined by the input uri.
+     *
+     * @param projection the original projection
+     * @param localUri the local uri for the query of the projection
+     * @return the original value of the input projection if it's not empty, otherwise an array of
+     * all the whitelisted columns.
+     * @throws IllegalArgumentException if the input projection contains a column that is not
+     * whitelisted for a given table.
+     */
+    public String[] getCalibratedProjection(String[] projection, Uri localUri) {
+        // If projection is not empty, check if it's valid. Otherwise fill it with all
+        // allowed columns.
+        Set<String> validColumnsSet = new ArraySet<String>();
+        if (CalendarContract.Events.CONTENT_URI.equals(localUri)) {
+            validColumnsSet = EVENTS_TABLE_WHITELIST;
+        } else if (CalendarContract.Calendars.CONTENT_URI.equals(localUri)) {
+            validColumnsSet = CALENDARS_TABLE_WHITELIST;
+        } else if (CalendarContract.Instances.CONTENT_URI.equals(localUri)
+                || CalendarContract.Instances.CONTENT_BY_DAY_URI.equals(localUri)
+                || CalendarContract.Instances.CONTENT_SEARCH_URI.equals(localUri)
+                || CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI.equals(localUri)) {
+            validColumnsSet = INSTANCES_TABLE_WHITELIST;
+        } else {
+            throw new IllegalArgumentException(String.format("Cross profile version of %d is not "
+                    + "supported", localUri.toSafeString()));
+        }
+
+        if (projection != null && projection.length > 0) {
+            // If there exists some columns in original projection, check if these columns are
+            // allowed.
+            ensureProjectionAllowed(projection, validColumnsSet);
+            return projection;
+        }
+        // Query of content provider will return cursor that contains all columns if projection is
+        // null or empty. To be consistent with this behavior, we fill projection with all allowed
+        // columns if it's null or empty for cross profile Uris.
+        return validColumnsSet.toArray(new String[validColumnsSet.size()]);
+    }
+}
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
index f73168e..a899fc1 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
@@ -5,7 +5,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.net.Uri;
-import android.os.PowerManager;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -44,6 +43,11 @@
         return true;
     }
 
+    @Override
+    protected String getCallingPackageName() {
+        return "";
+    }
+
     private static class MockCalendarAlarmManager extends CalendarAlarmManager {
 
         public MockCalendarAlarmManager(Context context) {
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
index 52cb8a3..b66eb8a 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
@@ -50,6 +50,8 @@
 import android.text.format.Time;
 import android.util.Log;
 
+import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper;
+
 import java.io.File;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -91,9 +93,12 @@
     private static final String DEFAULT_SORT_ORDER = "begin ASC";
 
     private CalendarProvider2ForTesting mProvider;
+    private CalendarProvider2ForTesting mWorkProfileProvider;
+
     private SQLiteDatabase mDb;
     private MetaData mMetaData;
     private Context mContext;
+    private Context mWorkContext;
     private MockContentResolver mResolver;
     private Uri mEventsUri = Events.CONTENT_URI;
     private Uri mCalendarsUri = Calendars.CONTENT_URI;
@@ -118,6 +123,36 @@
     private static final long ONE_HOUR_MILLIS = 3600*1000;
     private static final long ONE_WEEK_MILLIS = 7 * 24 * 3600 * 1000;
 
+    private static final int WORK_PROFILE_USER_ID = 10;
+    private static final String WORK_PROFILE_AUTHORITY = String.format("%d@%s",
+            WORK_PROFILE_USER_ID, CalendarContract.AUTHORITY);
+
+    private static long parseTimeStringToMillis(String timeStr, String timeZone) {
+        Time time = new Time(timeZone);
+        time.parse3339(timeStr);
+        return time.toMillis(false /* use isDst */);
+    }
+
+    private static String WORK_DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES;
+
+    private static String WORK_CALENDAR_TITLE = "Calendar1";
+    private static String WORK_CALENDAR_TITLE_STANDBY = "Calendar2";
+    private static int WORK_CALENDAR_COLOR = 0xFFFF0000;
+
+    private static String WORK_EVENT_TITLE = "event_title1";
+    private static String WORK_EVENT_TITLE_STANDBY = "event_title2";
+    private static long WORK_EVENT_DTSTART = parseTimeStringToMillis(
+            "2018-05-01T00:00:00", WORK_DEFAULT_TIMEZONE);
+    private static long WORK_EVENT_DTEND = parseTimeStringToMillis(
+            "2018-05-01T20:00:00", WORK_DEFAULT_TIMEZONE);
+    private final long WORK_EVENT_DTSTART_STANDBY = parseTimeStringToMillis(
+            "2008-05-01T00:00:00", WORK_DEFAULT_TIMEZONE);
+    private final long WORK_EVENT_DTEND_STANDBY = parseTimeStringToMillis(
+            "2008-05-01T20:00:00", WORK_DEFAULT_TIMEZONE);
+    private static int WORK_EVENT_COLOR = 0xff123456;
+    private static String WORK_EVENT_LOCATION = "Work event location.";
+    private static String WORK_EVENT_DESCRIPTION = "This is a work event.";
+
     /**
      * We need a few more stub methods so that our tests can run
      */
@@ -964,14 +999,52 @@
             }
         };
 
-        mProvider = new CalendarProvider2ForTesting();
+        mWorkContext = new IsolatedContext(mResolver, targetContextWrapper) {
+            @Override
+            public int getUserId() {
+                return WORK_PROFILE_USER_ID;
+            }
+        };
+
+        mProvider = new CalendarProvider2ForTesting() {
+            @Override
+            protected int getWorkProfileUserId() {
+                return WORK_PROFILE_USER_ID;
+            }
+
+            @Override
+            protected void initCrossProfileCalendarHelper() {
+                mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext);
+            }
+
+            @Override
+            protected boolean isCallerCrossProfile() {
+                return false;
+            }
+        };
         ProviderInfo info = new ProviderInfo();
         info.authority = CalendarContract.AUTHORITY;
         mProvider.attachInfoForTesting(mContext, info);
 
+        mWorkProfileProvider = new CalendarProvider2ForTesting() {
+            @Override
+            protected void initCrossProfileCalendarHelper() {
+                mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext);
+            }
+
+            @Override
+            protected boolean isCallerCrossProfile() {
+                return true;
+            }
+        };
+        ProviderInfo workProviderInfo = new ProviderInfo();
+        workProviderInfo.authority = WORK_PROFILE_AUTHORITY;
+        mWorkProfileProvider.attachInfoForTesting(mWorkContext, info);
+
         mResolver.addProvider(CalendarContract.AUTHORITY, mProvider);
         mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
         mResolver.addProvider("sync", new MockProvider("sync"));
+        mResolver.addProvider(WORK_PROFILE_AUTHORITY, mWorkProfileProvider);
 
         mMetaData = getProvider().mMetaData;
         mForceDtend = false;
@@ -3122,4 +3195,364 @@
         checkCalendarCount(0);
     }
 
+    public void testEnterpriseInstancesGetCorrectValue() {
+        final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        insertWorkEvent(WORK_EVENT_TITLE, calendarId,
+                WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
+        insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
+                WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        Uri.Builder builder = Instances.ENTERPRISE_CONTENT_URI.buildUpon();
+        ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS);
+        ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
+        String[] projection = new String[]{
+                Instances.TITLE,
+                Instances.CALENDAR_ID,
+                Instances.DTSTART,
+                Instances.CALENDAR_DISPLAY_NAME,
+        };
+        Cursor cursor = mResolver.query(
+                builder.build(),
+                projection, null, null, null);
+
+        // Test the return cursor is correct when the all checks are met.
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(WORK_EVENT_TITLE, cursor.getString(0));
+        assertEquals(calendarId, cursor.getLong(1));
+        assertEquals(WORK_EVENT_DTSTART, cursor.getLong(2));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(3));
+
+        cleanupEnterpriseTestForEvents(calendarId, 2);
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseInstancesContentSearch() {
+        final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        insertWorkEvent(WORK_EVENT_TITLE, calendarId,
+                WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
+        insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
+                WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        Uri.Builder builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon();
+        ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS);
+        ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
+        builder = builder.appendPath(WORK_EVENT_TITLE /* search query */);
+        Cursor cursor = mResolver.query(
+                builder.build(),
+                null, null, null, null);
+        // There is only one event that meets the search criteria.
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+
+        builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon();
+        ContentUris.appendId(builder, WORK_EVENT_DTSTART_STANDBY - DateUtils.YEAR_IN_MILLIS);
+        ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS);
+        builder = builder.appendPath(WORK_EVENT_DESCRIPTION /* search query */);
+        cursor = mResolver.query(
+                builder.build(),
+                null, null, null, null);
+        // There are two events that meet the search criteria.
+        assertNotNull(cursor);
+        assertEquals(2, cursor.getCount());
+
+        cleanupEnterpriseTestForEvents(calendarId, 2);
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseEventsGetCorrectValue() {
+        final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
+                WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
+        insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
+                WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        String selection = "(" + Events.TITLE + " = ? )";
+        String[] selectionArgs = new String[]{
+                WORK_EVENT_TITLE
+        };
+        String[] projection = new String[]{
+                Events._ID,
+                Events.TITLE,
+                Events.CALENDAR_ID,
+                Events.DTSTART,
+                Calendars.CALENDAR_DISPLAY_NAME
+        };
+        Cursor cursor = mResolver.query(
+                Events.ENTERPRISE_CONTENT_URI,
+                projection, selection, selectionArgs, null);
+
+        // Test the return cursor is correct when the all checks are met.
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(idToTest, cursor.getLong(0));
+        assertEquals(WORK_EVENT_TITLE, cursor.getString(1));
+        assertEquals(calendarId, cursor.getLong(2));
+        assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(4));
+
+        cleanupEnterpriseTestForEvents(calendarId, 2);
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseEventsGetCorrectValueWithId() {
+        final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
+                WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
+        insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
+                WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        // Test ENTERPRISE_CONTENT_URI_ID.
+        String[] projection = new String[]{
+                Events._ID,
+                Events.TITLE,
+                Events.CALENDAR_ID,
+                Events.DTSTART,
+                Calendars.CALENDAR_DISPLAY_NAME
+        };
+        final Cursor cursor = mResolver.query(
+                ContentUris.withAppendedId(Events.ENTERPRISE_CONTENT_URI, idToTest),
+                projection, null, null, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(idToTest, cursor.getLong(0));
+        assertEquals(WORK_EVENT_TITLE, cursor.getString(1));
+        assertEquals(calendarId, cursor.getLong(2));
+        assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(4));
+
+        cleanupEnterpriseTestForEvents(calendarId, 2);
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseEventsProjectionCalibration() {
+        final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId,
+                WORK_EVENT_DTSTART, WORK_EVENT_DTEND);
+        insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId,
+                WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        // Test all whitelisted columns are returned when projection is empty.
+        String selection = "(" + Events.TITLE + " = ? )";
+        String[] selectionArgs = new String[]{
+                WORK_EVENT_TITLE
+        };
+        final Cursor cursor = mResolver.query(
+                Events.ENTERPRISE_CONTENT_URI,
+                new String[] {}, selection, selectionArgs, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        for (String column : CrossProfileCalendarHelper.EVENTS_TABLE_WHITELIST) {
+            final int index = cursor.getColumnIndex(column);
+            assertTrue(index != -1);
+        }
+        assertEquals(idToTest, cursor.getLong(
+                cursor.getColumnIndex(Events._ID)));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(
+                cursor.getColumnIndex(Calendars.CALENDAR_DISPLAY_NAME)));
+        assertEquals(calendarId, cursor.getLong(
+                cursor.getColumnIndex(Events.CALENDAR_ID)));
+
+        cleanupEnterpriseTestForEvents(calendarId, 2);
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    private void cleanupEnterpriseTestForEvents(long calendarId, int numToDelete) {
+        // Selection arguments must be provided when deleting events.
+        final int numDeleted = mWorkProfileProvider.delete(Events.CONTENT_URI,
+                "(" + Events.CALENDAR_ID + " = ? )",
+                new String[]{String.valueOf(calendarId)});
+        assertTrue(numDeleted == numToDelete);
+    }
+
+    private long insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd) {
+        final ContentValues cv = new ContentValues();
+        cv.put(Events.TITLE, eventTitle);
+        cv.put(Events.CALENDAR_ID, calendarId);
+        cv.put(Events.DESCRIPTION, WORK_EVENT_DESCRIPTION);
+        cv.put(Events.EVENT_LOCATION, WORK_EVENT_LOCATION);
+        cv.put(Events.EVENT_COLOR, WORK_EVENT_COLOR);
+        cv.put(Events.DTSTART, dtStart);
+        cv.put(Events.DTEND, dtEnd);
+        cv.put(Events.EVENT_TIMEZONE, WORK_DEFAULT_TIMEZONE);
+        final Uri uri = mWorkProfileProvider.insert(Events.CONTENT_URI, cv);
+        return Long.parseLong(uri.getLastPathSegment());
+    }
+
+    public void testEnterpriseCalendarGetCorrectValue() {
+        final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        // Test the return cursor is correct when the all checks are met.
+        String selection = "(" + Calendars.CALENDAR_DISPLAY_NAME + " = ? )";
+        String[] selectionArgs = new String[] {
+                WORK_CALENDAR_TITLE
+        };
+        String[] projection = new String[] {
+                Calendars._ID,
+                Calendars.CALENDAR_DISPLAY_NAME,
+                Calendars.CALENDAR_COLOR
+        };
+        Cursor cursor = mResolver.query(
+                Calendars.ENTERPRISE_CONTENT_URI,
+                projection, selection, selectionArgs, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(idToTest, cursor.getLong(0));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(1));
+        assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(2));
+
+        cleanupEnterpriseTestForCalendars(2);
+    }
+
+    public void testEnterpriseCalendarGetCorrectValueWithId() {
+        final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        // Test Calendars.ENTERPRISE_CONTENT_URI with id.
+        String[] projection = new String[] {
+                Calendars._ID,
+                Calendars.CALENDAR_DISPLAY_NAME,
+                Calendars.CALENDAR_COLOR
+        };
+        final Cursor cursor = mResolver.query(
+                ContentUris.withAppendedId(Calendars.ENTERPRISE_CONTENT_URI, idToTest),
+                projection, null, null, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(idToTest, cursor.getLong(0));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(1));
+        assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(2));
+
+        cleanupEnterpriseTestForCalendars(2);
+    }
+
+    public void testEnterpriseCalendarsProjectionCalibration() {
+        final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE);
+        // Assume cross profile uri access is allowed by policy and settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+
+        // Test all whitelisted columns are returned when projection is empty.
+        final Cursor cursor = mResolver.query(
+                Calendars.ENTERPRISE_CONTENT_URI,
+                new String[] {}, null, null, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        for (String column : CrossProfileCalendarHelper.CALENDARS_TABLE_WHITELIST) {
+            final int index = cursor.getColumnIndex(column);
+            assertTrue(index != -1);
+        }
+        assertEquals(idToTest, cursor.getLong(
+                cursor.getColumnIndex(Calendars._ID)));
+        assertEquals(WORK_CALENDAR_TITLE, cursor.getString(
+                cursor.getColumnIndex(Calendars.CALENDAR_DISPLAY_NAME)));
+        assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(
+                cursor.getColumnIndex(Calendars.CALENDAR_COLOR)));
+
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseCalendarsNonWhitelistedProjection() {
+        // Test SecurityException is thrown there is non-whitelisted column in the projection.
+        try {
+            String[] projection = new String[] {
+                    Calendars._ID,
+                    Calendars.CALENDAR_DISPLAY_NAME,
+                    Calendars.CALENDAR_COLOR,
+                    Calendars.OWNER_ACCOUNT
+            };
+            mResolver.query(
+                    Calendars.ENTERPRISE_CONTENT_URI,
+                    projection, null, null, null);
+            fail("IllegalArgumentException is not thrown when querying non-whitelisted columns");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    public void testEnterpriseCalendarsDisabledInSettings() {
+        insertWorkCalendar(WORK_CALENDAR_TITLE);
+        // Assume cross profile uri access is allowed by policy but disabled settings.
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(true);
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(false);
+
+        // Test empty cursor is returned if cross profile calendar is disabled in settings.
+        final Cursor cursor = mResolver.query(
+                Calendars.ENTERPRISE_CONTENT_URI,
+                new String[]{}, null, null, null);
+        assertTrue(cursor != null);
+        assertTrue(cursor.getCount() == 0);
+
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    public void testEnterpriseCalendarsNonWhitelistedPackage() {
+        insertWorkCalendar(WORK_CALENDAR_TITLE);
+        // Assume cross profile uri access is allowed by settings but disabled by policy.
+        MockCrossProfileCalendarHelper.setCrossProfileCalendarEnabledInSettings(true);
+        MockCrossProfileCalendarHelper.setPackageWhitelisted(false);
+
+        // Test empty cursor is returned if cross profile calendar is disabled by policy.
+        final Cursor cursor = mResolver.query(
+                Calendars.ENTERPRISE_CONTENT_URI,
+                new String[]{}, null, null, null);
+        assertTrue(cursor != null);
+        assertTrue(cursor.getCount() == 0);
+
+        cleanupEnterpriseTestForCalendars(1);
+    }
+
+    // Remove the two inserted calendars.
+    private void cleanupEnterpriseTestForCalendars(int numToDelete) {
+        final int numDeleted =  mWorkProfileProvider.delete(Calendars.CONTENT_URI, null, null);
+        assertTrue(numDeleted == numToDelete);
+    }
+
+    private long insertWorkCalendar(String displayName) {
+        final ContentValues cv = new ContentValues();
+        cv.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
+        cv.put(Calendars.OWNER_ACCOUNT, DEFAULT_ACCOUNT);
+        cv.put(Calendars.ACCOUNT_NAME, DEFAULT_ACCOUNT);
+        cv.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
+        cv.put(Calendars.CALENDAR_COLOR, WORK_CALENDAR_COLOR);
+        cv.put(Calendars.CALENDAR_TIME_ZONE, WORK_DEFAULT_TIMEZONE);
+        final Uri uri = mWorkProfileProvider.insert(
+                addSyncQueryParams(Calendars.CONTENT_URI, "local_account",
+                        CalendarContract.ACCOUNT_TYPE_LOCAL), cv);
+        return Long.parseLong(uri.getLastPathSegment());
+    }
 }
+
diff --git a/tests/src/com/android/providers/calendar/MockCrossProfileCalendarHelper.java b/tests/src/com/android/providers/calendar/MockCrossProfileCalendarHelper.java
new file mode 100644
index 0000000..2eba442
--- /dev/null
+++ b/tests/src/com/android/providers/calendar/MockCrossProfileCalendarHelper.java
@@ -0,0 +1,57 @@
+/*
+ * 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.calendar;
+
+import android.content.Context;
+
+import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper;
+
+public class MockCrossProfileCalendarHelper extends CrossProfileCalendarHelper {
+
+    private static boolean mCrossProfileCalendarEnabledInSettings = true;
+    private static boolean mCallingPackageWhitelisted = true;
+
+    public MockCrossProfileCalendarHelper (Context context) {
+        super(context);
+    }
+
+    /**
+     * Mock this method in unit test since it depends on Settings provider.
+     * It will be tested in integration test.
+     */
+    @Override
+    public boolean isCrossProfileCalendarEnabledInSettings() {
+        return mCrossProfileCalendarEnabledInSettings;
+    }
+
+    public static void setCrossProfileCalendarEnabledInSettings(boolean enabled) {
+        mCrossProfileCalendarEnabledInSettings = enabled;
+    }
+
+    /**
+     * Mock this method in unit test since it depends on DevicePolicyManager.
+     * It will be tested in integration test.
+     */
+    @Override
+    public boolean isPackageWhitelisted(String packageName, int managedProfileUserId) {
+        return mCallingPackageWhitelisted;
+    }
+
+    public static void setPackageWhitelisted(boolean isWhitelisted) {
+        mCallingPackageWhitelisted = isWhitelisted;
+    }
+}
diff --git a/tests/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelperTest.java b/tests/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelperTest.java
new file mode 100644
index 0000000..4b28476
--- /dev/null
+++ b/tests/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.calendar.enterprise;
+
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.Instances;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+public class CrossProfileCalendarHelperTest extends AndroidTestCase {
+
+    private CrossProfileCalendarHelper mHelper;
+
+    public void setUp() throws Exception {
+        super.setUp();
+        mHelper = new CrossProfileCalendarHelper(mContext);
+    }
+
+    public void testProjectionNotWhitelisted_throwErrorForCalendars() {
+        final String[] projection = new String[]{
+                Calendars._ID,
+                Calendars.OWNER_ACCOUNT
+        };
+        try {
+            mHelper.getCalibratedProjection(projection, Calendars.CONTENT_URI);
+            fail(String.format("Exception not found for projection %s", Calendars.OWNER_ACCOUNT));
+        } catch (IllegalArgumentException e) {
+            // Do nothing.
+        }
+    }
+
+    public void testProjectionNotWhitelisted_throwErrorForEvents() {
+        final String[] projection = new String[] {
+                Events._ID,
+                Events.DESCRIPTION
+        };
+        try {
+            mHelper.getCalibratedProjection(projection, Events.CONTENT_URI);
+            fail(String.format("Exception not found for projection %s", Events.DESCRIPTION));
+        } catch (IllegalArgumentException e) {
+            // Do nothing.
+        }
+    }
+
+    public void testProjectionNotWhitelisted_throwErrorForInstances() {
+        final String[] projection = new String[] {
+                Instances._ID,
+                Events.DESCRIPTION
+        };
+        try {
+            mHelper.getCalibratedProjection(projection, Instances.CONTENT_URI);
+            fail(String.format("Exception not found for projection %s", Events.DESCRIPTION));
+        } catch (IllegalArgumentException e) {
+            // Do nothing.
+        }
+    }
+
+    public void testNoProjection_getFullWhitelistedProjectionForCalendars() {
+        final String[] projection = mHelper.getCalibratedProjection(null, Calendars.CONTENT_URI);
+        final Set<String> projectionSet = new ArraySet<String>(Arrays.asList(projection));
+        assertTrue(Objects.deepEquals(CrossProfileCalendarHelper.CALENDARS_TABLE_WHITELIST,
+                projectionSet));
+    }
+
+    public void testNoProjection_getFullWhitelistedProjectionForEvents() {
+        final String[] projection = mHelper.getCalibratedProjection(null, Events.CONTENT_URI);
+        final Set<String> projectionSet = new ArraySet<String>(Arrays.asList(projection));
+        assertTrue(CrossProfileCalendarHelper.EVENTS_TABLE_WHITELIST.equals(projectionSet));
+    }
+
+    public void testNoProjection_getFullWhitelistedProjectionForInstances() {
+        final String[] projection = mHelper.getCalibratedProjection(null, Instances.CONTENT_URI);
+        final Set<String> projectionSet = new ArraySet<String>(Arrays.asList(projection));
+        assertTrue(CrossProfileCalendarHelper.INSTANCES_TABLE_WHITELIST.equals(projectionSet));
+    }
+}