ContactsActor.java revision bee1a6b2e7cbea778195890e442c9e50f2a4e6d9
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.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.PackageManager;
25import android.content.res.Resources;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Binder;
29import android.provider.BaseColumns;
30import android.provider.ContactsContract;
31import android.provider.Contacts.Phones;
32import android.provider.ContactsContract.Aggregates;
33import android.provider.ContactsContract.CommonDataKinds;
34import android.provider.ContactsContract.Contacts;
35import android.provider.ContactsContract.Data;
36import android.provider.ContactsContract.RestrictionExceptions;
37import android.test.IsolatedContext;
38import android.test.RenamingDelegatingContext;
39import android.test.mock.MockContentResolver;
40import android.test.mock.MockContext;
41import android.test.mock.MockPackageManager;
42import android.util.Log;
43
44import java.util.HashMap;
45
46/**
47 * Helper class that encapsulates an "actor" which is owned by a specific
48 * package name. It correctly maintains a wrapped {@link Context} and an
49 * attached {@link MockContentResolver}. Multiple actors can be used to test
50 * security scenarios between multiple packages.
51 */
52public class ContactsActor {
53    private static final String FILENAME_PREFIX = "test.";
54
55    public static final String PACKAGE_GREY = "edu.example.grey";
56    public static final String PACKAGE_RED = "net.example.red";
57    public static final String PACKAGE_GREEN = "com.example.green";
58    public static final String PACKAGE_BLUE = "org.example.blue";
59
60    public Context context;
61    public String packageName;
62    public MockContentResolver resolver;
63    public ContentProvider provider;
64
65    /**
66     * Create an "actor" using the given parent {@link Context} and the specific
67     * package name. Internally, all {@link Context} method calls are passed to
68     * a new instance of {@link RestrictionMockContext}, which stubs out the
69     * security infrastructure.
70     */
71    public ContactsActor(Context overallContext, String packageName,
72            Class<? extends ContentProvider> providerClass, String authority) throws Exception {
73        context = new RestrictionMockContext(overallContext, packageName);
74        this.packageName = packageName;
75        resolver = new MockContentResolver();
76
77        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
78                overallContext, FILENAME_PREFIX);
79        Context providerContext = new IsolatedContext(resolver, targetContextWrapper);
80
81        provider = providerClass.newInstance();
82        provider.attachInfo(providerContext, null);
83        resolver.addProvider(authority, provider);
84    }
85
86    /**
87     * Mock {@link Context} that reports specific well-known values for testing
88     * data protection. The creator can override the owner package name, and
89     * force the {@link PackageManager} to always return a well-known package
90     * list for any call to {@link PackageManager#getPackagesForUid(int)}.
91     * <p>
92     * For example, the creator could request that the {@link Context} lives in
93     * package name "com.example.red", and also cause the {@link PackageManager}
94     * to report that no UID contains that package name.
95     */
96    private static class RestrictionMockContext extends MockContext {
97        private final Context mOverallContext;
98        private final String mReportedPackageName;
99        private final RestrictionMockPackageManager mPackageManager;
100
101        /**
102         * Create a {@link Context} under the given package name.
103         */
104        public RestrictionMockContext(Context overallContext, String reportedPackageName) {
105            mOverallContext = overallContext;
106            mReportedPackageName = reportedPackageName;
107            mPackageManager = new RestrictionMockPackageManager();
108            mPackageManager.addPackage(1000, PACKAGE_GREY);
109            mPackageManager.addPackage(2000, PACKAGE_RED);
110            mPackageManager.addPackage(3000, PACKAGE_GREEN);
111            mPackageManager.addPackage(4000, PACKAGE_BLUE);
112        }
113
114        @Override
115        public String getPackageName() {
116            return mReportedPackageName;
117        }
118
119        @Override
120        public PackageManager getPackageManager() {
121            return mPackageManager;
122        }
123
124        @Override
125        public Resources getResources() {
126            return mOverallContext.getResources();
127        }
128    }
129
130    /**
131     * Mock {@link PackageManager} that knows about a specific set of packages
132     * to help test security models. Because {@link Binder#getCallingUid()}
133     * can't be mocked, you'll have to find your mock-UID manually using your
134     * {@link Context#getPackageName()}.
135     */
136    private static class RestrictionMockPackageManager extends MockPackageManager {
137        private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
138        private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
139
140        /**
141         * Add a UID-to-package mapping, which is then stored internally.
142         */
143        public void addPackage(int packageUid, String packageName) {
144            mForward.put(packageUid, packageName);
145            mReverse.put(packageName, packageUid);
146        }
147
148        @Override
149        public String[] getPackagesForUid(int uid) {
150            return new String[] { mForward.get(uid) };
151        }
152
153        @Override
154        public ApplicationInfo getApplicationInfo(String packageName, int flags) {
155            ApplicationInfo info = new ApplicationInfo();
156            Integer uid = mReverse.get(packageName);
157            info.uid = (uid != null) ? uid : -1;
158            return info;
159        }
160    }
161
162    public long createContact(boolean isRestricted, String name) {
163        long contactId = createContact(isRestricted);
164        createName(contactId, name);
165        return contactId;
166    }
167
168    public long createContact(boolean isRestricted) {
169        final ContentValues values = new ContentValues();
170        values.put(Contacts.PACKAGE, packageName);
171        if (isRestricted) {
172            values.put(Contacts.IS_RESTRICTED, 1);
173        }
174
175        Uri contactUri = resolver.insert(Contacts.CONTENT_URI, values);
176        return ContentUris.parseId(contactUri);
177    }
178
179    public long createName(long contactId, String name) {
180        final ContentValues values = new ContentValues();
181        values.put(Data.CONTACT_ID, contactId);
182        values.put(Data.IS_PRIMARY, 1);
183        values.put(Data.IS_SUPER_PRIMARY, 1);
184        values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
185        values.put(CommonDataKinds.StructuredName.FAMILY_NAME, name);
186        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
187                contactId), Contacts.Data.CONTENT_DIRECTORY);
188        Uri dataUri = resolver.insert(insertUri, values);
189        return ContentUris.parseId(dataUri);
190    }
191
192    public long createPhone(long contactId, String phoneNumber) {
193        final ContentValues values = new ContentValues();
194        values.put(Data.CONTACT_ID, contactId);
195        values.put(Data.IS_PRIMARY, 1);
196        values.put(Data.IS_SUPER_PRIMARY, 1);
197        values.put(Data.MIMETYPE, Phones.CONTENT_ITEM_TYPE);
198        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
199        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
200                contactId), Contacts.Data.CONTENT_DIRECTORY);
201        Uri dataUri = resolver.insert(insertUri, values);
202        return ContentUris.parseId(dataUri);
203    }
204
205    public void updateException(String packageProvider, String packageClient, boolean allowAccess) {
206        final ContentValues values = new ContentValues();
207        values.put(RestrictionExceptions.PACKAGE_PROVIDER, packageProvider);
208        values.put(RestrictionExceptions.PACKAGE_CLIENT, packageClient);
209        values.put(RestrictionExceptions.ALLOW_ACCESS, allowAccess ? 1 : 0);
210        resolver.update(RestrictionExceptions.CONTENT_URI, values, null, null);
211    }
212
213    public long getAggregateForContact(long contactId) {
214        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
215        final Cursor cursor = resolver.query(contactUri, Projections.PROJ_CONTACTS, null,
216                null, null);
217        if (!cursor.moveToFirst()) {
218            cursor.close();
219            throw new RuntimeException("Contact didn't have an aggregate");
220        }
221        final long aggId = cursor.getLong(Projections.COL_CONTACTS_AGGREGATE);
222        cursor.close();
223        return aggId;
224    }
225
226    public int getDataCountForAggregate(long aggId) {
227        Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(Aggregates.CONTENT_URI,
228                aggId), Aggregates.Data.CONTENT_DIRECTORY);
229        final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null,
230                null);
231        final int count = cursor.getCount();
232        cursor.close();
233        return count;
234    }
235
236    public void setSuperPrimaryPhone(long dataId) {
237        final ContentValues values = new ContentValues();
238        values.put(Data.IS_PRIMARY, 1);
239        values.put(Data.IS_SUPER_PRIMARY, 1);
240        Uri updateUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
241        resolver.update(updateUri, values, null, null);
242    }
243
244    public long getPrimaryPhoneId(long aggId) {
245        Uri aggUri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, aggId);
246        final Cursor cursor = resolver.query(aggUri, Projections.PROJ_AGGREGATES, null,
247                null, null);
248        long primaryPhoneId = -1;
249        if (cursor.moveToFirst()) {
250            primaryPhoneId = cursor.getLong(Projections.COL_AGGREGATES_PRIMARY_PHONE_ID);
251        }
252        cursor.close();
253        return primaryPhoneId;
254    }
255
256    public long createGroup(String groupName) {
257        final ContentValues values = new ContentValues();
258        values.put(ContactsContract.Groups.PACKAGE, packageName);
259        values.put(ContactsContract.Groups.TITLE, groupName);
260        Uri groupUri = resolver.insert(ContactsContract.Groups.CONTENT_URI, values);
261        return ContentUris.parseId(groupUri);
262    }
263
264    public long createGroupMembership(long contactId, long groupId) {
265        final ContentValues values = new ContentValues();
266        values.put(Data.CONTACT_ID, contactId);
267        values.put(Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE);
268        values.put(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
269        Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
270                contactId), Contacts.Data.CONTENT_DIRECTORY);
271        Uri dataUri = resolver.insert(insertUri, values);
272        return ContentUris.parseId(dataUri);
273    }
274
275    /**
276     * Various internal database projections.
277     */
278    private interface Projections {
279        static final String[] PROJ_ID = new String[] {
280                BaseColumns._ID,
281        };
282
283        static final int COL_ID = 0;
284
285        static final String[] PROJ_CONTACTS = new String[] {
286                Contacts.AGGREGATE_ID
287        };
288
289        static final int COL_CONTACTS_AGGREGATE = 0;
290
291        static final String[] PROJ_AGGREGATES = new String[] {
292                Aggregates.PRIMARY_PHONE_ID
293        };
294
295        static final int COL_AGGREGATES_PRIMARY_PHONE_ID = 0;
296
297    }
298}
299