diff options
author | Xin Li <delphij@google.com> | 2019-09-04 13:34:54 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2019-09-04 13:34:54 -0700 |
commit | 78bc12304a60669ca931cc0bd7ceda0ee853b15e (patch) | |
tree | 2250d0ca95b1f4d9737844a407824a5914a4296f | |
parent | 49003cfd280426f7da67ef05a0d2fff5f546ac59 (diff) | |
parent | 2e1c5e9b0cf88594645171c42e737daf18f95366 (diff) | |
download | CalendarProvider-78bc12304a60669ca931cc0bd7ceda0ee853b15e.tar.gz |
DO NOT MERGE - Merge Android 10 into master
Bug: 139893257
Change-Id: I12c299b18f26a6135dc8573693318679f443b5df
9 files changed, 1051 insertions, 17 deletions
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/CalendarAlarmManager.java b/src/com/android/providers/calendar/CalendarAlarmManager.java index 7019797..e64cac2 100644 --- a/src/com/android/providers/calendar/CalendarAlarmManager.java +++ b/src/com/android/providers/calendar/CalendarAlarmManager.java @@ -181,7 +181,7 @@ public class CalendarAlarmManager { // Trigger the check in 5s from now, so that we can have batch processing. long triggerAtTime = SystemClock.elapsedRealtime() + ALARM_CHECK_DELAY_MILLIS; // Given to the short delay, we just use setExact here. - setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending); + setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pending); } } @@ -482,6 +482,10 @@ public class CalendarAlarmManager { mAlarmManager.set(type, triggerAtTime, operation); } + public void setAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) { + mAlarmManager.setAndAllowWhileIdle(type, triggerAtTime, operation); + } + public void setExact(int type, long triggerAtTime, PendingIntent operation) { mAlarmManager.setExact(type, triggerAtTime, operation); } diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java index bb979c6..1e2b986 100644 --- a/src/com/android/providers/calendar/CalendarDatabaseHelper.java +++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java @@ -74,7 +74,7 @@ import java.util.TimeZone; // 5xx for JB MR1 // 6xx for K // Bump this to the next hundred at each major release. - static final int DATABASE_VERSION = 600; + static final int DATABASE_VERSION = 601; private static final int PRE_FROYO_SYNC_STATE_VERSION = 3; @@ -1412,6 +1412,11 @@ import java.util.TimeZone; createEventsView = true; // This is needed if the calendars or events schema changed oldVersion = 600; } + if (oldVersion < 601) { + // There are no table changes in 601, but recreating the events view is required + createEventsView = true; + oldVersion = 601; + } if (createEventsView) { createEventsView(db); @@ -3236,6 +3241,7 @@ import java.util.TimeZone; + Calendars.CAN_ORGANIZER_RESPOND + "," + Calendars.CAN_MODIFY_TIME_ZONE + "," + Calendars.CAN_PARTIALLY_UPDATE + "," + + Calendars.IS_PRIMARY + "," + Calendars.CAL_SYNC1 + "," + Calendars.CAL_SYNC2 + "," + Calendars.CAL_SYNC3 + "," diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java index c7f1f7b..4a6bd22 100644 --- a/src/com/android/providers/calendar/CalendarProvider2.java +++ b/src/com/android/providers/calendar/CalendarProvider2.java @@ -35,8 +35,10 @@ import android.content.IntentFilter; 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.net.Uri; 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.calendarcommon2.RecurrenceSet; 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 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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 + ", " + @@ -442,6 +450,10 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun /** set to 'true' to enable debug logging for recurrence exception code */ private static final boolean DEBUG_EXCEPTION = false; + + private static final String SELECTION_PRIMARY_CALENDAR = + Calendars.IS_PRIMARY + "= 1" + + " OR " + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT; private final ThreadLocal<Boolean> mCallingPackageErrorLogged = new ThreadLocal<Boolean>(); @@ -454,6 +466,8 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>(); private final ProviderAccessStats mStats = new ProviderAccessStats(); + private int mParentUserId; + /** * Listens for timezone changes and disk-no-longer-full events */ @@ -531,14 +545,24 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun mCalendarCache = new CalendarCache(mDbHelper); + // Unit test overrides this method to get a mock helper. + initCrossProfileCalendarHelper(); + // This is pulled out for testing initCalendarAlarm(); + mParentUserId = getParentUserId(); + postInitialize(); return true; } + @VisibleForTesting + protected void initCrossProfileCalendarHelper() { + mCrossProfileCalendarHelper = new CrossProfileCalendarHelper(mContext); + } + protected void initCalendarAlarm() { mCalendarAlarm = getOrCreateCalendarAlarmManager(); } @@ -792,12 +816,25 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun mCalendarAlarm.rescheduleMissedAlarms(); } + @VisibleForTesting + protected int getParentUserId() { + final UserManager userManager = mContext.getSystemService(UserManager.class); + final UserInfo parentUser = userManager.getProfileParent(UserHandle.myUserId()); + return parentUser == null ? UserHandle.USER_NULL : parentUser.id; + } @Override protected void notifyChange(boolean syncToNetwork) { // Note that semantics are changed: notification is for CONTENT_URI, not the specific // Uri that was modified. mContentResolver.notifyChange(CalendarContract.CONTENT_URI, null, syncToNetwork); + // If this is a managed profile CalendarProvider, notify the content observers of + // enterprise uris in the parent profile. + if (mParentUserId != UserHandle.USER_NULL) { + mContentResolver.notifyChange( + CalendarContract.ENTERPRISE_CONTENT_URI, + /* observer = */ null, /* syncToNetwork = */ false, mParentUserId); + } } /** @@ -829,6 +866,103 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun } } + /** + * @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, and if work profile is disabled. + if (parent.id == currentUserId + && !userManager.isQuietModeEnabled(UserHandle.of(userInfo.id))) { + 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); + } + + /** + * @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 and the work profile is not + // disabled. + // 2. Profile owner of the work profile has allowed the calling package for cross + // profile calendar. + // 3. CROSS_PROFILE_CALENDAR_ENABLED is turned on in Settings. + return workProfileUserId != UserHandle.USER_NULL + && mCrossProfileCalendarHelper.isPackageAllowedToAccessCalendar( + getCallingPackageName(), workProfileUserId); + } + + private String appendPrimaryOnlyToSelection(String selection) { + return TextUtils.isEmpty(selection) + ? SELECTION_PRIMARY_CALENDAR + : selection + " AND (" + SELECTION_PRIMARY_CALENDAR + ")"; + } + + /* + * Throw UnsupportedOperationException if + * <p>1. Work profile doesn't exits or disabled. + * <p>2. Calling package is not allowed to access cross profile calendar. + * <p>3. CROSS_PROFILE_CALENDAR_ENABLED is turned off in Settings. + */ + 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); + // Throw exception if cross profile calendar is currently not available. + final int workProfileUserId = getWorkProfileUserId(); + if (!canAccessCrossProfileCalendar(workProfileUserId)) { + throw new UnsupportedOperationException("Can't access cross profile for " + localUri); + } + + Uri remoteUri = maybeAddUserId( + localUri, workProfileUserId).buildUpon().build(); + if (additionalPathSegments != null) { + for (String segment : additionalPathSegments) { + remoteUri = Uri.withAppendedPath(remoteUri, segment); + } + } + + selection = appendPrimaryOnlyToSelection(selection); + + final Cursor cursor = getContext().getContentResolver().query(remoteUri, projection, + selection, selectionArgs, sortOrder); + return cursor == null ? createEmptyCursor(projection) : cursor; + } + private Cursor queryInternal(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -842,6 +976,9 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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 +993,13 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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 +1035,13 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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 +1096,22 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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; @@ -4160,8 +4327,6 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun } if (events.getCount() == 0) { - Log.i(TAG, "No events to update: uri=" + uri + " selection=" + selection + - " selectionArgs=" + Arrays.toString(selectionArgs)); return 0; } @@ -4645,14 +4810,16 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun Log.d(TAG, "sendUpdateNotification: delay=" + delay); } - mCalendarAlarm.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delay, + mCalendarAlarm.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + delay, PendingIntent.getBroadcast(mContext, 0, createProviderChangedBroadcast(), PendingIntent.FLAG_UPDATE_CURRENT)); } private Intent createProviderChangedBroadcast() { return new Intent(Intent.ACTION_PROVIDER_CHANGED, CalendarContract.CONTENT_URI) - .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING) + .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); } private static final int TRANSACTION_QUERY = 0; @@ -4700,6 +4867,14 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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 +4925,21 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun 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>(); @@ -4784,7 +4974,7 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun sCalendarsProjectionMap.put(Calendars.CALENDAR_TIME_ZONE, Calendars.CALENDAR_TIME_ZONE); sCalendarsProjectionMap.put(Calendars.OWNER_ACCOUNT, Calendars.OWNER_ACCOUNT); sCalendarsProjectionMap.put(Calendars.IS_PRIMARY, - "COALESCE(" + Events.IS_PRIMARY + ", " + "COALESCE(" + Calendars.IS_PRIMARY + ", " + Calendars.OWNER_ACCOUNT + " = " + Calendars.ACCOUNT_NAME + ") AS " + Calendars.IS_PRIMARY); sCalendarsProjectionMap.put(Calendars.CAN_ORGANIZER_RESPOND, @@ -4870,6 +5060,10 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun sEventsProjectionMap.put(Calendars.MAX_REMINDERS, Calendars.MAX_REMINDERS); sEventsProjectionMap.put(Calendars.CAN_ORGANIZER_RESPOND, Calendars.CAN_ORGANIZER_RESPOND); sEventsProjectionMap.put(Calendars.CAN_MODIFY_TIME_ZONE, Calendars.CAN_MODIFY_TIME_ZONE); + sEventsProjectionMap.put(Calendars.IS_PRIMARY, + "COALESCE(" + Calendars.IS_PRIMARY + ", " + + Calendars.OWNER_ACCOUNT + " = " + Calendars.ACCOUNT_NAME + ") AS " + + Calendars.IS_PRIMARY); sEventsProjectionMap.put(Events.DISPLAY_COLOR, Events.DISPLAY_COLOR); // Put the shared items into the Instances projection map @@ -5144,7 +5338,8 @@ public class CalendarProvider2 extends SQLiteContentProvider implements OnAccoun } } - 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..57aa5b3 --- /dev/null +++ b/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelper.java @@ -0,0 +1,182 @@ +/* + * 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); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.LAST_DATE); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.SELF_ATTENDEE_STATUS); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DISPLAY_COLOR); + + CALENDARS_TABLE_WHITELIST = new ArraySet<>(); + CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars._ID); + 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_COLOR); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE); + EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY); + + ((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 package is allowed to access cross-profile calendar APIs. + * + * A package is allowed to access cross-profile calendar APIs if it's allowed by the + * profile owner of a managed profile to access the managed profile calendar provider, + * and the setting {@link Settings.Secure#CROSS_PROFILE_CALENDAR_ENABLED} is turned + * on in the managed profile. + * + * @param packageName the name of the package + * @param managedProfileUserId the user id of the managed profile + * @return {@code true} if the package is allowed, {@false} otherwise. + */ + public boolean isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId) { + final Context managedProfileUserContext = createPackageContextAsUser( + mContext, managedProfileUserId); + final DevicePolicyManager mDpm = managedProfileUserContext.getSystemService( + DevicePolicyManager.class); + return mDpm.isPackageAllowedToAccessCalendar(packageName); + } + + 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.app.PendingIntent; 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 @@ public class CalendarProvider2ForTesting extends CalendarProvider2 { 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..e0a4edf 100644 --- a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java +++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java @@ -31,6 +31,7 @@ import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.os.UserHandle; import android.provider.BaseColumns; import android.provider.CalendarContract; import android.provider.CalendarContract.Calendars; @@ -50,6 +51,8 @@ import android.text.format.DateUtils; 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 +94,12 @@ public class CalendarProvider2Test extends AndroidTestCase { 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 +124,36 @@ public class CalendarProvider2Test extends AndroidTestCase { 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 +1000,57 @@ public class CalendarProvider2Test extends AndroidTestCase { } }; - 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 int getParentUserId() { + return UserHandle.USER_NULL; + } + + @Override + protected void initCrossProfileCalendarHelper() { + mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); + } + }; ProviderInfo info = new ProviderInfo(); info.authority = CalendarContract.AUTHORITY; mProvider.attachInfoForTesting(mContext, info); + mWorkProfileProvider = new CalendarProvider2ForTesting() { + @Override + protected int getWorkProfileUserId() { + return UserHandle.USER_NULL; + } + + @Override + protected int getParentUserId() { + return UserHandle.myUserId(); + } + + @Override + protected void initCrossProfileCalendarHelper() { + mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); + } + }; + 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; @@ -1090,6 +1169,31 @@ public class CalendarProvider2Test extends AndroidTestCase { return Integer.parseInt(id); } + /** + * Creates a new calendar, with the provided name, time zone, and account name, but an empty + * owner account, which makes this calendar non-primary calendar. + * + * @return the new calendar's _ID value + */ + private int insertNonPrimaryCal(String name, String timezone, String account) { + ContentValues m = new ContentValues(); + m.put(Calendars.NAME, name); + m.put(Calendars.CALENDAR_DISPLAY_NAME, name); + m.put(Calendars.CALENDAR_COLOR, 0xff123456); + m.put(Calendars.CALENDAR_TIME_ZONE, timezone); + m.put(Calendars.VISIBLE, 1); + m.put(Calendars.CAL_SYNC1, CALENDAR_URL); + m.put(Calendars.OWNER_ACCOUNT, ""); + m.put(Calendars.ACCOUNT_NAME, account); + m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); + m.put(Calendars.SYNC_EVENTS, 1); + + Uri url = mResolver.insert( + addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m); + String id = url.getLastPathSegment(); + return Integer.parseInt(id); + } + private String obsToString(Object... objs) { StringBuilder bob = new StringBuilder(); @@ -3102,6 +3206,71 @@ public class CalendarProvider2Test extends AndroidTestCase { checkCalendarCount(0); } + public void testGetIsPrimary_ForEvents() { + checkCalendarCount(0); + int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); + + final String START = "2008-05-01T00:00:00"; + final String END = "2008-05-01T20:00:00"; + EventInfo event = new EventInfo("search orange", + START, + END, + false /* allDay */, + DEFAULT_TIMEZONE); + + insertEvent(calendarId0, event); + + String[] projection = new String[] { + Calendars.IS_PRIMARY + }; + String selection = + "((" + Calendars.IS_PRIMARY + " = ? OR " + Calendars.ACCOUNT_NAME + " = ?))"; + String[] selectionArgs = new String[] { + "1", DEFAULT_ACCOUNT + }; + Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, + null); + assertNotNull(cursor); + cursor.moveToLast(); + assertEquals(1, cursor.getCount()); + assertEquals(1, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); + cursor.close(); + deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); + checkCalendarCount(0); + } + + public void testGetIsNotPrimary_ForEvents() { + checkCalendarCount(0); + int calendarId0 = insertNonPrimaryCal("Calendar0", DEFAULT_TIMEZONE, DEFAULT_ACCOUNT); + + final String START = "2008-05-01T00:00:00"; + final String END = "2008-05-01T20:00:00"; + EventInfo event = new EventInfo("search orange", + START, + END, + false /* allDay */, + DEFAULT_TIMEZONE); + + insertEvent(calendarId0, event); + + String[] projection = new String[] { + Calendars.IS_PRIMARY + }; + String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; + String[] selectionArgs = new String[] { + DEFAULT_ACCOUNT + }; + Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, + null); + assertNotNull(cursor); + cursor.moveToLast(); + assertEquals(1, cursor.getCount()); + assertEquals(0, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); + cursor.close(); + deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); + checkCalendarCount(0); + } + public void testGetColumnIndex_Count() { checkCalendarCount(0); int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); @@ -3122,4 +3291,345 @@ public class CalendarProvider2Test extends AndroidTestCase { 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.setPackageAllowedToAccessCalendar(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_COLOR, + }; + 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_COLOR, cursor.getInt(3)); + cursor.close(); + + 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.setPackageAllowedToAccessCalendar(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()); + cursor.close(); + + 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.setPackageAllowedToAccessCalendar(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_COLOR + }; + 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_COLOR, cursor.getInt(4)); + cursor.close(); + + 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.setPackageAllowedToAccessCalendar(true); + + // Test ENTERPRISE_CONTENT_URI_ID. + String[] projection = new String[]{ + Events._ID, + Events.TITLE, + Events.CALENDAR_ID, + Events.DTSTART, + Calendars.CALENDAR_COLOR + }; + 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_COLOR, cursor.getInt(4)); + assertEquals(1, cursor.getInt(2)); + cursor.close(); + + 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.setPackageAllowedToAccessCalendar(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(calendarId, cursor.getLong( + cursor.getColumnIndex(Events.CALENDAR_ID))); + assertEquals(WORK_CALENDAR_COLOR, cursor.getInt( + cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); + assertEquals(1, cursor.getInt( + cursor.getColumnIndex(Calendars.IS_PRIMARY))); + cursor.close(); + + 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.setPackageAllowedToAccessCalendar(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_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_COLOR, cursor.getInt(1)); + + 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.setPackageAllowedToAccessCalendar(true); + + // Test Calendars.ENTERPRISE_CONTENT_URI with id. + String[] projection = new String[] { + Calendars._ID, + 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_COLOR, cursor.getInt(1)); + cursor.close(); + + cleanupEnterpriseTestForCalendars(2); + } + + public void testEnterpriseCalendarsProjectionCalibration() { + final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); + // Assume cross profile uri access is allowed by policy and settings. + MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(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_COLOR, cursor.getInt( + cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); + cursor.close(); + + 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 testEnterpriseCalendarsNotAllowed() { + insertWorkCalendar(WORK_CALENDAR_TITLE); + // Assume cross profile uri access is not allowed by policy or disabled in settings. + MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(false); + + // Throw exception if cross profile calendar is disabled in settings. + try { + final Cursor cursor = mResolver.query( + Calendars.ENTERPRISE_CONTENT_URI, + new String[]{}, null, null, null); + fail("Unsupported operation exception should have been raised."); + } catch (UnsupportedOperationException e) { + // Exception expected. + } + 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..5b7e4f4 --- /dev/null +++ b/tests/src/com/android/providers/calendar/MockCrossProfileCalendarHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.providers.calendar; + +import android.content.Context; + +import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper; + +public class MockCrossProfileCalendarHelper extends CrossProfileCalendarHelper { + + private static boolean mCallingPackageAllowed = true; + + public MockCrossProfileCalendarHelper (Context context) { + super(context); + } + + /** + * Mock this method in unit test since it depends on DevicePolicyManager and SettingsProvider. + * It will be tested in integration test. + */ + @Override + public boolean isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId) { + return mCallingPackageAllowed; + } + + public static void setPackageAllowedToAccessCalendar(boolean isCallingPackageAllowed) { + mCallingPackageAllowed = isCallingPackageAllowed; + } +} 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)); + } +} |