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