Home | History | Annotate | Download | only in contacts
      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 {
     91         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
     92         sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
     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>();
    101         sCallsProjectionMap.put(Calls._ID, Calls._ID);
    102         sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
    103         sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
    104         sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
    105         sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
    106         sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
    107         sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
    108         sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
    109         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
    110         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
    111         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
    112         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
    113         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
    114         sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
    115         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
    116         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
    117         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
    118         sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
    119         sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
    120         sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
    121         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
    122         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
    123         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
    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
    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
    172     protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
    173         return DefaultCallLogInsertionHelper.getInstance(context);
    174     }
    175 
    176     @VisibleForTesting
    177     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
    178         return ContactsDatabaseHelper.getInstance(context);
    179     }
    180 
    181     @Override
    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      */
    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
    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
    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
    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
    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.
    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      */
    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      */
    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);
    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      */
    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      */
    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      */
    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     */
    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      */
    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
    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 
    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 
    527     private void setLastTimeSynced(long time) {
    528         mDbHelper.setProperty(DbProperties.CALL_LOG_LAST_SYNCED, String.valueOf(time));
    529     }
    530 
    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 
    546     private void scheduleBackgroundTask(int task) {
    547         mBackgroundHandler.sendEmptyMessage(task);
    548     }
    549 
    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