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