Home | History | Annotate | Download | only in contacts
      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