1/*
2 * Copyright (C) 2010 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.exchange.provider;
18
19import android.content.ContentProvider;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.UriMatcher;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.net.Uri;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29import java.util.Set;
30import java.util.Map.Entry;
31
32/**
33 * MockProvider is a ContentProvider that can be used to simulate the storage and retrieval of
34 * records from any ContentProvider, even if that ContentProvider does not exist in the caller's
35 * package.  It is specifically designed to enable testing of sync adapters that create
36 * ContentProviderOperations (CPOs) that are then executed using ContentResolver.applyBatch()
37 *
38 * Why is this useful?  Because we can't instantiate CalendarProvider or ContactsProvider from our
39 * package, as required by MockContentResolver.addProvider()
40 *
41 * Usage:
42 *     ContentResolver.applyBatch(MockProvider.AUTHORITY, batch) will cause the CPOs to be executed,
43 *     returning an array of ContentProviderResult; in the case of inserts, the result will include
44 *     a Uri that can be used via query().  Note that the CPOs in the batch can contain references
45 *     to any authority.
46 *
47 *     query() does not allow non-null selection, selectionArgs, or sortOrder arguments; the
48 *     presence of these will result in an UnsupportedOperationException
49 *
50 *     insert() acts as expected, returning a Uri that can be directly used in a query
51 *
52 *     delete() and update() do not allow non-null selection or selectionArgs arguments; the
53 *     presence of these will result in an UnsupportedOperationException
54 *
55 *     NOTE: When using any operation other than applyBatch, the Uri to be used must be created
56 *     with MockProvider.uri(yourUri).  This guarantees that the operation is sent to MockProvider
57 *
58 *     NOTE: MockProvider only simulates direct storage/retrieval of rows; it does not (and can not)
59 *     simulate other actions (e.g. creation of ancillary data) that the actual provider might
60 *     perform
61 *
62 *     NOTE: See MockProviderTests for usage examples
63 **/
64public class MockProvider extends ContentProvider {
65    public static final String AUTHORITY = "com.android.exchange.mock.provider";
66    /*package*/ static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
67
68    /*package*/ static final int TABLE = 100;
69    /*package*/ static final int RECORD = 101;
70
71    public static final String ID_COLUMN = "_id";
72
73    public MockProvider(Context context) {
74        super(context, null, null, null);
75    }
76
77    public MockProvider() {
78        super();
79    }
80
81    // We'll store our values here
82    private HashMap<String, ContentValues> mMockStore = new HashMap<String, ContentValues>();
83    // And we'll generate new id's from here
84    long mMockId = 1;
85
86    /**
87     * Create a Uri for MockProvider from a given Uri
88     * @param uri the Uri from which the MockProvider Uri will be created
89     * @return a Uri that can be used with MockProvider
90     */
91    public static Uri uri(Uri uri) {
92        return new Uri.Builder().scheme("content").authority(AUTHORITY)
93            .path(uri.getPath().substring(1)).build();
94    }
95
96    @Override
97    public int delete(Uri uri, String selection, String[] selectionArgs) {
98        if (selection != null || selectionArgs != null) {
99            throw new UnsupportedOperationException();
100        }
101        String path = uri.getPath();
102        if (mMockStore.containsKey(path)) {
103            mMockStore.remove(path);
104            return 1;
105        } else {
106            return 0;
107        }
108    }
109
110    @Override
111    public String getType(Uri uri) {
112        throw new UnsupportedOperationException();
113    }
114
115    @Override
116    public Uri insert(Uri uri, ContentValues values) {
117        // Remove the leading slash
118        String table = uri.getPath().substring(1);
119        long id = mMockId++;
120        Uri newUri = new Uri.Builder().scheme("content").authority(AUTHORITY).path(table)
121            .appendPath(Long.toString(id)).build();
122        // Remember to store the _id
123        values.put(ID_COLUMN, id);
124        mMockStore.put(newUri.getPath(), values);
125        int match = sURIMatcher.match(uri);
126        if (match == UriMatcher.NO_MATCH) {
127            sURIMatcher.addURI(AUTHORITY, table, TABLE);
128            sURIMatcher.addURI(AUTHORITY, table + "/#", RECORD);
129        }
130        return newUri;
131    }
132
133    @Override
134    public boolean onCreate() {
135        return false;
136    }
137
138    @Override
139    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
140            String sortOrder) {
141        if (selection != null || selectionArgs != null || sortOrder != null || projection == null) {
142            throw new UnsupportedOperationException();
143        }
144        final int match = sURIMatcher.match(uri(uri));
145        ArrayList<ContentValues> valuesList = new ArrayList<ContentValues>();
146        switch(match) {
147            case TABLE:
148                Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
149                String prefix = uri.getPath() + "/";
150                for (Entry<String, ContentValues> entry: entrySet) {
151                    if (entry.getKey().startsWith(prefix)) {
152                        valuesList.add(entry.getValue());
153                    }
154                }
155                break;
156            case RECORD:
157                ContentValues values = mMockStore.get(uri.getPath());
158                if (values != null) {
159                    valuesList.add(values);
160                }
161                break;
162            default:
163                throw new IllegalArgumentException("Unknown URI " + uri);
164        }
165        MatrixCursor cursor = new MatrixCursor(projection, 1);
166        for (ContentValues cv: valuesList) {
167            Object[] rowValues = new Object[projection.length];
168            int i = 0;
169            for (String column: projection) {
170                rowValues[i++] = cv.get(column);
171            }
172            cursor.addRow(rowValues);
173        }
174        return cursor;
175    }
176
177    @Override
178    public int update(Uri uri, ContentValues newValues, String selection, String[] selectionArgs) {
179        if (selection != null || selectionArgs != null) {
180            throw new UnsupportedOperationException();
181        }
182        final int match = sURIMatcher.match(uri(uri));
183        ArrayList<ContentValues> updateValuesList = new ArrayList<ContentValues>();
184        String path = uri.getPath();
185        switch(match) {
186            case TABLE:
187                Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
188                String prefix = path + "/";
189                for (Entry<String, ContentValues> entry: entrySet) {
190                    if (entry.getKey().startsWith(prefix)) {
191                        updateValuesList.add(entry.getValue());
192                    }
193                }
194                break;
195            case RECORD:
196                ContentValues cv = mMockStore.get(path);
197                if (cv != null) {
198                    updateValuesList.add(cv);
199                }
200                break;
201            default:
202                throw new IllegalArgumentException("Unknown URI " + uri);
203        }
204        Set<Entry<String, Object>> newValuesSet = newValues.valueSet();
205        for (Entry<String, Object> entry: newValuesSet) {
206            String key = entry.getKey();
207            Object value = entry.getValue();
208            for (ContentValues targetValues: updateValuesList) {
209                if (value instanceof Integer) {
210                    targetValues.put(key, (Integer)value);
211                } else if (value instanceof Long) {
212                    targetValues.put(key, (Long)value);
213                } else if (value instanceof String) {
214                    targetValues.put(key, (String)value);
215                } else if (value instanceof Boolean) {
216                    targetValues.put(key, (Boolean)value);
217                } else {
218                    throw new IllegalArgumentException();
219                }
220            }
221        }
222        for (ContentValues targetValues: updateValuesList) {
223            mMockStore.put(path + "/" + targetValues.getAsLong(ID_COLUMN), targetValues);
224        }
225        return updateValuesList.size();
226    }
227}
228