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