blob: e547e6da7830bf9d43e62c4c59bb0c637a5550e7 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.providers.contacts;
import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
import android.app.AppOpsManager;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ProviderAccessStats;
import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.UserUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* Call log content provider.
*/
public class CallLogProvider extends ContentProvider {
private static final String TAG = "CallLogProvider";
public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
private static final int BACKGROUND_TASK_INITIALIZE = 0;
private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
/** Selection clause for selecting all calls that were made after a certain time */
private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
/** Selection clause to use to exclude voicemail records. */
private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
Calls.TYPE, Calls.VOICEMAIL_TYPE);
/** Selection clause to exclude hidden records. */
private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause(
Calls.PHONE_ACCOUNT_HIDDEN, 0);
@VisibleForTesting
static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
Calls.NUMBER,
Calls.NUMBER_PRESENTATION,
Calls.TYPE,
Calls.FEATURES,
Calls.DATE,
Calls.DURATION,
Calls.DATA_USAGE,
Calls.PHONE_ACCOUNT_COMPONENT_NAME,
Calls.PHONE_ACCOUNT_ID,
Calls.ADD_FOR_ALL_USERS
};
static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID };
private static final int CALLS = 1;
private static final int CALLS_ID = 2;
private static final int CALLS_FILTER = 3;
private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY =
"UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;";
private static final String UNHIDE_BY_ADDRESS_QUERY =
"UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
Calls.PHONE_ACCOUNT_ADDRESS + "=?;";
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
// Shadow provider only supports "/calls".
sURIMatcher.addURI(CallLog.SHADOW_AUTHORITY, "calls", CALLS);
}
private static final ArrayMap<String, String> sCallsProjectionMap;
static {
// Calls projection map
sCallsProjectionMap = new ArrayMap<>();
sCallsProjectionMap.put(Calls._ID, Calls._ID);
sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
sCallsProjectionMap.put(Calls.POST_DIAL_DIGITS, Calls.POST_DIAL_DIGITS);
sCallsProjectionMap.put(Calls.VIA_NUMBER, Calls.VIA_NUMBER);
sCallsProjectionMap.put(Calls.NUMBER_PRESENTATION, Calls.NUMBER_PRESENTATION);
sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
sCallsProjectionMap.put(Calls.DATA_USAGE, Calls.DATA_USAGE);
sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE);
sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
sCallsProjectionMap.put(Calls.GEOCODED_LOCATION, Calls.GEOCODED_LOCATION);
sCallsProjectionMap.put(Calls.CACHED_LOOKUP_URI, Calls.CACHED_LOOKUP_URI);
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
sCallsProjectionMap.put(Calls.ADD_FOR_ALL_USERS, Calls.ADD_FOR_ALL_USERS);
sCallsProjectionMap.put(Calls.LAST_MODIFIED, Calls.LAST_MODIFIED);
sCallsProjectionMap
.put(Calls.CALL_SCREENING_COMPONENT_NAME, Calls.CALL_SCREENING_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.CALL_SCREENING_APP_NAME, Calls.CALL_SCREENING_APP_NAME);
sCallsProjectionMap.put(Calls.BLOCK_REASON, Calls.BLOCK_REASON);
sCallsProjectionMap.put(Calls.MISSED_REASON, Calls.MISSED_REASON);
}
private static final String ALLOWED_PACKAGE_FOR_TESTING = "com.android.providers.contacts";
@VisibleForTesting
static final String PARAM_KEY_QUERY_FOR_TESTING = "query_for_testing";
/**
* A long to override the clock used for timestamps, or "null" to reset to the system clock.
*/
@VisibleForTesting
static final String PARAM_KEY_SET_TIME_FOR_TESTING = "set_time_for_testing";
private static Long sTimeForTestMillis;
private ContactsTaskScheduler mTaskScheduler;
private volatile CountDownLatch mReadAccessLatch;
private CallLogDatabaseHelper mDbHelper;
private DatabaseUtils.InsertHelper mCallsInserter;
private boolean mUseStrictPhoneNumberComparation;
private int mMinMatch;
private VoicemailPermissions mVoicemailPermissions;
private CallLogInsertionHelper mCallLogInsertionHelper;
private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<>();
private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>();
private final ProviderAccessStats mStats = new ProviderAccessStats();
protected boolean isShadow() {
return false;
}
protected final String getProviderName() {
return this.getClass().getSimpleName();
}
@Override
public boolean onCreate() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
+ " user=" + android.os.Process.myUserHandle().getIdentifier());
}
setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start");
}
final Context context = getContext();
mDbHelper = getDatabaseHelper(context);
mUseStrictPhoneNumberComparation =
context.getResources().getBoolean(
com.android.internal.R.bool.config_use_strict_phone_number_comparation);
mMinMatch =
context.getResources().getInteger(
com.android.internal.R.integer.config_phonenumber_compare_min_match);
mVoicemailPermissions = new VoicemailPermissions(context);
mCallLogInsertionHelper = createCallLogInsertionHelper(context);
mReadAccessLatch = new CountDownLatch(1);
mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
@Override
public void onPerformTask(int taskId, Object arg) {
performBackgroundTask(taskId, arg);
}
};
mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish");
}
return true;
}
@VisibleForTesting
protected CallLogInsertionHelper createCallLogInsertionHelper(final Context context) {
return DefaultCallLogInsertionHelper.getInstance(context);
}
@VisibleForTesting
public void setMinMatchForTest(int minMatch) {
mMinMatch = minMatch;
}
@VisibleForTesting
public int getMinMatchForTest() {
return mMinMatch;
}
protected CallLogDatabaseHelper getDatabaseHelper(final Context context) {
return CallLogDatabaseHelper.getInstance(context);
}
protected boolean applyingBatch() {
final Boolean applying = mApplyingBatch.get();
return applying != null && applying;
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
final int callingUid = Binder.getCallingUid();
mCallingUid.set(callingUid);
mStats.incrementBatchStats(callingUid);
mApplyingBatch.set(true);
try {
return super.applyBatch(operations);
} finally {
mApplyingBatch.set(false);
mStats.finishOperation(callingUid);
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
final int callingUid = Binder.getCallingUid();
mCallingUid.set(callingUid);
mStats.incrementBatchStats(callingUid);
mApplyingBatch.set(true);
try {
return super.bulkInsert(uri, values);
} finally {
mApplyingBatch.set(false);
mStats.finishOperation(callingUid);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// Note don't use mCallingUid here. That's only used by mutation functions.
final int callingUid = Binder.getCallingUid();
mStats.incrementQueryStats(callingUid);
try {
return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
} finally {
mStats.finishOperation(callingUid);
}
}
private Cursor queryInternal(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
" CUID=" + Binder.getCallingUid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
queryForTesting(uri);
waitForAccess(mReadAccessLatch);
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(Tables.CALLS);
qb.setProjectionMap(sCallsProjectionMap);
qb.setStrict(true);
// If the caller doesn't have READ_VOICEMAIL, make sure they can't
// do any SQL shenanigans to get access to the voicemails. If the caller does have the
// READ_VOICEMAIL permission, then they have sufficient permissions to access any data in
// the database, so the strict check is unnecessary.
if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
qb.setStrictGrammar(true);
}
final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
final int match = sURIMatcher.match(uri);
switch (match) {
case CALLS:
break;
case CALLS_ID: {
selectionBuilder.addClause(getEqualityClause(Calls._ID,
parseCallIdFromUri(uri)));
break;
}
case CALLS_FILTER: {
List<String> pathSegments = uri.getPathSegments();
String phoneNumber = pathSegments.size() >= 2 ? pathSegments.get(2) : null;
if (!TextUtils.isEmpty(phoneNumber)) {
qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
qb.appendWhereEscapeString(phoneNumber);
qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)"
: ", 0, " + mMinMatch + ")");
} else {
qb.appendWhere(Calls.NUMBER_PRESENTATION + "!="
+ Calls.PRESENTATION_ALLOWED);
}
break;
}
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
final int limit = getIntParam(uri, Calls.LIMIT_PARAM_KEY, 0);
final int offset = getIntParam(uri, Calls.OFFSET_PARAM_KEY, 0);
String limitClause = null;
if (limit > 0) {
limitClause = offset + "," + limit;
}
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
final Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
null, sortOrder, limitClause);
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
}
return c;
}
private void queryForTesting(Uri uri) {
if (!uri.getBooleanQueryParameter(PARAM_KEY_QUERY_FOR_TESTING, false)) {
return;
}
if (!getCallingPackage().equals(ALLOWED_PACKAGE_FOR_TESTING)) {
throw new IllegalArgumentException("query_for_testing set from foreign package "
+ getCallingPackage());
}
String timeString = uri.getQueryParameter(PARAM_KEY_SET_TIME_FOR_TESTING);
if (timeString != null) {
if (timeString.equals("null")) {
sTimeForTestMillis = null;
} else {
sTimeForTestMillis = Long.parseLong(timeString);
}
}
}
@VisibleForTesting
static Long getTimeForTestMillis() {
return sTimeForTestMillis;
}
/**
* Gets an integer query parameter from a given uri.
*
* @param uri The uri to extract the query parameter from.
* @param key The query parameter key.
* @param defaultValue A default value to return if the query parameter does not exist.
* @return The value from the query parameter in the Uri. Or the default value if the parameter
* does not exist in the uri.
* @throws IllegalArgumentException when the value in the query parameter is not an integer.
*/
private int getIntParam(Uri uri, String key, int defaultValue) {
String valueString = uri.getQueryParameter(key);
if (valueString == null) {
return defaultValue;
}
try {
return Integer.parseInt(valueString);
} catch (NumberFormatException e) {
String msg = "Integer required for " + key + " parameter but value '" + valueString +
"' was found instead.";
throw new IllegalArgumentException(msg, e);
}
}
@Override
public String getType(Uri uri) {
int match = sURIMatcher.match(uri);
switch (match) {
case CALLS:
return Calls.CONTENT_TYPE;
case CALLS_ID:
return Calls.CONTENT_ITEM_TYPE;
case CALLS_FILTER:
return Calls.CONTENT_TYPE;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
final int callingUid =
applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
mStats.incrementInsertStats(callingUid, applyingBatch());
try {
return insertInternal(uri, values);
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final int callingUid =
applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
mStats.incrementInsertStats(callingUid, applyingBatch());
try {
return updateInternal(uri, values, selection, selectionArgs);
} finally {
mStats.finishOperation(callingUid);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final int callingUid =
applyingBatch() ? mCallingUid.get() : Binder.getCallingUid();
mStats.incrementInsertStats(callingUid, applyingBatch());
try {
return deleteInternal(uri, selection, selectionArgs);
} finally {
mStats.finishOperation(callingUid);
}
}
private Uri insertInternal(Uri uri, ContentValues values) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" +
" CPID=" + Binder.getCallingPid() +
" CUID=" + Binder.getCallingUid());
}
waitForAccess(mReadAccessLatch);
checkForSupportedColumns(sCallsProjectionMap, values);
// Inserting a voicemail record through call_log requires the voicemail
// permission and also requires the additional voicemail param set.
if (hasVoicemailValue(values)) {
checkIsAllowVoicemailRequest(uri);
mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
}
if (mCallsInserter == null) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
}
ContentValues copiedValues = new ContentValues(values);
// Add the computed fields to the copied values.
mCallLogInsertionHelper.addComputedValues(copiedValues);
long rowId = createDatabaseModifier(mCallsInserter).insert(copiedValues);
if (rowId > 0) {
return ContentUris.withAppendedId(uri, rowId);
}
return null;
}
private int updateInternal(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "update: uri=" + uri +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" values=[" + values + "] CPID=" + Binder.getCallingPid() +
" CUID=" + Binder.getCallingUid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
waitForAccess(mReadAccessLatch);
checkForSupportedColumns(sCallsProjectionMap, values);
// Request that involves changing record type to voicemail requires the
// voicemail param set in the uri.
if (hasVoicemailValue(values)) {
checkIsAllowVoicemailRequest(uri);
}
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(Tables.CALLS);
qb.setProjectionMap(sCallsProjectionMap);
qb.setStrict(true);
// If the caller doesn't have READ_VOICEMAIL, make sure they can't
// do any SQL shenanigans to get access to the voicemails. If the caller does have the
// READ_VOICEMAIL permission, then they have sufficient permissions to access any data in
// the database, so the strict check is unnecessary.
if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
qb.setStrictGrammar(true);
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
case CALLS:
break;
case CALLS_ID:
selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri)));
break;
default:
throw new UnsupportedOperationException("Cannot update URL: " + uri);
}
return qb.update(db, values, selectionBuilder.build(), selectionArgs);
}
private int deleteInternal(Uri uri, String selection, String[] selectionArgs) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "delete: uri=" + uri +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" CPID=" + Binder.getCallingPid() +
" CUID=" + Binder.getCallingUid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
waitForAccess(mReadAccessLatch);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(Tables.CALLS);
qb.setProjectionMap(sCallsProjectionMap);
qb.setStrict(true);
// If the caller doesn't have READ_VOICEMAIL, make sure they can't
// do any SQL shenanigans to get access to the voicemails. If the caller does have the
// READ_VOICEMAIL permission, then they have sufficient permissions to access any data in
// the database, so the strict check is unnecessary.
if (!mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
qb.setStrictGrammar(true);
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
case CALLS:
// TODO: Special case - We may want to forward the delete request on user 0 to the
// shadow provider too.
return qb.delete(db, selectionBuilder.build(), selectionArgs);
default:
throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
}
}
void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
}
/**
* Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
* after the operation is performed.
*/
private DatabaseModifier createDatabaseModifier(SQLiteDatabase db) {
return new DbModifierWithNotification(Tables.CALLS, db, getContext());
}
/**
* Same as {@link #createDatabaseModifier(SQLiteDatabase)} but used for insert helper operations
* only.
*/
private DatabaseModifier createDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
return new DbModifierWithNotification(Tables.CALLS, insertHelper, getContext());
}
private static final Integer VOICEMAIL_TYPE = new Integer(Calls.VOICEMAIL_TYPE);
private boolean hasVoicemailValue(ContentValues values) {
return VOICEMAIL_TYPE.equals(values.getAsInteger(Calls.TYPE));
}
/**
* Checks if the supplied uri requests to include voicemails and take appropriate
* action.
* <p> If voicemail is requested, then check for voicemail permissions. Otherwise
* modify the selection to restrict to non-voicemail entries only.
*/
private void checkVoicemailPermissionAndAddRestriction(Uri uri,
SelectionBuilder selectionBuilder, boolean isQuery) {
if (isAllowVoicemailRequest(uri)) {
if (isQuery) {
mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage());
} else {
mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
}
} else {
selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
}
}
/**
* Determines if the supplied uri has the request to allow voicemails to be
* included.
*/
private boolean isAllowVoicemailRequest(Uri uri) {
return uri.getBooleanQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, false);
}
/**
* Checks to ensure that the given uri has allow_voicemail set. Used by
* insert and update operations to check that ContentValues with voicemail
* call type must use the voicemail uri.
* @throws IllegalArgumentException if allow_voicemail is not set.
*/
private void checkIsAllowVoicemailRequest(Uri uri) {
if (!isAllowVoicemailRequest(uri)) {
throw new IllegalArgumentException(
String.format("Uri %s cannot be used for voicemail record." +
" Please set '%s=true' in the uri.", uri,
Calls.ALLOW_VOICEMAILS_PARAM_KEY));
}
}
/**
* Parses the call Id from the given uri, assuming that this is a uri that
* matches CALLS_ID. For other uri types the behaviour is undefined.
* @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
*/
private long parseCallIdFromUri(Uri uri) {
try {
return Long.parseLong(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
}
}
/**
* Sync all calllog entries that were inserted
*/
private void syncEntries() {
if (isShadow()) {
return; // It's the shadow provider itself. No copying.
}
final UserManager userManager = UserUtils.getUserManager(getContext());
// TODO: http://b/24944959
if (!Calls.shouldHaveSharedCallLogEntries(getContext(), userManager,
userManager.getUserHandle())) {
return;
}
final int myUserId = userManager.getUserHandle();
// See the comment in Calls.addCall() for the logic.
if (userManager.isSystemUser()) {
// If it's the system user, just copy from shadow.
syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ true,
/* forAllUsersOnly =*/ false);
} else {
// Otherwise, copy from system's real provider, as well as self's shadow.
syncEntriesFrom(UserHandle.USER_SYSTEM, /* sourceIsShadow = */ false,
/* forAllUsersOnly =*/ true);
syncEntriesFrom(myUserId, /* sourceIsShadow = */ true,
/* forAllUsersOnly =*/ false);
}
}
private void syncEntriesFrom(int sourceUserId, boolean sourceIsShadow,
boolean forAllUsersOnly) {
final Uri sourceUri = sourceIsShadow ? Calls.SHADOW_CONTENT_URI : Calls.CONTENT_URI;
final long lastSyncTime = getLastSyncTime(sourceIsShadow);
final Uri uri = ContentProvider.maybeAddUserId(sourceUri, sourceUserId);
final long newestTimeStamp;
final ContentResolver cr = getContext().getContentResolver();
final StringBuilder selection = new StringBuilder();
selection.append(
"(" + EXCLUDE_VOICEMAIL_SELECTION + ") AND (" + MORE_RECENT_THAN_SELECTION + ")");
if (forAllUsersOnly) {
selection.append(" AND (" + Calls.ADD_FOR_ALL_USERS + "=1)");
}
final Cursor cursor = cr.query(
uri,
CALL_LOG_SYNC_PROJECTION,
selection.toString(),
new String[] {String.valueOf(lastSyncTime)},
Calls.DATE + " ASC");
if (cursor == null) {
return;
}
try {
newestTimeStamp = copyEntriesFromCursor(cursor, lastSyncTime, sourceIsShadow);
} finally {
cursor.close();
}
if (sourceIsShadow) {
// delete all entries in shadow.
cr.delete(uri, Calls.DATE + "<= ?", new String[] {String.valueOf(newestTimeStamp)});
}
}
/**
* Un-hides any hidden call log entries that are associated with the specified handle.
*
* @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
*/
private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
String[] handleArgs =
new String[] { handle.getComponentName().flattenToString(), handle.getId() };
// Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
// update. If not, then try to identify the call from the phone number.
Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
handleArgs, null);
if (cursor != null) {
try {
if (cursor.getCount() >= 1) {
// run un-hiding process based on phone account
mDbHelper.getWritableDatabase().execSQL(
UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
} else {
TelecomManager tm = getContext().getSystemService(TelecomManager.class);
if (tm != null) {
PhoneAccount account = tm.getPhoneAccount(handle);
if (account != null && account.getAddress() != null) {
// We did not find any items for the specific phone account, so run the
// query based on the phone number instead.
mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
new String[] { account.getAddress().toString() });
}
}
}
} finally {
cursor.close();
}
}
}
/**
* @param cursor to copy call log entries from
*/
@VisibleForTesting
long copyEntriesFromCursor(Cursor cursor, long lastSyncTime, boolean forShadow) {
long latestTimestamp = 0;
final ContentValues values = new ContentValues();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
final String[] args = new String[2];
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
values.clear();
DatabaseUtils.cursorRowToContentValues(cursor, values);
final String startTime = values.getAsString(Calls.DATE);
final String number = values.getAsString(Calls.NUMBER);
if (startTime == null || number == null) {
continue;
}
if (cursor.isLast()) {
try {
latestTimestamp = Long.valueOf(startTime);
} catch (NumberFormatException e) {
Log.e(TAG, "Call log entry does not contain valid start time: "
+ startTime);
}
}
// Avoid duplicating an already existing entry (which is uniquely identified by
// the number, and the start time)
args[0] = startTime;
args[1] = number;
if (DatabaseUtils.queryNumEntries(db, Tables.CALLS,
Calls.DATE + " = ? AND " + Calls.NUMBER + " = ?", args) > 0) {
continue;
}
db.insert(Tables.CALLS, null, values);
}
if (latestTimestamp > lastSyncTime) {
setLastTimeSynced(latestTimestamp, forShadow);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return latestTimestamp;
}
private static String getLastSyncTimePropertyName(boolean forShadow) {
return forShadow
? DbProperties.CALL_LOG_LAST_SYNCED_FOR_SHADOW
: DbProperties.CALL_LOG_LAST_SYNCED;
}
@VisibleForTesting
long getLastSyncTime(boolean forShadow) {
try {
return Long.valueOf(mDbHelper.getProperty(getLastSyncTimePropertyName(forShadow), "0"));
} catch (NumberFormatException e) {
return 0;
}
}
private void setLastTimeSynced(long time, boolean forShadow) {
mDbHelper.setProperty(getLastSyncTimePropertyName(forShadow), String.valueOf(time));
}
private static void waitForAccess(CountDownLatch latch) {
if (latch == null) {
return;
}
while (true) {
try {
latch.await();
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void performBackgroundTask(int task, Object arg) {
if (task == BACKGROUND_TASK_INITIALIZE) {
try {
syncEntries();
} finally {
mReadAccessLatch.countDown();
mReadAccessLatch = null;
}
} else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
}
}
@Override
public void shutdown() {
mTaskScheduler.shutdownForTest();
}
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
mStats.dump(writer, " ");
}
}