summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2019-07-01 21:00:09 +0000
committerXin Li <delphij@google.com>2019-07-01 21:00:09 +0000
commit2e1c5e9b0cf88594645171c42e737daf18f95366 (patch)
tree2250d0ca95b1f4d9737844a407824a5914a4296f
parent49003cfd280426f7da67ef05a0d2fff5f546ac59 (diff)
parentaae64670394e9c9fb29a3a2410a804e7226745c0 (diff)
downloadCalendarProvider-2e1c5e9b0cf88594645171c42e737daf18f95366.tar.gz
DO NOT MERGE - Merge qt-dev-plus-aosp-without-vendor (5699924) into stage-aosp-mastertemp_140451723
Bug: 134405016 Change-Id: I0d0d6bee866349030f2bc6b885d7ffd5e2901454
-rw-r--r--AndroidManifest.xml9
-rw-r--r--src/com/android/providers/calendar/CalendarAlarmManager.java6
-rw-r--r--src/com/android/providers/calendar/CalendarDatabaseHelper.java8
-rw-r--r--src/com/android/providers/calendar/CalendarProvider2.java207
-rw-r--r--src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelper.java182
-rw-r--r--tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java6
-rw-r--r--tests/src/com/android/providers/calendar/CalendarProvider2Test.java512
-rw-r--r--tests/src/com/android/providers/calendar/MockCrossProfileCalendarHelper.java43
-rw-r--r--tests/src/com/android/providers/calendar/enterprise/CrossProfileCalendarHelperTest.java95
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));
+ }
+}