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 android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Binder; 31 import android.provider.BaseColumns; 32 import android.provider.ContactsContract; 33 import android.provider.ContactsContract.AggregationExceptions; 34 import android.provider.ContactsContract.CommonDataKinds; 35 import android.provider.ContactsContract.Contacts; 36 import android.provider.ContactsContract.Data; 37 import android.provider.ContactsContract.RawContacts; 38 import android.provider.ContactsContract.StatusUpdates; 39 import android.provider.ContactsContract.CommonDataKinds.Email; 40 import android.provider.ContactsContract.CommonDataKinds.Phone; 41 import android.test.IsolatedContext; 42 import android.test.RenamingDelegatingContext; 43 import android.test.mock.MockContentResolver; 44 import android.test.mock.MockContext; 45 import android.test.mock.MockPackageManager; 46 import android.test.mock.MockResources; 47 import android.util.TypedValue; 48 49 import java.util.HashMap; 50 import java.util.Locale; 51 52 /** 53 * Helper class that encapsulates an "actor" which is owned by a specific 54 * package name. It correctly maintains a wrapped {@link Context} and an 55 * attached {@link MockContentResolver}. Multiple actors can be used to test 56 * security scenarios between multiple packages. 57 */ 58 public class ContactsActor { 59 private static final String FILENAME_PREFIX = "test."; 60 61 public static final String PACKAGE_GREY = "edu.example.grey"; 62 public static final String PACKAGE_RED = "net.example.red"; 63 public static final String PACKAGE_GREEN = "com.example.green"; 64 public static final String PACKAGE_BLUE = "org.example.blue"; 65 66 public Context context; 67 public String packageName; 68 public MockContentResolver resolver; 69 public ContentProvider provider; 70 71 private IsolatedContext mProviderContext; 72 73 /** 74 * Create an "actor" using the given parent {@link Context} and the specific 75 * package name. Internally, all {@link Context} method calls are passed to 76 * a new instance of {@link RestrictionMockContext}, which stubs out the 77 * security infrastructure. 78 */ 79 public ContactsActor(Context overallContext, String packageName, 80 Class<? extends ContentProvider> providerClass, String authority) throws Exception { 81 resolver = new MockContentResolver(); 82 context = new RestrictionMockContext(overallContext, packageName, resolver); 83 this.packageName = packageName; 84 85 RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context, 86 overallContext, FILENAME_PREFIX); 87 mProviderContext = new IsolatedContext(resolver, targetContextWrapper); 88 provider = addProvider(providerClass, authority); 89 } 90 91 public void addAuthority(String authority) { 92 resolver.addProvider(authority, provider); 93 } 94 95 public ContentProvider addProvider(Class<? extends ContentProvider> providerClass, 96 String authority) throws Exception { 97 ContentProvider provider = providerClass.newInstance(); 98 provider.attachInfo(mProviderContext, null); 99 resolver.addProvider(authority, provider); 100 return provider; 101 } 102 103 /** 104 * Mock {@link Context} that reports specific well-known values for testing 105 * data protection. The creator can override the owner package name, and 106 * force the {@link PackageManager} to always return a well-known package 107 * list for any call to {@link PackageManager#getPackagesForUid(int)}. 108 * <p> 109 * For example, the creator could request that the {@link Context} lives in 110 * package name "com.example.red", and also cause the {@link PackageManager} 111 * to report that no UID contains that package name. 112 */ 113 private static class RestrictionMockContext extends MockContext { 114 private final Context mOverallContext; 115 private final String mReportedPackageName; 116 private final RestrictionMockPackageManager mPackageManager; 117 private final ContentResolver mResolver; 118 private final Resources mRes; 119 120 /** 121 * Create a {@link Context} under the given package name. 122 */ 123 public RestrictionMockContext(Context overallContext, String reportedPackageName, 124 ContentResolver resolver) { 125 mOverallContext = overallContext; 126 mReportedPackageName = reportedPackageName; 127 mResolver = resolver; 128 129 mPackageManager = new RestrictionMockPackageManager(); 130 mPackageManager.addPackage(1000, PACKAGE_GREY); 131 mPackageManager.addPackage(2000, PACKAGE_RED); 132 mPackageManager.addPackage(3000, PACKAGE_GREEN); 133 mPackageManager.addPackage(4000, PACKAGE_BLUE); 134 135 Resources resources = overallContext.getResources(); 136 Configuration configuration = new Configuration(resources.getConfiguration()); 137 configuration.locale = Locale.US; 138 resources.updateConfiguration(configuration, resources.getDisplayMetrics()); 139 mRes = new RestrictionMockResources(resources); 140 } 141 142 @Override 143 public String getPackageName() { 144 return mReportedPackageName; 145 } 146 147 @Override 148 public PackageManager getPackageManager() { 149 return mPackageManager; 150 } 151 152 @Override 153 public Resources getResources() { 154 return mRes; 155 } 156 157 @Override 158 public ContentResolver getContentResolver() { 159 return mResolver; 160 } 161 } 162 163 private static class RestrictionMockResources extends MockResources { 164 private static final String UNRESTRICTED = "unrestricted_packages"; 165 private static final int UNRESTRICTED_ID = 1024; 166 167 private static final String[] UNRESTRICTED_LIST = new String[] { 168 PACKAGE_GREY 169 }; 170 171 private final Resources mRes; 172 173 public RestrictionMockResources(Resources res) { 174 mRes = res; 175 } 176 177 @Override 178 public int getIdentifier(String name, String defType, String defPackage) { 179 if (UNRESTRICTED.equals(name)) { 180 return UNRESTRICTED_ID; 181 } else { 182 return mRes.getIdentifier(name, defType, defPackage); 183 } 184 } 185 186 @Override 187 public String[] getStringArray(int id) throws NotFoundException { 188 if (id == UNRESTRICTED_ID) { 189 return UNRESTRICTED_LIST; 190 } else { 191 return mRes.getStringArray(id); 192 } 193 } 194 195 @Override 196 public void getValue(int id, TypedValue outValue, boolean resolveRefs) 197 throws NotFoundException { 198 mRes.getValue(id, outValue, resolveRefs); 199 } 200 201 @Override 202 public String getString(int id) throws NotFoundException { 203 return mRes.getString(id); 204 } 205 206 @Override 207 public String getString(int id, Object... formatArgs) throws NotFoundException { 208 return mRes.getString(id, formatArgs); 209 } 210 211 @Override 212 public CharSequence getText(int id) throws NotFoundException { 213 return mRes.getText(id); 214 } 215 } 216 217 private static String sCallingPackage = null; 218 219 void ensureCallingPackage() { 220 sCallingPackage = this.packageName; 221 } 222 223 /** 224 * Mock {@link PackageManager} that knows about a specific set of packages 225 * to help test security models. Because {@link Binder#getCallingUid()} 226 * can't be mocked, you'll have to find your mock-UID manually using your 227 * {@link Context#getPackageName()}. 228 */ 229 private static class RestrictionMockPackageManager extends MockPackageManager { 230 private final HashMap<Integer, String> mForward = new HashMap<Integer, String>(); 231 private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>(); 232 233 public RestrictionMockPackageManager() { 234 } 235 236 /** 237 * Add a UID-to-package mapping, which is then stored internally. 238 */ 239 public void addPackage(int packageUid, String packageName) { 240 mForward.put(packageUid, packageName); 241 mReverse.put(packageName, packageUid); 242 } 243 244 @Override 245 public String getNameForUid(int uid) { 246 return "name-for-uid"; 247 } 248 249 @Override 250 public String[] getPackagesForUid(int uid) { 251 return new String[] { sCallingPackage }; 252 } 253 254 @Override 255 public ApplicationInfo getApplicationInfo(String packageName, int flags) { 256 ApplicationInfo info = new ApplicationInfo(); 257 Integer uid = mReverse.get(packageName); 258 info.uid = (uid != null) ? uid : -1; 259 return info; 260 } 261 } 262 263 public long createRawContact(boolean isRestricted, String name) { 264 ensureCallingPackage(); 265 long rawContactId = createRawContact(isRestricted); 266 createName(rawContactId, name); 267 return rawContactId; 268 } 269 270 public long createRawContact(boolean isRestricted) { 271 ensureCallingPackage(); 272 final ContentValues values = new ContentValues(); 273 if (isRestricted) { 274 values.put(RawContacts.IS_RESTRICTED, 1); 275 } 276 277 Uri rawContactUri = resolver.insert(RawContacts.CONTENT_URI, values); 278 return ContentUris.parseId(rawContactUri); 279 } 280 281 public long createRawContactWithStatus(boolean isRestricted, String name, String address, 282 String status) { 283 final long rawContactId = createRawContact(isRestricted, name); 284 final long dataId = createEmail(rawContactId, address); 285 createStatus(dataId, status); 286 return rawContactId; 287 } 288 289 public long createName(long contactId, String name) { 290 ensureCallingPackage(); 291 final ContentValues values = new ContentValues(); 292 values.put(Data.RAW_CONTACT_ID, contactId); 293 values.put(Data.IS_PRIMARY, 1); 294 values.put(Data.IS_SUPER_PRIMARY, 1); 295 values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); 296 values.put(CommonDataKinds.StructuredName.FAMILY_NAME, name); 297 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 298 contactId), RawContacts.Data.CONTENT_DIRECTORY); 299 Uri dataUri = resolver.insert(insertUri, values); 300 return ContentUris.parseId(dataUri); 301 } 302 303 public long createPhone(long contactId, String phoneNumber) { 304 ensureCallingPackage(); 305 final ContentValues values = new ContentValues(); 306 values.put(Data.RAW_CONTACT_ID, contactId); 307 values.put(Data.IS_PRIMARY, 1); 308 values.put(Data.IS_SUPER_PRIMARY, 1); 309 values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 310 values.put(ContactsContract.CommonDataKinds.Phone.TYPE, 311 ContactsContract.CommonDataKinds.Phone.TYPE_HOME); 312 values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber); 313 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 314 contactId), RawContacts.Data.CONTENT_DIRECTORY); 315 Uri dataUri = resolver.insert(insertUri, values); 316 return ContentUris.parseId(dataUri); 317 } 318 319 public long createEmail(long contactId, String address) { 320 ensureCallingPackage(); 321 final ContentValues values = new ContentValues(); 322 values.put(Data.RAW_CONTACT_ID, contactId); 323 values.put(Data.IS_PRIMARY, 1); 324 values.put(Data.IS_SUPER_PRIMARY, 1); 325 values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 326 values.put(Email.TYPE, Email.TYPE_HOME); 327 values.put(Email.DATA, address); 328 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 329 contactId), RawContacts.Data.CONTENT_DIRECTORY); 330 Uri dataUri = resolver.insert(insertUri, values); 331 return ContentUris.parseId(dataUri); 332 } 333 334 public long createStatus(long dataId, String status) { 335 ensureCallingPackage(); 336 final ContentValues values = new ContentValues(); 337 values.put(StatusUpdates.DATA_ID, dataId); 338 values.put(StatusUpdates.STATUS, status); 339 Uri dataUri = resolver.insert(StatusUpdates.CONTENT_URI, values); 340 return ContentUris.parseId(dataUri); 341 } 342 343 public void updateException(String packageProvider, String packageClient, boolean allowAccess) { 344 throw new UnsupportedOperationException("RestrictionExceptions are hard-coded"); 345 } 346 347 public long getContactForRawContact(long rawContactId) { 348 ensureCallingPackage(); 349 Uri contactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 350 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_RAW_CONTACTS, null, 351 null, null); 352 if (!cursor.moveToFirst()) { 353 cursor.close(); 354 throw new RuntimeException("Contact didn't have an aggregate"); 355 } 356 final long aggId = cursor.getLong(Projections.COL_CONTACTS_ID); 357 cursor.close(); 358 return aggId; 359 } 360 361 public int getDataCountForContact(long contactId) { 362 ensureCallingPackage(); 363 Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, 364 contactId), Contacts.Data.CONTENT_DIRECTORY); 365 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null, 366 null); 367 final int count = cursor.getCount(); 368 cursor.close(); 369 return count; 370 } 371 372 public int getDataCountForRawContact(long rawContactId) { 373 ensureCallingPackage(); 374 Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 375 rawContactId), Contacts.Data.CONTENT_DIRECTORY); 376 final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null, 377 null); 378 final int count = cursor.getCount(); 379 cursor.close(); 380 return count; 381 } 382 383 public void setSuperPrimaryPhone(long dataId) { 384 ensureCallingPackage(); 385 final ContentValues values = new ContentValues(); 386 values.put(Data.IS_PRIMARY, 1); 387 values.put(Data.IS_SUPER_PRIMARY, 1); 388 Uri updateUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId); 389 resolver.update(updateUri, values, null, null); 390 } 391 392 public long createGroup(String groupName) { 393 ensureCallingPackage(); 394 final ContentValues values = new ContentValues(); 395 values.put(ContactsContract.Groups.RES_PACKAGE, packageName); 396 values.put(ContactsContract.Groups.TITLE, groupName); 397 Uri groupUri = resolver.insert(ContactsContract.Groups.CONTENT_URI, values); 398 return ContentUris.parseId(groupUri); 399 } 400 401 public long createGroupMembership(long rawContactId, long groupId) { 402 ensureCallingPackage(); 403 final ContentValues values = new ContentValues(); 404 values.put(Data.RAW_CONTACT_ID, rawContactId); 405 values.put(Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE); 406 values.put(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId); 407 Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, 408 rawContactId), RawContacts.Data.CONTENT_DIRECTORY); 409 Uri dataUri = resolver.insert(insertUri, values); 410 return ContentUris.parseId(dataUri); 411 } 412 413 protected void setAggregationException(int type, long rawContactId1, long rawContactId2) { 414 ContentValues values = new ContentValues(); 415 values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 416 values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 417 values.put(AggregationExceptions.TYPE, type); 418 resolver.update(AggregationExceptions.CONTENT_URI, values, null, null); 419 } 420 421 /** 422 * Various internal database projections. 423 */ 424 private interface Projections { 425 static final String[] PROJ_ID = new String[] { 426 BaseColumns._ID, 427 }; 428 429 static final int COL_ID = 0; 430 431 static final String[] PROJ_RAW_CONTACTS = new String[] { 432 RawContacts.CONTACT_ID 433 }; 434 435 static final int COL_CONTACTS_ID = 0; 436 } 437 } 438