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 
17 package com.android.providers.contacts;
18 
19 import com.android.internal.telephony.CallerInfo;
20 import com.android.internal.telephony.PhoneConstants;
21 import com.android.providers.contacts.testutil.CommonDatabaseUtils;
22 
23 import android.content.ComponentName;
24 import android.content.ContentProvider;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.ContextWrapper;
29 import android.content.Intent;
30 import android.content.pm.PackageManager;
31 import android.database.Cursor;
32 import android.database.MatrixCursor;
33 import android.net.Uri;
34 import android.provider.CallLog;
35 import android.provider.CallLog.Calls;
36 import android.provider.ContactsContract;
37 import android.provider.ContactsContract.CommonDataKinds.Phone;
38 import android.provider.VoicemailContract.Voicemails;
39 import android.telecom.PhoneAccountHandle;
40 import android.test.suitebuilder.annotation.MediumTest;
41 
42 import java.util.Arrays;
43 import java.util.List;
44 
45 /**
46  * Unit tests for {@link CallLogProvider}.
47  *
48  * Run the test like this:
49  * <code>
50  * adb shell am instrument -e class com.android.providers.contacts.CallLogProviderTest -w \
51  *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
52  * </code>
53  */
54 @MediumTest
55 public class CallLogProviderTest extends BaseContactsProvider2Test {
56     /** Fields specific to voicemail provider that should not be exposed by call_log*/
57     private static final String[] VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS = new String[] {
58             Voicemails._DATA,
59             Voicemails.HAS_CONTENT,
60             Voicemails.MIME_TYPE,
61             Voicemails.SOURCE_PACKAGE,
62             Voicemails.SOURCE_DATA,
63             Voicemails.STATE};
64     /** Total number of columns exposed by call_log provider. */
65     private static final int NUM_CALLLOG_FIELDS = 24;
66 
67     private CallLogProvider mCallLogProvider;
68 
69     @Override
getProviderClass()70     protected Class<? extends ContentProvider> getProviderClass() {
71        return SynchronousContactsProvider2.class;
72     }
73 
74     @Override
getAuthority()75     protected String getAuthority() {
76         return ContactsContract.AUTHORITY;
77     }
78 
79     @Override
setUp()80     protected void setUp() throws Exception {
81         super.setUp();
82         mCallLogProvider = (CallLogProvider) addProvider(TestCallLogProvider.class,
83                 CallLog.AUTHORITY);
84     }
85 
86     @Override
tearDown()87     protected void tearDown() throws Exception {
88         setUpWithVoicemailPermissions();
89         mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
90         super.tearDown();
91     }
92 
testInsert_RegularCallRecord()93     public void testInsert_RegularCallRecord() {
94         ContentValues values = getDefaultCallValues();
95         Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
96         values.put(Calls.COUNTRY_ISO, "us");
97         assertStoredValues(uri, values);
98         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
99     }
100 
setUpWithVoicemailPermissions()101     private void setUpWithVoicemailPermissions() {
102         mActor.addPermissions(ADD_VOICEMAIL_PERMISSION);
103         mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
104         mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
105     }
106 
testInsert_VoicemailCallRecord()107     public void testInsert_VoicemailCallRecord() {
108         setUpWithVoicemailPermissions();
109         final ContentValues values = getDefaultCallValues();
110         values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE);
111         values.put(Calls.VOICEMAIL_URI, "content://foo/voicemail/2");
112 
113         // Should fail with the base content uri without the voicemail param.
114         EvenMoreAsserts.assertThrows(IllegalArgumentException.class, new Runnable() {
115             @Override
116             public void run() {
117                 mResolver.insert(Calls.CONTENT_URI, values);
118             }
119         });
120 
121         // Now grant voicemail permission - should succeed.
122         Uri uri  = mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, values);
123         assertStoredValues(uri, values);
124         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
125     }
126 
testUpdate()127     public void testUpdate() {
128         Uri uri = insertCallRecord();
129         ContentValues values = new ContentValues();
130         values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
131         values.put(Calls.NUMBER, "1-800-263-7643");
132         values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
133         values.put(Calls.DATE, 2000);
134         values.put(Calls.DURATION, 40);
135         values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
136         values.put(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM);
137         values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
138 
139         int count = mResolver.update(uri, values, null, null);
140         assertEquals(1, count);
141         assertStoredValues(uri, values);
142     }
143 
testDelete()144     public void testDelete() {
145         Uri uri = insertCallRecord();
146         try {
147             mResolver.delete(uri, null, null);
148             fail();
149         } catch (UnsupportedOperationException ex) {
150             // Expected
151         }
152 
153         int count = mResolver.delete(Calls.CONTENT_URI, Calls._ID + "="
154                 + ContentUris.parseId(uri), null);
155         assertEquals(1, count);
156         assertEquals(0, getCount(uri, null, null));
157     }
158 
testCallLogFilter()159     public void testCallLogFilter() {
160         ContentValues values = getDefaultCallValues();
161         mResolver.insert(Calls.CONTENT_URI, values);
162 
163         Uri filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-800-4664-411");
164         Cursor c = mResolver.query(filterUri, null, null, null, null);
165         assertEquals(1, c.getCount());
166         c.moveToFirst();
167         assertCursorValues(c, values);
168         c.close();
169 
170         filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-888-4664-411");
171         c = mResolver.query(filterUri, null, null, null, null);
172         assertEquals(0, c.getCount());
173         c.close();
174     }
175 
testAddCall()176     public void testAddCall() {
177         CallerInfo ci = new CallerInfo();
178         ci.name = "1-800-GOOG-411";
179         ci.numberType = Phone.TYPE_CUSTOM;
180         ci.numberLabel = "Directory";
181         final ComponentName sComponentName = new ComponentName(
182                 "com.android.server.telecom",
183                 "TelecomServiceImpl");
184         PhoneAccountHandle subscription = new PhoneAccountHandle(
185                 sComponentName, "sub0");
186 
187         Uri uri = Calls.addCall(ci, getMockContext(), "1-800-263-7643",
188                 PhoneConstants.PRESENTATION_ALLOWED, Calls.OUTGOING_TYPE, 0, subscription, 2000,
189                 40, null);
190 
191         ContentValues values = new ContentValues();
192         values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
193         values.put(Calls.FEATURES, 0);
194         values.put(Calls.NUMBER, "1-800-263-7643");
195         values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
196         values.put(Calls.DATE, 2000);
197         values.put(Calls.DURATION, 40);
198         values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
199         values.put(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM);
200         values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
201         values.put(Calls.COUNTRY_ISO, "us");
202         values.put(Calls.GEOCODED_LOCATION, "usa");
203         values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME,
204                 "com.android.server.telecom/TelecomServiceImpl");
205         values.put(Calls.PHONE_ACCOUNT_ID, "sub0");
206         // Casting null to Long as there are many forms of "put" which have nullable second
207         // parameters and the compiler needs a hint as to which form is correct.
208         values.put(Calls.DATA_USAGE, (Long) null);
209         assertStoredValues(uri, values);
210     }
211 
212     // Test to check that the calls and voicemail uris returns expected results.
testDifferentContentUris()213     public void testDifferentContentUris() {
214         setUpWithVoicemailPermissions();
215         // Insert one voicemaail and two regular call record.
216         insertVoicemailRecord();
217         insertCallRecord();
218         insertCallRecord();
219 
220         // With the default uri, only 2 call entries should be returned.
221         // With the voicemail uri all 3 should be returned.
222         assertEquals(2, getCount(Calls.CONTENT_URI, null, null));
223         assertEquals(3, getCount(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null));
224     }
225 
testLimitParamReturnsCorrectLimit()226     public void testLimitParamReturnsCorrectLimit() {
227         for (int i=0; i<10; i++) {
228             insertCallRecord();
229         }
230         Uri uri = Calls.CONTENT_URI.buildUpon()
231                 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, "4")
232                 .build();
233         assertEquals(4, getCount(uri, null, null));
234     }
235 
testLimitAndOffsetParamReturnsCorrectEntries()236     public void testLimitAndOffsetParamReturnsCorrectEntries() {
237         for (int i=0; i<10; i++) {
238             mResolver.insert(Calls.CONTENT_URI, getDefaultValues(Calls.INCOMING_TYPE));
239         }
240         for (int i=0; i<10; i++) {
241             mResolver.insert(Calls.CONTENT_URI, getDefaultValues(Calls.MISSED_TYPE));
242         }
243         // Limit 4 records.  Discard first 8.
244         Uri uri = Calls.CONTENT_URI.buildUpon()
245                 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, "4")
246                 .appendQueryParameter(Calls.OFFSET_PARAM_KEY, "8")
247                 .build();
248         String[] projection = new String[] {Calls._ID, Calls.TYPE};
249         Cursor c = mResolver.query(uri, projection, null, null, null);
250         try {
251             // First two should be incoming, next two should be missed.
252             for (int i = 0; i < 2; i++) {
253                 c.moveToNext();
254                 assertEquals(Calls.INCOMING_TYPE, c.getInt(1));
255             }
256             for (int i = 0; i < 2; i++) {
257                 c.moveToNext();
258                 assertEquals(Calls.MISSED_TYPE, c.getInt(1));
259             }
260         } finally {
261             c.close();
262         }
263     }
264 
testUriWithBadLimitParamThrowsException()265     public void testUriWithBadLimitParamThrowsException() {
266         assertParamThrowsIllegalArgumentException(Calls.LIMIT_PARAM_KEY, "notvalid");
267     }
268 
testUriWithBadOffsetParamThrowsException()269     public void testUriWithBadOffsetParamThrowsException() {
270         assertParamThrowsIllegalArgumentException(Calls.OFFSET_PARAM_KEY, "notvalid");
271     }
272 
assertParamThrowsIllegalArgumentException(String key, String value)273     private void assertParamThrowsIllegalArgumentException(String key, String value) {
274         Uri uri = Calls.CONTENT_URI.buildUpon()
275                 .appendQueryParameter(key, value)
276                 .build();
277         try {
278             mResolver.query(uri, null, null, null, null);
279             fail();
280         } catch (IllegalArgumentException e) {
281             assertTrue("Error does not contain value in question.",
282                     e.toString().contains(value));
283         }
284     }
285 
286     // Test to check that none of the voicemail provider specific fields are
287     // insertable through call_log provider.
testCannotAccessVoicemailSpecificFields_Insert()288     public void testCannotAccessVoicemailSpecificFields_Insert() {
289         for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
290             final ContentValues values = getDefaultCallValues();
291             values.put(voicemailColumn, "foo");
292             EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
293                     IllegalArgumentException.class, new Runnable() {
294                     @Override
295                     public void run() {
296                         mResolver.insert(Calls.CONTENT_URI, values);
297                     }
298                 });
299         }
300     }
301 
302     // Test to check that none of the voicemail provider specific fields are
303     // exposed through call_log provider query.
testCannotAccessVoicemailSpecificFields_Query()304     public void testCannotAccessVoicemailSpecificFields_Query() {
305         // Query.
306         Cursor cursor = mResolver.query(Calls.CONTENT_URI, null, null, null, null);
307         List<String> columnNames = Arrays.asList(cursor.getColumnNames());
308         assertEquals(NUM_CALLLOG_FIELDS, columnNames.size());
309         // None of the voicemail provider specific columns should be present.
310         for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
311             assertFalse("Unexpected column: '" + voicemailColumn + "' returned.",
312                     columnNames.contains(voicemailColumn));
313         }
314     }
315 
316     // Test to check that none of the voicemail provider specific fields are
317     // updatable through call_log provider.
testCannotAccessVoicemailSpecificFields_Update()318     public void testCannotAccessVoicemailSpecificFields_Update() {
319         for (String voicemailColumn : VOICEMAIL_PROVIDER_SPECIFIC_COLUMNS) {
320             final Uri insertedUri = insertCallRecord();
321             final ContentValues values = new ContentValues();
322             values.put(voicemailColumn, "foo");
323             EvenMoreAsserts.assertThrows("Column: " + voicemailColumn,
324                     IllegalArgumentException.class, new Runnable() {
325                     @Override
326                     public void run() {
327                         mResolver.update(insertedUri, values, null, null);
328                     }
329                 });
330         }
331     }
332 
testVoicemailPermissions_Insert()333     public void testVoicemailPermissions_Insert() {
334         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
335             @Override
336             public void run() {
337                 mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
338             }
339         });
340         // Should now succeed with permissions granted.
341         setUpWithVoicemailPermissions();
342         mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
343     }
344 
testVoicemailPermissions_Update()345     public void testVoicemailPermissions_Update() {
346         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
347             @Override
348             public void run() {
349                 mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues(),
350                         null, null);
351             }
352         });
353 
354         // Should succeed with manage permission granted
355         mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
356         mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultCallValues(), null, null);
357         mActor.removePermissions(WRITE_VOICEMAIL_PERMISSION);
358 
359         // Should also succeed with full permissions granted.
360         setUpWithVoicemailPermissions();
361         mResolver.update(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultCallValues(), null, null);
362     }
363 
testVoicemailPermissions_Query()364     public void testVoicemailPermissions_Query() {
365         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
366             @Override
367             public void run() {
368                 mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
369             }
370         });
371 
372         // Should succeed with read_all permission granted
373         mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
374         mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
375         mActor.removePermissions(READ_VOICEMAIL_PERMISSION);
376 
377         // Should also succeed with full permissions granted.
378         setUpWithVoicemailPermissions();
379         mResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null, null, null);
380     }
381 
testVoicemailPermissions_Delete()382     public void testVoicemailPermissions_Delete() {
383         EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
384             @Override
385             public void run() {
386                 mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
387             }
388         });
389 
390         // Should succeed with manage permission granted
391         mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
392         mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
393         mActor.removePermissions(WRITE_VOICEMAIL_PERMISSION);
394 
395         // Should now succeed with permissions granted.
396         setUpWithVoicemailPermissions();
397         mResolver.delete(Calls.CONTENT_URI_WITH_VOICEMAIL, null, null);
398     }
399 
testCopyEntriesFromCursor_ReturnsMostRecentEntryTimestamp()400     public void testCopyEntriesFromCursor_ReturnsMostRecentEntryTimestamp() {
401         assertEquals(10, mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor()));
402     }
403 
testCopyEntriesFromCursor_AllEntriesSyncedWithoutDuplicatesPresent()404     public void testCopyEntriesFromCursor_AllEntriesSyncedWithoutDuplicatesPresent() {
405         assertStoredValues(Calls.CONTENT_URI);
406         mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
407         assertStoredValues(Calls.CONTENT_URI,
408                 getTestCallLogValues(2),
409                 getTestCallLogValues(1),
410                 getTestCallLogValues(0));
411     }
412 
testCopyEntriesFromCursor_DuplicatesIgnoredCorrectly()413     public void testCopyEntriesFromCursor_DuplicatesIgnoredCorrectly() {
414         mResolver.insert(Calls.CONTENT_URI, getTestCallLogValues(1));
415         assertStoredValues(Calls.CONTENT_URI, getTestCallLogValues(1));
416         mCallLogProvider.copyEntriesFromCursor(getTestCallLogCursor());
417         assertStoredValues(Calls.CONTENT_URI,
418                 getTestCallLogValues(2),
419                 getTestCallLogValues(1),
420                 getTestCallLogValues(0));
421     }
422 
getDefaultValues(int callType)423     private ContentValues getDefaultValues(int callType) {
424         ContentValues values = new ContentValues();
425         values.put(Calls.TYPE, callType);
426         values.put(Calls.NUMBER, "1-800-4664-411");
427         values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
428         values.put(Calls.DATE, 1000);
429         values.put(Calls.DURATION, 30);
430         values.put(Calls.NEW, 1);
431         return values;
432     }
433 
getDefaultCallValues()434     private ContentValues getDefaultCallValues() {
435         return getDefaultValues(Calls.INCOMING_TYPE);
436     }
437 
getDefaultVoicemailValues()438     private ContentValues getDefaultVoicemailValues() {
439         return getDefaultValues(Calls.VOICEMAIL_TYPE);
440     }
441 
insertCallRecord()442     private Uri insertCallRecord() {
443         return mResolver.insert(Calls.CONTENT_URI, getDefaultCallValues());
444     }
445 
insertVoicemailRecord()446     private Uri insertVoicemailRecord() {
447         return mResolver.insert(Calls.CONTENT_URI_WITH_VOICEMAIL, getDefaultVoicemailValues());
448     }
449 
450     public static class TestCallLogProvider extends CallLogProvider {
451         private static ContactsDatabaseHelper mDbHelper;
452 
453         @Override
getDatabaseHelper(final Context context)454         protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
455             if (mDbHelper == null) {
456                 mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
457             }
458             return mDbHelper;
459         }
460 
461         @Override
createCallLogInsertionHelper(Context context)462         protected CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
463             return new CallLogInsertionHelper() {
464                 @Override
465                 public String getGeocodedLocationFor(String number, String countryIso) {
466                     return "usa";
467                 }
468 
469                 @Override
470                 public void addComputedValues(ContentValues values) {
471                     values.put(Calls.COUNTRY_ISO, "us");
472                     values.put(Calls.GEOCODED_LOCATION, "usa");
473                 }
474             };
475         }
476 
477         @Override
context()478         protected Context context() {
479             return new ContextWrapper(super.context()) {
480                 @Override
481                 public PackageManager getPackageManager() {
482                     return new MockPackageManager("com.test.package1", "com.test.package2");
483                 }
484 
485                 @Override
486                 public void sendBroadcast(Intent intent, String receiverPermission) {
487                    // Do nothing for now.
488                 }
489             };
490         }
491     }
492 
493     private Cursor getTestCallLogCursor() {
494         final MatrixCursor cursor = new MatrixCursor(CallLogProvider.CALL_LOG_SYNC_PROJECTION);
495         for (int i = 2; i >= 0; i--) {
496             cursor.addRow(CommonDatabaseUtils.getArrayFromContentValues(getTestCallLogValues(i),
497                     CallLogProvider.CALL_LOG_SYNC_PROJECTION));
498         }
499         return cursor;
500     }
501 
502     /**
503      * Returns a predefined {@link ContentValues} object based on the provided index.
504      */
505     private ContentValues getTestCallLogValues(int i) {
506         ContentValues values = new ContentValues();
507         switch (i) {
508             case 0:
509                 values.put(Calls.NUMBER, "123456");
510                 values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
511                 values.put(Calls.TYPE, Calls.MISSED_TYPE);
512                 values.put(Calls.FEATURES, 0);
513                 values.put(Calls.DATE, 10);
514                 values.put(Calls.DURATION, 100);
515                 values.put(Calls.DATA_USAGE, 1000);
516                 values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
517                 values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
518                 break;
519             case 1:
520                 values.put(Calls.NUMBER, "654321");
521                 values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
522                 values.put(Calls.TYPE, Calls.INCOMING_TYPE);
523                 values.put(Calls.FEATURES, 0);
524                 values.put(Calls.DATE, 5);
525                 values.put(Calls.DURATION, 200);
526                 values.put(Calls.DATA_USAGE, 0);
527                 values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
528                 values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
529                 break;
530             case 2:
531                 values.put(Calls.NUMBER, "123456");
532                 values.put(Calls.NUMBER_PRESENTATION, Calls.PRESENTATION_ALLOWED);
533                 values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
534                 values.put(Calls.FEATURES, Calls.FEATURES_VIDEO);
535                 values.put(Calls.DATE, 1);
536                 values.put(Calls.DURATION, 50);
537                 values.put(Calls.DATA_USAGE, 2000);
538                 values.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, (String) null);
539                 values.put(Calls.PHONE_ACCOUNT_ID, (Long) null);
540                 break;
541         }
542         return values;
543     }
544 }
545