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