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