/* * 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 EVENTS_TABLE_ALLOWED_LIST; public static final Set CALENDARS_TABLE_ALLOWED_LIST; public static final Set INSTANCES_TABLE_ALLOWED_LIST; static { EVENTS_TABLE_ALLOWED_LIST = new ArraySet<>(); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events._ID); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.CALENDAR_ID); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.TITLE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EVENT_LOCATION); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EVENT_COLOR); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.STATUS); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.DTSTART); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.DTEND); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EVENT_TIMEZONE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EVENT_END_TIMEZONE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.DURATION); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.ALL_DAY); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.AVAILABILITY); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.RRULE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.RDATE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EXRULE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.EXDATE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.LAST_DATE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.SELF_ATTENDEE_STATUS); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Events.DISPLAY_COLOR); CALENDARS_TABLE_ALLOWED_LIST = new ArraySet<>(); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars._ID); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.CALENDAR_COLOR); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.VISIBLE); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.CALENDAR_LOCATION); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE); CALENDARS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.IS_PRIMARY); INSTANCES_TABLE_ALLOWED_LIST = new ArraySet<>(); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances._ID); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.EVENT_ID); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.BEGIN); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.END); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.START_DAY); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.END_DAY); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.START_MINUTE); INSTANCES_TABLE_ALLOWED_LIST.add(CalendarContract.Instances.END_MINUTE); // Add calendar columns. EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.CALENDAR_COLOR); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.VISIBLE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE); EVENTS_TABLE_ALLOWED_LIST.add(CalendarContract.Calendars.IS_PRIMARY); ((ArraySet) INSTANCES_TABLE_ALLOWED_LIST).addAll(EVENTS_TABLE_ALLOWED_LIST); } 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 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 allowed list of 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 allowed list of columns. * @throws IllegalArgumentException if the input projection contains a column that is not * in the allowed list 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 validColumnsSet = new ArraySet(); if (CalendarContract.Events.CONTENT_URI.equals(localUri)) { validColumnsSet = EVENTS_TABLE_ALLOWED_LIST; } else if (CalendarContract.Calendars.CONTENT_URI.equals(localUri)) { validColumnsSet = CALENDARS_TABLE_ALLOWED_LIST; } 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_ALLOWED_LIST; } else { throw new IllegalArgumentException(String.format("Cross profile version of %s 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()]); } }