1 /* 2 * Copyright (C) 2009 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.contacts; 18 19 import com.google.android.collect.Sets; 20 21 import android.accounts.Account; 22 import android.accounts.AccountManager; 23 import android.accounts.AccountManagerCallback; 24 import android.accounts.AccountManagerFuture; 25 import android.accounts.AuthenticatorException; 26 import android.accounts.OnAccountsUpdateListener; 27 import android.accounts.OperationCanceledException; 28 import android.content.ContentProvider; 29 import android.content.ContentResolver; 30 import android.content.ContentUris; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.content.pm.ApplicationInfo; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ProviderInfo; 36 import android.content.res.Configuration; 37 import android.content.res.Resources; 38 import android.database.Cursor; 39 import android.location.Country; 40 import android.location.CountryDetector; 41 import android.location.CountryListener; 42 import android.net.Uri; 43 import android.os.Handler; 44 import android.os.Looper; 45 import android.provider.BaseColumns; 46 import android.provider.ContactsContract; 47 import android.provider.ContactsContract.AggregationExceptions; 48 import android.provider.ContactsContract.CommonDataKinds; 49 import android.provider.ContactsContract.CommonDataKinds.Email; 50 import android.provider.ContactsContract.CommonDataKinds.Phone; 51 import android.provider.ContactsContract.Contacts; 52 import android.provider.ContactsContract.Data; 53 import android.provider.ContactsContract.RawContacts; 54 import android.provider.ContactsContract.StatusUpdates; 55 import android.test.IsolatedContext; 56 import android.test.RenamingDelegatingContext; 57 import android.test.mock.MockContentResolver; 58 import android.test.mock.MockContext; 59 60 import java.io.File; 61 import java.io.IOException; 62 import java.util.Arrays; 63 import java.util.Locale; 64 import java.util.Set; 65 66 /** 67 * Helper class that encapsulates an "actor" which is owned by a specific 68 * package name. It correctly maintains a wrapped {@link Context} and an 69 * attached {@link MockContentResolver}. Multiple actors can be used to test 70 * security scenarios between multiple packages. 71 */ 72 public class ContactsActor { 73 private static final String FILENAME_PREFIX = "test."; 74 75 public static final String PACKAGE_GREY = "edu.example.grey"; 76 public static final String PACKAGE_RED = "net.example.red"; 77 public static final String PACKAGE_GREEN = "com.example.green"; 78 public static final String PACKAGE_BLUE = "org.example.blue"; 79 80 public Context context; 81 public String packageName; 82 public MockContentResolver resolver; 83 public ContentProvider provider; 84 private Country mMockCountry = new Country("us", 0); 85 86 private Account[] mAccounts = new Account[0]; 87 88 private Set<String> mGrantedPermissions = Sets.newHashSet(); 89 private final Set<Uri> mGrantedUriPermissions = Sets.newHashSet(); 90 91 private CountryDetector mMockCountryDetector = new CountryDetector(null){ 92 @Override 93 public Country detectCountry() { 94 return mMockCountry; 95 } 96 97 @Override 98 public void addCountryListener(CountryListener listener, Looper looper) { 99 } 100 }; 101 102 private AccountManager mMockAccountManager; 103 104 private class MockAccountManager extends AccountManager { 105 public MockAccountManager(Context conteact) { 106 super(context, null, null); 107 } 108 109 @Override 110 public void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener, 111 Handler handler, boolean updateImmediately) { 112 // do nothing 113 } 114 115 @Override 116 public Account[] getAccounts() { 117 return mAccounts; 118 } 119 120 @Override 121 public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( 122 final String type, final String[] features, 123 AccountManagerCallback<Account[]> callback, Handler handler) { 124 return null; 125 } 126 127 @Override 128 public String blockingGetAuthToken(Account account, String authTokenType, 129 boolean notifyAuthFailure) 130 throws OperationCanceledException, IOException, AuthenticatorException { 131 return null; 132 } 133 } 134 135 private IsolatedContext mProviderContext; 136 137 /** 138 * Create an "actor" using the given parent {@link Context} and the specific 139 * package name. Internally, all {@link Context} method calls are passed to 140 * a new instance of {@link RestrictionMockContext}, which stubs out the 141 * security infrastructure. 142 */ 143 public ContactsActor(Context overallContext, String packageName, 144 Class<? extends ContentProvider> providerClass, String authority) throws Exception { 145 resolver = new MockContentResolver(); 146 context = new RestrictionMockContext(overallContext, packageName, resolver, 147 mGrantedPermissions, mGrantedUriPermissions); 148 this.packageName = packageName; 149 150 // Let the Secure class initialize the settings provider, which is done when we first 151 // tries to get any setting. Because our mock context/content resolver doesn't have the 152 // settings provider, we need to do this with an actual context, before other classes 153 // try to do this with a mock context. 154 // (Otherwise ContactsProvider2.initialzie() will crash trying to get a setting with 155 // a mock context.) 156 android.provider.Settings.Secure.getString(overallContext.getContentResolver(), "dummy"); 157 158 RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context, 159 overallContext, FILENAME_PREFIX); 160 mProviderContext = new IsolatedContext(resolver, targetContextWrapper){ 161 162 @Override 163 public File getFilesDir() { 164 // TODO: Need to figure out something more graceful than this. 165 return new File("/data/data/com.android.providers.contacts.tests/files"); 166 } 167 168 @Override 169 public Object getSystemService(String name) { 170 if (Context.COUNTRY_DETECTOR.equals(name)) { 171 return mMockCountryDetector; 172 } 173 if (Context.ACCOUNT_SERVICE.equals(name)) { 174 return mMockAccountManager; 175 } 176 return super.getSystemService(name); 177 } 178 }; 179 180 mMockAccountManager = new MockAccountManager(mProviderContext); 181 provider = addProvider(providerClass, authority); 182 } 183 184 public void addAuthority(String authority) { 185 resolver.addProvider(authority, provider); 186 } 187 188 public ContentProvider addProvider(Class<? extends ContentProvider> providerClass, 189 String authority) throws Exception { 190 ContentProvider provider = providerClass.newInstance(); 191 ProviderInfo info = new ProviderInfo(); 192 info.authority = authority; 193 provider.attachInfo(mProviderContext, info); 194 resolver.addProvider(authority, provider); 195 return provider; 196 } 197 198 public void addPermissions(String... permissions) { 199 mGrantedPermissions.addAll(Arrays.asList(permissions)); 200 } 201 202 public void removePermissions(String... permissions) { 203 mGrantedPermissions.removeAll(Arrays.asList(permissions)); 204 } 205 206 public void addUriPermissions(Uri... uris) { 207 mGrantedUriPermissions.addAll(Arrays.asList(uris)); 208 } 209 210 public void removeUriPermissions(Uri... uris) { 211 mGrantedUriPermissions.removeAll(Arrays.asList(uris)); 212 } 213 214 /** 215 * Mock {@link Context} that reports specific well-known values for testing 216 * data protection. The creator can override the owner package name, and 217 * force the {@link PackageManager} to always return a well-known package 218 * list for any call to {@link PackageManager#getPackagesForUid(int)}. 219 * <p> 220 * For example, the creator could request that the {@link Context} lives in 221 * package name "com.example.red", and also cause the {@link PackageManager} 222 * to report that no UID contains that package name. 223 */ 224 private static class RestrictionMockContext extends MockContext { 225 private final Context mOverallContext; 226 private final String mReportedPackageName; 227 private final ContactsMockPackageManager mPackageManager; 228 private final ContentResolver mResolver; 229 private final Resources mRes; 230 private final Set<String> mGrantedPermissions; 231 private final Set<Uri> mGrantedUriPermissions; 232 233 /** 234 * Create a {@link Context} under the given package name. 235 */ 236 public RestrictionMockContext(Context overallContext, String reportedPackageName, 237 ContentResolver resolver, Set<String> grantedPermissions, 238 Set<Uri> grantedUriPermissions) { 239 mOverallContext = overallContext; 240 mReportedPackageName = reportedPackageName; 241 mResolver = resolver; 242 mGrantedPermissions = grantedPermissions; 243 mGrantedUriPermissions = grantedUriPermissions; 244 245 mPackageManager = new ContactsMockPackageManager(); 246 mPackageManager.addPackage(1000, PACKAGE_GREY); 247 mPackageManager.addPackage(2000, PACKAGE_RED); 248 mPackageManager.addPackage(3000, PACKAGE_GREEN); 249 mPackageManager.addPackage(4000, PACKAGE_BLUE); 250 251 Resources resources = overallContext.getResources(); 252 Configuration configuration = new Configuration(resources.getConfiguration()); 253 configuration.locale = Locale.US; 254 resources.updateConfiguration(configuration, resources.getDisplayMetrics()); 255 mRes = resources; 256 } 257 258 @Override 259 public String getPackageName() { 260 return mReportedPackageName; 261 } 262 263 @Override 264 public PackageManager getPackageManager() { 265 return mPackageManager; 266 } 267 268 @Override 269 public Resources getResources() { 270 return mRes; 271 } 272 273 @Override 274 public ContentResolver getContentResolver() { 275 return mResolver; 276 } 277 278 @Override 279 public ApplicationInfo getApplicationInfo() { 280 ApplicationInfo ai = new ApplicationInfo(); 281 ai.packageName = "contactsTestPackage"; 282 return ai; 283 } 284 285 // All permission checks are implemented to simply check against the granted permission set. 286 287 @Override 288 public int checkPermission(String permission, int pid, int uid) { 289 return checkCallingPermission(permission); 290 } 291 292 @Override 293 public int checkCallingPermission(String permission) { 294 if (mGrantedPermissions.contains(permission)) { 295 return PackageManager.PERMISSION_GRANTED; 296 } else { 297 return PackageManager.PERMISSION_DENIED; 298 } 299 } 300 301 @Override 302 public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { 303 return checkCallingUriPermission(uri, modeFlags); 304 } 305 306 @Override 307 public int checkCallingUriPermission(Uri uri, int modeFlags) { 308 if (mGrantedUriPermissions.contains(uri)) { 309 return PackageManager.PERMISSION_GRANTED; 310 } else { 311 return PackageManager.PERMISSION_DENIED; 312 } 313 } 314 315 @Override 316 public int checkCallingOrSelfPermission(String permission) { 317 return checkCallingPermission(permission); 318 } 319 320 @Override 321 public void enforcePermission(String permission, int pid, int uid, String message) { 322 enforceCallingPermission(permission, message); 323 } 324 325 @Override 326 public void enforceCallingPermission(String permission, String message) { 327 if (!mGrantedPermissions.contains(permission)) { 328 throw new SecurityException(message); 329 } 330 } 331 332 @Override 333 public void enforceCallingOrSelfPermission(String permission, String message) { 334 enforceCallingPermission(permission, message); 335 } 336 } 337 338 static String sCallingPackage = null; 339 340 void ensureCallingPackage() { 341 sCallingPackage = this.packageName; 342 } 343 344 public long createRawContact(String name) { 345 ensureCallingPackage(); 346 long rawContactId = createRawContact(); 347 createName(rawContactId, name); 348 return rawContactId; 349 } 350 351 public long createRawContact() { 352 ensureCallingPackage(); 353 final ContentValues values = new ContentValues(); 354 355 Uri rawContactUri = resolver.insert(RawContacts.CONTENT_URI, values); 356 return ContentUris.parseId(rawContactUri); 357 } 358 359 public long createRawContactWithStatus(String name, String address, 360 String status) { 361 final long rawContactId = createRawContact(name); 362 final long dataId = createEmail(rawContactId, address); 363 createStatus(dataId, status); 364 return rawContactId; 365 } 366 367 public long createName(long contactId, String name) { 368 ensureCallingPackage(); 369 final ContentValues values = new ContentValues(); 370 values.put(Data.RAW_CONTACT_ID, contactId); 371 values.put(Data.IS_PRIMARY, 1); 372 values.put(Data.IS_SUPER_PRIMARY, 1); 373 values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); 374 values.put(CommonDataKinds.StructuredName.FAMILY_NAME, name); 375 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 376 contactId), RawContacts.Data.CONTENT_DIRECTORY); 377 Uri dataUri = resolver.insert(insertUri, values); 378 return ContentUris.parseId(dataUri); 379 } 380 381 public long createPhone(long contactId, String phoneNumber) { 382 ensureCallingPackage(); 383 final ContentValues values = new ContentValues(); 384 values.put(Data.RAW_CONTACT_ID, contactId); 385 values.put(Data.IS_PRIMARY, 1); 386 values.put(Data.IS_SUPER_PRIMARY, 1); 387 values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 388 values.put(ContactsContract.CommonDataKinds.Phone.TYPE, 389 ContactsContract.CommonDataKinds.Phone.TYPE_HOME); 390 values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber); 391 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 392 contactId), RawContacts.Data.CONTENT_DIRECTORY); 393 Uri dataUri = resolver.insert(insertUri, values); 394 return ContentUris.parseId(dataUri); 395 } 396 397 public long createEmail(long contactId, String address) { 398 ensureCallingPackage(); 399 final ContentValues values = new ContentValues(); 400 values.put(Data.RAW_CONTACT_ID, contactId); 401 values.put(Data.IS_PRIMARY, 1); 402 values.put(Data.IS_SUPER_PRIMARY, 1); 403 values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 404 values.put(Email.TYPE, Email.TYPE_HOME); 405 values.put(Email.DATA, address); 406 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 407 contactId), RawContacts.Data.CONTENT_DIRECTORY); 408 Uri dataUri = resolver.insert(insertUri, values); 409 return ContentUris.parseId(dataUri); 410 } 411 412 public long createStatus(long dataId, String status) { 413 ensureCallingPackage(); 414 final ContentValues values = new ContentValues(); 415 values.put(StatusUpdates.DATA_ID, dataId); 416 values.put(StatusUpdates.STATUS, status); 417 Uri dataUri = resolver.insert(StatusUpdates.CONTENT_URI, values); 418 return ContentUris.parseId(dataUri); 419 } 420 421 public void updateException(String packageProvider, String packageClient, boolean allowAccess) { 422 throw new UnsupportedOperationException("RestrictionExceptions are hard-coded"); 423 } 424 425 public long getContactForRawContact(long rawContactId) { 426 ensureCallingPackage(); 427 Uri contactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 428 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_RAW_CONTACTS, null, 429 null, null); 430 if (!cursor.moveToFirst()) { 431 cursor.close(); 432 throw new RuntimeException("Contact didn't have an aggregate"); 433 } 434 final long aggId = cursor.getLong(Projections.COL_CONTACTS_ID); 435 cursor.close(); 436 return aggId; 437 } 438 439 public int getDataCountForContact(long contactId) { 440 ensureCallingPackage(); 441 Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, 442 contactId), Contacts.Data.CONTENT_DIRECTORY); 443 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null, 444 null); 445 final int count = cursor.getCount(); 446 cursor.close(); 447 return count; 448 } 449 450 public int getDataCountForRawContact(long rawContactId) { 451 ensureCallingPackage(); 452 Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 453 rawContactId), Contacts.Data.CONTENT_DIRECTORY); 454 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null, 455 null); 456 final int count = cursor.getCount(); 457 cursor.close(); 458 return count; 459 } 460 461 public void setSuperPrimaryPhone(long dataId) { 462 ensureCallingPackage(); 463 final ContentValues values = new ContentValues(); 464 values.put(Data.IS_PRIMARY, 1); 465 values.put(Data.IS_SUPER_PRIMARY, 1); 466 Uri updateUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId); 467 resolver.update(updateUri, values, null, null); 468 } 469 470 public long createGroup(String groupName) { 471 ensureCallingPackage(); 472 final ContentValues values = new ContentValues(); 473 values.put(ContactsContract.Groups.RES_PACKAGE, packageName); 474 values.put(ContactsContract.Groups.TITLE, groupName); 475 Uri groupUri = resolver.insert(ContactsContract.Groups.CONTENT_URI, values); 476 return ContentUris.parseId(groupUri); 477 } 478 479 public long createGroupMembership(long rawContactId, long groupId) { 480 ensureCallingPackage(); 481 final ContentValues values = new ContentValues(); 482 values.put(Data.RAW_CONTACT_ID, rawContactId); 483 values.put(Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE); 484 values.put(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId); 485 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 486 rawContactId), RawContacts.Data.CONTENT_DIRECTORY); 487 Uri dataUri = resolver.insert(insertUri, values); 488 return ContentUris.parseId(dataUri); 489 } 490 491 protected void setAggregationException(int type, long rawContactId1, long rawContactId2) { 492 ContentValues values = new ContentValues(); 493 values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 494 values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 495 values.put(AggregationExceptions.TYPE, type); 496 resolver.update(AggregationExceptions.CONTENT_URI, values, null, null); 497 } 498 499 public void setAccounts(Account[] accounts) { 500 mAccounts = accounts; 501 } 502 503 /** 504 * Various internal database projections. 505 */ 506 private interface Projections { 507 static final String[] PROJ_ID = new String[] { 508 BaseColumns._ID, 509 }; 510 511 static final int COL_ID = 0; 512 513 static final String[] PROJ_RAW_CONTACTS = new String[] { 514 RawContacts.CONTACT_ID 515 }; 516 517 static final int COL_CONTACTS_ID = 0; 518 } 519 } 520