• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
20 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
21 import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
22 
23 import android.app.AppOpsManager;
24 import android.content.ContentProvider;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.UriMatcher;
29 import android.database.Cursor;
30 import android.database.DatabaseUtils;
31 import android.database.sqlite.SQLiteDatabase;
32 import android.database.sqlite.SQLiteQueryBuilder;
33 import android.net.Uri;
34 import android.os.UserHandle;
35 import android.os.UserManager;
36 import android.provider.CallLog;
37 import android.provider.CallLog.Calls;
38 import android.text.TextUtils;
39 import android.util.Log;
40 
41 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
42 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
43 import com.android.providers.contacts.util.SelectionBuilder;
44 import com.android.providers.contacts.util.UserUtils;
45 
46 import com.google.common.annotations.VisibleForTesting;
47 
48 import java.util.HashMap;
49 import java.util.List;
50 
51 /**
52  * Call log content provider.
53  */
54 public class CallLogProvider extends ContentProvider {
55     private static final String TAG = CallLogProvider.class.getSimpleName();
56 
57     /** Selection clause for selecting all calls that were made after a certain time */
58     private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
59     /** Selection clause to use to exclude voicemail records.  */
60     private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
61             Calls.TYPE, Calls.VOICEMAIL_TYPE);
62 
63     @VisibleForTesting
64     static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
65         Calls.NUMBER,
66         Calls.NUMBER_PRESENTATION,
67         Calls.TYPE,
68         Calls.FEATURES,
69         Calls.DATE,
70         Calls.DURATION,
71         Calls.DATA_USAGE,
72         Calls.PHONE_ACCOUNT_COMPONENT_NAME,
73         Calls.PHONE_ACCOUNT_ID
74     };
75 
76     private static final int CALLS = 1;
77 
78     private static final int CALLS_ID = 2;
79 
80     private static final int CALLS_FILTER = 3;
81 
82     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
83     static {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS)84         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID)85         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER)86         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
87     }
88 
89     private static final HashMap<String, String> sCallsProjectionMap;
90     static {
91 
92         // Calls projection map
93         sCallsProjectionMap = new HashMap<String, String>();
sCallsProjectionMap.put(Calls._ID, Calls._ID)94         sCallsProjectionMap.put(Calls._ID, Calls._ID);
sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER)95         sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION)96         sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
sCallsProjectionMap.put(Calls.DATE, Calls.DATE)97         sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION)98         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE)99         sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE)100         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES)101         sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME)102         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID)103         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW)104         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI)105         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION)106         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ)107         sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME)108         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE)109         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL)110         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO)111         sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION)112         sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI)113         sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER)114         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER)115         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID)116         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER)117         sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
118     }
119 
120     private ContactsDatabaseHelper mDbHelper;
121     private DatabaseUtils.InsertHelper mCallsInserter;
122     private boolean mUseStrictPhoneNumberComparation;
123     private VoicemailPermissions mVoicemailPermissions;
124     private CallLogInsertionHelper mCallLogInsertionHelper;
125 
126     @Override
onCreate()127     public boolean onCreate() {
128         setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
129         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
130             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start");
131         }
132         final Context context = getContext();
133         mDbHelper = getDatabaseHelper(context);
134         mUseStrictPhoneNumberComparation =
135             context.getResources().getBoolean(
136                     com.android.internal.R.bool.config_use_strict_phone_number_comparation);
137         mVoicemailPermissions = new VoicemailPermissions(context);
138         mCallLogInsertionHelper = createCallLogInsertionHelper(context);
139         final UserManager userManager = UserUtils.getUserManager(context);
140         if (userManager != null &&
141                 !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)) {
142             syncEntriesFromPrimaryUser(userManager);
143         }
144 
145         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
146             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
147         }
148         return true;
149     }
150 
151     @VisibleForTesting
createCallLogInsertionHelper(final Context context)152     protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
153         return DefaultCallLogInsertionHelper.getInstance(context);
154     }
155 
156     @VisibleForTesting
getDatabaseHelper(final Context context)157     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
158         return ContactsDatabaseHelper.getInstance(context);
159     }
160 
161     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)162     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
163             String sortOrder) {
164         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
165         qb.setTables(Tables.CALLS);
166         qb.setProjectionMap(sCallsProjectionMap);
167         qb.setStrict(true);
168 
169         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
170         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
171 
172         final int match = sURIMatcher.match(uri);
173         switch (match) {
174             case CALLS:
175                 break;
176 
177             case CALLS_ID: {
178                 selectionBuilder.addClause(getEqualityClause(Calls._ID,
179                         parseCallIdFromUri(uri)));
180                 break;
181             }
182 
183             case CALLS_FILTER: {
184                 List<String> pathSegments = uri.getPathSegments();
185                 String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
186                 if (!TextUtils.isEmpty(phoneNumber)) {
187                     qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
188                     qb.appendWhereEscapeString(phoneNumber);
189                     qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
190                 } else {
191                     qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
192                             + Calls.PRESENTATION_ALLOWED);
193                 }
194                 break;
195             }
196 
197             default:
198                 throw new IllegalArgumentException("Unknown URL " + uri);
199         }
200 
201         final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
202         final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
203         String limitClause = null;
204         if (limit > 0) {
205             limitClause = offset + "," + limit;
206         }
207 
208         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
209         final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
210                 null, sortOrder, limitClause);
211         if (c != null) {
212             c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
213         }
214         return c;
215     }
216 
217     /**
218      * Gets an integer query parameter from a given uri.
219      *
220      * @param uri The uri to extract the query parameter from.
221      * @param key The query parameter key.
222      * @param defaultValue A default value to return if the query parameter does not exist.
223      * @return The value from the query parameter in the Uri.  Or the default value if the parameter
224      * does not exist in the uri.
225      * @throws IllegalArgumentException when the value in the query parameter is not an integer.
226      */
getIntParam(Uri uri, String key, int defaultValue)227     private int getIntParam(Uri uri, String key, int defaultValue) {
228         String valueString = uri.getQueryParameter(key);
229         if (valueString == null) {
230             return defaultValue;
231         }
232 
233         try {
234             return Integer.parseInt(valueString);
235         } catch (NumberFormatException e) {
236             String msg = "Integer required for " + key + " parameter but value '" + valueString +
237                     "' was found instead.";
238             throw new IllegalArgumentException(msg, e);
239         }
240     }
241 
242     @Override
getType(Uri uri)243     public String getType(Uri uri) {
244         int match = sURIMatcher.match(uri);
245         switch (match) {
246             case CALLS:
247                 return Calls.CONTENT_TYPE;
248             case CALLS_ID:
249                 return Calls.CONTENT_ITEM_TYPE;
250             case CALLS_FILTER:
251                 return Calls.CONTENT_TYPE;
252             default:
253                 throw new IllegalArgumentException("Unknown URI: " + uri);
254         }
255     }
256 
257     @Override
insert(Uri uri, ContentValues values)258     public Uri insert(Uri uri, ContentValues values) {
259         checkForSupportedColumns(sCallsProjectionMap, values);
260         // Inserting a voicemail record through call_log requires the voicemail
261         // permission and also requires the additional voicemail param set.
262         if (hasVoicemailValue(values)) {
263             checkIsAllowVoicemailRequest(uri);
264             mVoicemailPermissions.checkCallerHasWriteAccess();
265         }
266         if (mCallsInserter == null) {
267             SQLiteDatabase db = mDbHelper.getWritableDatabase();
268             mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
269         }
270 
271         ContentValues copiedValues = new ContentValues(values);
272 
273         // Add the computed fields to the copied values.
274         mCallLogInsertionHelper.addComputedValues(copiedValues);
275 
276         long rowId = getDatabaseModifier(mCallsInserter).insert(copiedValues);
277         if (rowId > 0) {
278             return ContentUris.withAppendedId(uri, rowId);
279         }
280         return null;
281     }
282 
283     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)284     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
285         checkForSupportedColumns(sCallsProjectionMap, values);
286         // Request that involves changing record type to voicemail requires the
287         // voicemail param set in the uri.
288         if (hasVoicemailValue(values)) {
289             checkIsAllowVoicemailRequest(uri);
290         }
291 
292         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
293         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
294 
295         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
296         final int matchedUriId = sURIMatcher.match(uri);
297         switch (matchedUriId) {
298             case CALLS:
299                 break;
300 
301             case CALLS_ID:
302                 selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
303                 break;
304 
305             default:
306                 throw new UnsupportedOperationException("Cannot update URL: " + uri);
307         }
308 
309         return getDatabaseModifier(db).update(Tables.CALLS, values, selectionBuilder.build(),
310                 selectionArgs);
311     }
312 
313     @Override
delete(Uri uri, String selection, String[] selectionArgs)314     public int delete(Uri uri, String selection, String[] selectionArgs) {
315         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
316         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
317 
318         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
319         final int matchedUriId = sURIMatcher.match(uri);
320         switch (matchedUriId) {
321             case CALLS:
322                 return getDatabaseModifier(db).delete(Tables.CALLS,
323                         selectionBuilder.build(), selectionArgs);
324             default:
325                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
326         }
327     }
328 
329     // Work around to let the test code override the context. getContext() is final so cannot be
330     // overridden.
context()331     protected Context context() {
332         return getContext();
333     }
334 
335     /**
336      * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
337      * after the operation is performed.
338      */
getDatabaseModifier(SQLiteDatabase db)339     private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
340         return new DbModifierWithNotification(Tables.CALLS, db, context());
341     }
342 
343     /**
344      * Same as {@link #getDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
345      * only.
346      */
getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper)347     private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
348         return new DbModifierWithNotification(Tables.CALLS, insertHelper, context());
349     }
350 
351     private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
hasVoicemailValue(ContentValues values)352     private boolean hasVoicemailValue(ContentValues values) {
353         return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
354     }
355 
356     /**
357      * Checks if the supplied uri requests to include voicemails and take appropriate
358      * action.
359      * <p> If voicemail is requested, then check for voicemail permissions. Otherwise
360      * modify the selection to restrict to non-voicemail entries only.
361      */
checkVoicemailPermissionAndAddRestriction(Uri uri, SelectionBuilder selectionBuilder, boolean isQuery)362     private void checkVoicemailPermissionAndAddRestriction(Uri uri,
363             SelectionBuilder selectionBuilder, boolean isQuery) {
364         if (isAllowVoicemailRequest(uri)) {
365             if (isQuery) {
366                 mVoicemailPermissions.checkCallerHasReadAccess();
367             } else {
368                 mVoicemailPermissions.checkCallerHasWriteAccess();
369             }
370         } else {
371             selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
372         }
373     }
374 
375     /**
376      * Determines if the supplied uri has the request to allow voicemails to be
377      * included.
378      */
isAllowVoicemailRequest(Uri uri)379     private boolean isAllowVoicemailRequest(Uri uri) {
380         return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
381     }
382 
383     /**
384      * Checks to ensure that the given uri has allow_voicemail set. Used by
385      * insert and update operations to check that ContentValues with voicemail
386      * call type must use the voicemail uri.
387      * @throws IllegalArgumentException if allow_voicemail is not set.
388      */
checkIsAllowVoicemailRequest(Uri uri)389     private void checkIsAllowVoicemailRequest(Uri uri) {
390         if (!isAllowVoicemailRequest(uri)) {
391             throw new IllegalArgumentException(
392                     String.format("Uri %s cannot be used for voicemail record." +
393                             " Please set '%s=true' in the uri.", uri,
394                             Calls.ALLOW_VOICEMAILS_PARAM_KEY));
395         }
396     }
397 
398    /**
399     * Parses the call Id from the given uri, assuming that this is a uri that
400     * matches CALLS_ID. For other uri types the behaviour is undefined.
401     * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
402     */
parseCallIdFromUri(Uri uri)403     private long parseCallIdFromUri(Uri uri) {
404         try {
405             return Long.parseLong(uri.getPathSegments().get(1));
406         } catch (NumberFormatException e) {
407             throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
408         }
409     }
410 
411     /**
412      * Syncs any unique call log entries that have been inserted into the primary user's call log
413      * since the last time the last sync occurred.
414      */
syncEntriesFromPrimaryUser(UserManager userManager)415     private void syncEntriesFromPrimaryUser(UserManager userManager) {
416         final int userHandle = userManager.getUserHandle();
417         if (userHandle == UserHandle.USER_OWNER
418                 || userManager.getUserInfo(userHandle).isManagedProfile()) {
419             return;
420         }
421 
422         final long lastSyncTime = getLastSyncTime();
423         final Uri uri = ContentProvider.maybeAddUserId(CallLog.Calls.CONTENT_URI,
424                 UserHandle.USER_OWNER);
425         final Cursor cursor = getContext().getContentResolver().query(
426                 uri,
427                 CALL_LOG_SYNC_PROJECTION,
428                 EXCLUDE_VOICEMAIL_SELECTION + " AND " + MORE_RECENT_THAN_SELECTION,
429                 new String[] {String.valueOf(lastSyncTime)},
430                 Calls.DATE + " DESC");
431         if (cursor == null) {
432             return;
433         }
434         try {
435             final long lastSyncedEntryTime = copyEntriesFromCursor(cursor);
436             if (lastSyncedEntryTime > lastSyncTime) {
437                 setLastTimeSynced(lastSyncedEntryTime);
438             }
439         } finally {
440             cursor.close();
441         }
442     }
443 
444     /**
445      * @param cursor to copy call log entries from
446      *
447      * @return the timestamp of the last synced entry.
448      */
449     @VisibleForTesting
copyEntriesFromCursor(Cursor cursor)450     long copyEntriesFromCursor(Cursor cursor) {
451         long lastSynced = 0;
452         final ContentValues values = new ContentValues();
453         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
454         db.beginTransaction();
455         try {
456             final String[] args = new String[2];
457             cursor.moveToPosition(-1);
458             while (cursor.moveToNext()) {
459                 values.clear();
460                 DatabaseUtils.cursorRowToContentValues(cursor, values);
461                 final String startTime = values.getAsString(Calls.DATE);
462                 final String number = values.getAsString(Calls.NUMBER);
463 
464                 if (startTime == null || number == null) {
465                     continue;
466                 }
467 
468                 if (cursor.isLast()) {
469                     try {
470                         lastSynced = Long.valueOf(startTime);
471                     } catch (NumberFormatException e) {
472                         Log.e(TAG, "Call log entry does not contain valid start time: "
473                                 + startTime);
474                     }
475                 }
476 
477                 // Avoid duplicating an already existing entry (which is uniquely identified by
478                 // the number, and the start time)
479                 args[0] = startTime;
480                 args[1] = number;
481                 if (DatabaseUtils.queryNumEntries(db, Tables.CALLS,
482                         Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) {
483                     continue;
484                 }
485 
486                 db.insert(Tables.CALLS, null, values);
487             }
488             db.setTransactionSuccessful();
489         } finally {
490             db.endTransaction();
491         }
492         return lastSynced;
493     }
494 
getLastSyncTime()495     private long getLastSyncTime() {
496         try {
497             return Long.valueOf(mDbHelper.getProperty(DbProperties.CALL_LOG_LAST_SYNCED, "0"));
498         } catch (NumberFormatException e) {
499             return 0;
500         }
501     }
502 
setLastTimeSynced(long time)503     private void setLastTimeSynced(long time) {
504         mDbHelper.setProperty(DbProperties.CALL_LOG_LAST_SYNCED, String.valueOf(time));
505     }
506 }
507