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 */
16package android.pim.vcard.test_utils;
17
18import android.content.ContentProviderOperation;
19import android.content.ContentProviderResult;
20import android.content.ContentValues;
21import android.net.Uri;
22import android.provider.ContactsContract.CommonDataKinds.Email;
23import android.provider.ContactsContract.CommonDataKinds.Event;
24import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
25import android.provider.ContactsContract.CommonDataKinds.Im;
26import android.provider.ContactsContract.CommonDataKinds.Nickname;
27import android.provider.ContactsContract.CommonDataKinds.Note;
28import android.provider.ContactsContract.CommonDataKinds.Organization;
29import android.provider.ContactsContract.CommonDataKinds.Phone;
30import android.provider.ContactsContract.CommonDataKinds.Photo;
31import android.provider.ContactsContract.CommonDataKinds.Relation;
32import android.provider.ContactsContract.CommonDataKinds.StructuredName;
33import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
34import android.provider.ContactsContract.CommonDataKinds.Website;
35import android.provider.ContactsContract.Data;
36import android.provider.ContactsContract.RawContacts;
37import android.test.AndroidTestCase;
38import android.test.mock.MockContentProvider;
39import android.text.TextUtils;
40
41import junit.framework.TestCase;
42
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.Collection;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.Map;
49import java.util.Map.Entry;
50import java.util.Set;
51import java.util.SortedMap;
52import java.util.TreeMap;
53
54public class ImportTestProvider extends MockContentProvider {
55    private static final Set<String> sKnownMimeTypeSet =
56        new HashSet<String>(Arrays.asList(StructuredName.CONTENT_ITEM_TYPE,
57                Nickname.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE,
58                Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE,
59                Im.CONTENT_ITEM_TYPE, Organization.CONTENT_ITEM_TYPE,
60                Event.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE,
61                Note.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE,
62                Relation.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE,
63                GroupMembership.CONTENT_ITEM_TYPE));
64
65    final Map<String, Collection<ContentValues>> mMimeTypeToExpectedContentValues;
66
67    public ImportTestProvider(AndroidTestCase androidTestCase) {
68        mMimeTypeToExpectedContentValues =
69            new HashMap<String, Collection<ContentValues>>();
70        for (String acceptanbleMimeType : sKnownMimeTypeSet) {
71            // Do not use HashSet since the current implementation changes the content of
72            // ContentValues after the insertion, which make the result of hashCode()
73            // changes...
74            mMimeTypeToExpectedContentValues.put(
75                    acceptanbleMimeType, new ArrayList<ContentValues>());
76        }
77    }
78
79    public void addExpectedContentValues(ContentValues expectedContentValues) {
80        final String mimeType = expectedContentValues.getAsString(Data.MIMETYPE);
81        if (!sKnownMimeTypeSet.contains(mimeType)) {
82            TestCase.fail(String.format(
83                    "Unknow MimeType %s in the test code. Test code should be broken.",
84                    mimeType));
85        }
86
87        final Collection<ContentValues> contentValuesCollection =
88                mMimeTypeToExpectedContentValues.get(mimeType);
89        contentValuesCollection.add(expectedContentValues);
90    }
91
92    @Override
93    public ContentProviderResult[] applyBatch(
94            ArrayList<ContentProviderOperation> operations) {
95        if (operations == null) {
96            TestCase.fail("There is no operation.");
97        }
98
99        final int size = operations.size();
100        ContentProviderResult[] fakeResultArray = new ContentProviderResult[size];
101        for (int i = 0; i < size; i++) {
102            Uri uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, String.valueOf(i));
103            fakeResultArray[i] = new ContentProviderResult(uri);
104        }
105
106        for (int i = 0; i < size; i++) {
107            ContentProviderOperation operation = operations.get(i);
108            ContentValues contentValues = operation.resolveValueBackReferences(
109                    fakeResultArray, i);
110        }
111        for (int i = 0; i < size; i++) {
112            ContentProviderOperation operation = operations.get(i);
113            ContentValues actualContentValues = operation.resolveValueBackReferences(
114                    fakeResultArray, i);
115            final Uri uri = operation.getUri();
116            if (uri.equals(RawContacts.CONTENT_URI)) {
117                TestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_NAME));
118                TestCase.assertNull(actualContentValues.get(RawContacts.ACCOUNT_TYPE));
119            } else if (uri.equals(Data.CONTENT_URI)) {
120                final String mimeType = actualContentValues.getAsString(Data.MIMETYPE);
121                if (!sKnownMimeTypeSet.contains(mimeType)) {
122                    TestCase.fail(String.format(
123                            "Unknown MimeType %s. Probably added after developing this test",
124                            mimeType));
125                }
126                // Remove data meaningless in this unit tests.
127                // Specifically, Data.DATA1 - DATA7 are set to null or empty String
128                // regardless of the input, but it may change depending on how
129                // resolver-related code handles it.
130                // Here, we ignore these implementation-dependent specs and
131                // just check whether vCard importer correctly inserts rellevent data.
132                Set<String> keyToBeRemoved = new HashSet<String>();
133                for (Entry<String, Object> entry : actualContentValues.valueSet()) {
134                    Object value = entry.getValue();
135                    if (value == null || TextUtils.isEmpty(value.toString())) {
136                        keyToBeRemoved.add(entry.getKey());
137                    }
138                }
139                for (String key: keyToBeRemoved) {
140                    actualContentValues.remove(key);
141                }
142                /* for testing
143                Log.d("@@@",
144                        String.format("MimeType: %s, data: %s",
145                                mimeType, actualContentValues.toString())); */
146                // Remove RAW_CONTACT_ID entry just for safety, since we do not care
147                // how resolver-related code handles the entry in this unit test,
148                if (actualContentValues.containsKey(Data.RAW_CONTACT_ID)) {
149                    actualContentValues.remove(Data.RAW_CONTACT_ID);
150                }
151                final Collection<ContentValues> contentValuesCollection =
152                    mMimeTypeToExpectedContentValues.get(mimeType);
153                if (contentValuesCollection.isEmpty()) {
154                    TestCase.fail("ContentValues for MimeType " + mimeType
155                            + " is not expected at all (" + actualContentValues + ")");
156                }
157                boolean checked = false;
158                for (ContentValues expectedContentValues : contentValuesCollection) {
159                    /*for testing
160                    Log.d("@@@", "expected: "
161                            + convertToEasilyReadableString(expectedContentValues));
162                    Log.d("@@@", "actual  : "
163                            + convertToEasilyReadableString(actualContentValues));*/
164                    if (equalsForContentValues(expectedContentValues,
165                            actualContentValues)) {
166                        TestCase.assertTrue(contentValuesCollection.remove(expectedContentValues));
167                        checked = true;
168                        break;
169                    }
170                }
171                if (!checked) {
172                    final StringBuilder builder = new StringBuilder();
173                    builder.append("\n");
174                    builder.append("Unexpected: ");
175                    builder.append(convertToEasilyReadableString(actualContentValues));
176                    builder.append("\n");
177                    builder.append("Expected  : ");
178                    for (ContentValues expectedContentValues : contentValuesCollection) {
179                        builder.append(convertToEasilyReadableString(expectedContentValues));
180                    }
181                    TestCase.fail(builder.toString());
182                }
183            } else {
184                TestCase.fail("Unexpected Uri has come: " + uri);
185            }
186        }  // for (int i = 0; i < size; i++) {
187        return fakeResultArray;
188    }
189
190    public void verify() {
191        StringBuilder builder = new StringBuilder();
192        for (Collection<ContentValues> contentValuesCollection :
193                mMimeTypeToExpectedContentValues.values()) {
194            for (ContentValues expectedContentValues: contentValuesCollection) {
195                builder.append(convertToEasilyReadableString(expectedContentValues));
196                builder.append("\n");
197            }
198        }
199        if (builder.length() > 0) {
200            final String failMsg =
201                "There is(are) remaining expected ContentValues instance(s): \n"
202                    + builder.toString();
203            TestCase.fail(failMsg);
204        }
205    }
206
207    /**
208     * Utility method to print ContentValues whose content is printed with sorted keys.
209     */
210    private String convertToEasilyReadableString(ContentValues contentValues) {
211        if (contentValues == null) {
212            return "null";
213        }
214        String mimeTypeValue = "";
215        SortedMap<String, String> sortedMap = new TreeMap<String, String>();
216        for (Entry<String, Object> entry : contentValues.valueSet()) {
217            final String key = entry.getKey();
218            final Object value = entry.getValue();
219            final String valueString = (value != null ? value.toString() : null);
220            if (Data.MIMETYPE.equals(key)) {
221                mimeTypeValue = valueString;
222            } else {
223                TestCase.assertNotNull(key);
224                sortedMap.put(key, valueString);
225            }
226        }
227        StringBuilder builder = new StringBuilder();
228        builder.append(Data.MIMETYPE);
229        builder.append('=');
230        builder.append(mimeTypeValue);
231        for (Entry<String, String> entry : sortedMap.entrySet()) {
232            final String key = entry.getKey();
233            final String value = entry.getValue();
234            builder.append(' ');
235            builder.append(key);
236            builder.append("=\"");
237            builder.append(value);
238            builder.append('"');
239        }
240        return builder.toString();
241    }
242
243    private static boolean equalsForContentValues(
244            final ContentValues expected, final ContentValues actual) {
245        if (expected == actual) {
246            return true;
247        } else if (expected == null || actual == null || expected.size() != actual.size()) {
248            return false;
249        }
250
251        for (Entry<String, Object> entry : expected.valueSet()) {
252            final String key = entry.getKey();
253            final Object value = entry.getValue();
254            if (!actual.containsKey(key)) {
255                return false;
256            }
257            // Type mismatch usuall happens as importer doesn't care the type of each value.
258            // For example, variable type might be Integer when importing the type of TEL,
259            // while variable type would be String when importing the type of RELATION.
260            final Object actualValue = actual.get(key);
261            if (value instanceof byte[]) {
262                if (!Arrays.equals((byte[])value, (byte[])actualValue)) {
263                    byte[] e = (byte[])value;
264                    byte[] a = (byte[])actualValue;
265                    return false;
266                }
267            } else if (!value.equals(actualValue) &&
268                    !value.toString().equals(actualValue.toString())) {
269                return false;
270            }
271        }
272        return true;
273    }
274}