1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.providers.calendar.enterprise; 18 19 import android.app.admin.DevicePolicyManager; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.net.Uri; 23 import android.os.UserHandle; 24 import android.provider.CalendarContract; 25 import android.provider.Settings; 26 import android.util.ArraySet; 27 import android.util.Log; 28 29 import java.util.Set; 30 31 /** 32 * Helper class for cross profile calendar related policies. 33 */ 34 public class CrossProfileCalendarHelper { 35 36 private static final String LOG_TAG = "CrossProfileCalendarHelper"; 37 38 final private Context mContext; 39 40 public static final Set<String> EVENTS_TABLE_WHITELIST; 41 public static final Set<String> CALENDARS_TABLE_WHITELIST; 42 public static final Set<String> INSTANCES_TABLE_WHITELIST; 43 44 static { 45 EVENTS_TABLE_WHITELIST = new ArraySet<>(); 46 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events._ID); 47 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.CALENDAR_ID); 48 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.TITLE); 49 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_LOCATION); 50 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_COLOR); 51 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.STATUS); 52 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTSTART); 53 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DTEND); 54 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_TIMEZONE); 55 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EVENT_END_TIMEZONE); 56 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DURATION); 57 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.ALL_DAY); 58 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.AVAILABILITY); 59 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RRULE); 60 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.RDATE); 61 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXRULE); 62 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.EXDATE); 63 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.LAST_DATE); 64 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.SELF_ATTENDEE_STATUS); 65 EVENTS_TABLE_WHITELIST.add(CalendarContract.Events.DISPLAY_COLOR); 66 67 CALENDARS_TABLE_WHITELIST = new ArraySet<>(); 68 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars._ID); 69 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR); 70 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE); 71 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_LOCATION); 72 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE); 73 CALENDARS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY); 74 75 INSTANCES_TABLE_WHITELIST = new ArraySet<>(); 76 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances._ID); 77 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.EVENT_ID); 78 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.BEGIN); 79 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END); 80 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_DAY); 81 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_DAY); 82 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.START_MINUTE); 83 INSTANCES_TABLE_WHITELIST.add(CalendarContract.Instances.END_MINUTE); 84 85 // Add calendar columns. 86 EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_COLOR); 87 EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.VISIBLE); 88 EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.CALENDAR_TIME_ZONE); 89 EVENTS_TABLE_WHITELIST.add(CalendarContract.Calendars.IS_PRIMARY); 90 addAll(EVENTS_TABLE_WHITELIST)91 ((ArraySet<String>) INSTANCES_TABLE_WHITELIST).addAll(EVENTS_TABLE_WHITELIST); 92 } 93 CrossProfileCalendarHelper(Context context)94 public CrossProfileCalendarHelper(Context context) { 95 mContext = context; 96 } 97 98 /** 99 * @return a context created from the given context for the given user, or null if it fails. 100 */ createPackageContextAsUser(Context context, int userId)101 private Context createPackageContextAsUser(Context context, int userId) { 102 try { 103 return context.createPackageContextAsUser( 104 context.getPackageName(), 0 /* flags */, UserHandle.of(userId)); 105 } catch (PackageManager.NameNotFoundException e) { 106 Log.e(LOG_TAG, "Failed to create user context", e); 107 } 108 return null; 109 } 110 111 /** 112 * Returns whether a package is allowed to access cross-profile calendar APIs. 113 * 114 * A package is allowed to access cross-profile calendar APIs if it's allowed by the 115 * profile owner of a managed profile to access the managed profile calendar provider, 116 * and the setting {@link Settings.Secure#CROSS_PROFILE_CALENDAR_ENABLED} is turned 117 * on in the managed profile. 118 * 119 * @param packageName the name of the package 120 * @param managedProfileUserId the user id of the managed profile 121 * @return {@code true} if the package is allowed, {@false} otherwise. 122 */ isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId)123 public boolean isPackageAllowedToAccessCalendar(String packageName, int managedProfileUserId) { 124 final Context managedProfileUserContext = createPackageContextAsUser( 125 mContext, managedProfileUserId); 126 final DevicePolicyManager mDpm = managedProfileUserContext.getSystemService( 127 DevicePolicyManager.class); 128 return mDpm.isPackageAllowedToAccessCalendar(packageName); 129 } 130 ensureProjectionAllowed(String[] projection, Set<String> validColumnsSet)131 private static void ensureProjectionAllowed(String[] projection, Set<String> validColumnsSet) { 132 for (String column : projection) { 133 if (!validColumnsSet.contains(column)) { 134 throw new IllegalArgumentException(String.format("Column %s is not " 135 + "allowed to be accessed from cross profile Uris", column)); 136 } 137 } 138 } 139 140 /** 141 * Returns the calibrated version of projection for a given table. 142 * 143 * If the input projection is empty, return an array of all the whitelisted columns for a 144 * given table. Table is determined by the input uri. 145 * 146 * @param projection the original projection 147 * @param localUri the local uri for the query of the projection 148 * @return the original value of the input projection if it's not empty, otherwise an array of 149 * all the whitelisted columns. 150 * @throws IllegalArgumentException if the input projection contains a column that is not 151 * whitelisted for a given table. 152 */ getCalibratedProjection(String[] projection, Uri localUri)153 public String[] getCalibratedProjection(String[] projection, Uri localUri) { 154 // If projection is not empty, check if it's valid. Otherwise fill it with all 155 // allowed columns. 156 Set<String> validColumnsSet = new ArraySet<String>(); 157 if (CalendarContract.Events.CONTENT_URI.equals(localUri)) { 158 validColumnsSet = EVENTS_TABLE_WHITELIST; 159 } else if (CalendarContract.Calendars.CONTENT_URI.equals(localUri)) { 160 validColumnsSet = CALENDARS_TABLE_WHITELIST; 161 } else if (CalendarContract.Instances.CONTENT_URI.equals(localUri) 162 || CalendarContract.Instances.CONTENT_BY_DAY_URI.equals(localUri) 163 || CalendarContract.Instances.CONTENT_SEARCH_URI.equals(localUri) 164 || CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI.equals(localUri)) { 165 validColumnsSet = INSTANCES_TABLE_WHITELIST; 166 } else { 167 throw new IllegalArgumentException(String.format("Cross profile version of %d is not " 168 + "supported", localUri.toSafeString())); 169 } 170 171 if (projection != null && projection.length > 0) { 172 // If there exists some columns in original projection, check if these columns are 173 // allowed. 174 ensureProjectionAllowed(projection, validColumnsSet); 175 return projection; 176 } 177 // Query of content provider will return cursor that contains all columns if projection is 178 // null or empty. To be consistent with this behavior, we fill projection with all allowed 179 // columns if it's null or empty for cross profile Uris. 180 return validColumnsSet.toArray(new String[validColumnsSet.size()]); 181 } 182 } 183