在拨打或接听来电时,都会在Dialer中看到新增的通话记录,前两天遇到了个通话记录中类型显示得不对的问题,就跟踪了下Android中CallLog的相关流程,在这记录下,以后查起来方便些,顺便分享给大家,欢迎指正错误,共同学习!
Android中电话分接来电(MT)和去电(MO),拨打电话流程可以看看《Android 6.0 Marshmallow 拨打电话流程》,这里就以来电或去电当call状态改变后,往数据库Contacts.db中插入通话记录开始分析
下面先来看看总体的时序图,再一步步介绍
在CallsManager.java中不管来电或去电,当挂断电话时会调用markCallAsDisconnected,在这个方法中会调用setCallState
void markCallAsDisconnected(Call call, DisconnectCause disconnectCause) {
call.setDisconnectCause(disconnectCause);
setCallState(call, CallState.DISCONNECTED, "disconnected set explicitly");
}
当然调用setCallState不止这一个方法,在
CallsManager中有多个地方调用,在onSuccessfulOutgoingCall,
markCallAsRinging,
markCallAsDialing等这些方法都有调用,下面主要看看setCallState这个方法
private void setCallState(Call call, int newState, String tag) {
if (call == null) {
return;
}
int oldState = call.getState();
Log.i(this, "setCallState %s -> %s, call: %s", CallState.toString(oldState),
CallState.toString(newState), call);
if (newState != oldState) {
// Unfortunately, in the telephony world the radio is king. So if the call notifies
// us that the call is in a particular state, we allow it even if it doesn't make
// sense (e.g., STATE_ACTIVE -> STATE_RINGING).
// TODO: Consider putting a stop to the above and turning CallState
// into a well-defined state machine.
// TODO: Define expected state transitions here, and log when an
// unexpected transition occurs.
call.setState(newState, tag);
maybeShowErrorDialogOnDisconnect(call);
Trace.beginSection("onCallStateChanged");
// Only broadcast state change for calls that are being tracked.
if (mCalls.contains(call)) {
updateCanAddCall();
for (CallsManagerListener listener : mListeners) {
if (Log.SYSTRACE_DEBUG) {
Trace.beginSection(listener.getClass().toString() + " onCallStateChanged");
}
listener.onCallStateChanged(call, oldState, newState);
if (Log.SYSTRACE_DEBUG) {
Trace.endSection();
}
}
}
Trace.endSection();
}
}
在这个方法中设置当前call的状态,然后调用CallsManagerListener的onCallStateChanged更新call的state,需要注意的是mListeners这个集合,定义如下
// For this set initial table size to 16 because we add 13 listeners in
// the CallsManager constructor.
private final Set mListeners = Collections.newSetFromMap(
new ConcurrentHashMap(16, 0.9f, 1));
这个集合初始化是在 CallsManager的构造方法中,在构造中往mListeners集合添加实现了CallsManagerListenerBase的类,另外CallsManagerListenerBase是实现CallsManager.CallsManagerListener这个接口的
//packages/services/Telecomm/src/com/android/server/telecom/CallsManager.java
CallsManager(
Context context,
TelecomSystem.SyncRoot lock,
ContactsAsyncHelper contactsAsyncHelper,
CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
MissedCallNotifier missedCallNotifier,
PhoneAccountRegistrar phoneAccountRegistrar,
HeadsetMediaButtonFactory headsetMediaButtonFactory,
ProximitySensorManagerFactory proximitySensorManagerFactory,
InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
CallAudioManager.AudioServiceFactory audioServiceFactory,
BluetoothManager bluetoothManager,
WiredHeadsetManager wiredHeadsetManager,
SystemStateProvider systemStateProvider,
DefaultDialerManagerAdapter defaultDialerAdapter,
Timeouts.Adapter timeoutsAdapter,
AsyncRingtonePlayer asyncRingtonePlayer,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
InterruptionFilterProxy interruptionFilterProxy) {
...
mListeners.add(mInCallWakeLockController);
mListeners.add(statusBarNotifier);
mListeners.add(mCallLogManager);
mListeners.add(mPhoneStateBroadcaster);
mListeners.add(mInCallController);
mListeners.add(mCallAudioManager);
mListeners.add(missedCallNotifier);
mListeners.add(mHeadsetMediaButton);
mListeners.add(mProximitySensorManager);
// There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
final UserManager userManager = UserManager.get(mContext);
// Don't load missed call if it is run in split user model.
if (userManager.isPrimaryUser()) {
onUserSwitch(Process.myUserHandle());
}
}
由于CallLogManager实现了CallsManagerListenerBase类,当调用listener.onCallStateChanged时,CallLogManager的onCallStateChanged方法也会被调用,下面就来看下这个类的定义
//packages/services/Telecomm/src/com/android/server/telecom/CallLogManager.java
public void onCallStateChanged(Call call, int oldState, int newState) {
int disconnectCause = call.getDisconnectCause().getCode();
boolean isNewlyDisconnected =
newState == CallState.DISCONNECTED || newState == CallState.ABORTED;
boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED;
// Log newly disconnected calls only if:
// 1) It was not in the "choose account" phase when disconnected
// 2) It is a conference call
// 3) Call was not explicitly canceled
// 4) Call is not an external call
if (isNewlyDisconnected &&
(oldState != CallState.SELECT_PHONE_ACCOUNT &&
!call.isConference() &&
!isCallCanceled) &&
!call.isExternalCall()) {
int type;
if (!call.isIncoming()) {
type = Calls.OUTGOING_TYPE;
} else if (disconnectCause == DisconnectCause.MISSED) {
type = Calls.MISSED_TYPE;
} else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) {
type = Calls.ANSWERED_EXTERNALLY_TYPE;
} else if (disconnectCause == DisconnectCause.REJECTED) {
type = Calls.REJECTED_TYPE;
} else {
type = Calls.INCOMING_TYPE;
}
logCall(call, type, true /*showNotificationForMissedCall*/);
}
}
这个方法主要判断了下当前call的状态,如果是disconnected的,然后根据DisconnectCause来确定call的type,Android原生定义了四种call type如下:
//frameworks/base/core/java/android/provider/CallLog.java
public static class Calls implements BaseColumns {
...
/** Call log type for incoming calls. */
public static final int INCOMING_TYPE = 1;
/** Call log type for outgoing calls. */
public static final int OUTGOING_TYPE = 2;
/** Call log type for missed calls. */
public static final int MISSED_TYPE = 3;
/** Call log type for voicemails. */
public static final int VOICEMAIL_TYPE = 4;
}
//packages/services/Telecomm/src/com/android/server/telecom/CallLogManager.java
private class LogCallAsyncTask extends AsyncTask {
private LogCallCompletedListener[] mListeners;
@Override
protected Uri[] doInBackground(AddCallArgs... callList) {
int count = callList.length;
Uri[] result = new Uri[count];
mListeners = new LogCallCompletedListener[count];
for (int i = 0; i < count; i++) {
AddCallArgs c = callList[i];
mListeners[i] = c.logCallCompletedListener;
try {
// May block.
result[i] = addCall(c);
} catch (Exception e) {
// This is very rare but may happen in legitimate cases.
// E.g. If the phone is encrypted and thus write request fails, it may cause
// some kind of Exception (right now it is IllegalArgumentException, but this
// might change).
//
// We don't want to crash the whole process just because of that, so just log
// it instead.
Log.e(TAG, e, "Exception raised during adding CallLog entry.");
result[i] = null;
}
}
return result;
}
private Uri addCall(AddCallArgs c) {
PhoneAccount phoneAccount = mPhoneAccountRegistrar
.getPhoneAccountUnchecked(c.accountHandle);
if (phoneAccount != null &&
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
if (c.initiatingUser != null &&
UserUtil.isManagedProfile(mContext, c.initiatingUser)) {
return addCall(c, c.initiatingUser);
} else {
return addCall(c, null);
}
} else {
return addCall(c, c.accountHandle == null ? null : c.accountHandle.getUserHandle());
}
}
/**
* Insert the call to a specific user or all users except managed profile.
* @param c context
* @param userToBeInserted user handle of user that the call going be inserted to. null
* if insert to all users except managed profile.
*/
private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) {
return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits, c.viaNumber,
c.presentation, c.callType, c.features, c.accountHandle, c.timestamp,
c.durationInSec, c.dataUsage, userToBeInserted == null,
userToBeInserted);
}
@Override
protected void onPostExecute(Uri[] result) {
for (int i = 0; i < result.length; i++) {
Uri uri = result[i];
/*
Performs a simple sanity check to make sure the call was written in the database.
Typically there is only one result per call so it is easy to identify which one
failed.
*/
if (uri == null) {
Log.w(TAG, "Failed to write call to the log.");
}
if (mListeners[i] != null) {
mListeners[i].onLogCompleted(uri);
}
}
}
}
在这个类中主要是调用addCall方法添加数据
//packages/services/Telecomm/src/com/android/server/telecom/CallLogManager.java
public static Uri addCall(CallerInfo ci, Context context, String number,
int presentation, int callType, int features, PhoneAccountHandle accountHandle,
long start, int duration, Long dataUsage, boolean addForAllUsers, boolean is_read) {
final ContentResolver resolver = context.getContentResolver();
...
ContentValues values = new ContentValues(6);
values.put(NUMBER, number);
values.put(NUMBER_PRESENTATION, Integer.valueOf(numberPresentation));
values.put(TYPE, Integer.valueOf(callType));
values.put(FEATURES, features);
values.put(DATE, Long.valueOf(start));
values.put(DURATION, Long.valueOf(duration));
if (dataUsage != null) {
values.put(DATA_USAGE, dataUsage);
}
values.put(PHONE_ACCOUNT_COMPONENT_NAME, accountComponentString);
values.put(PHONE_ACCOUNT_ID, accountId);
values.put(PHONE_ACCOUNT_ADDRESS, accountAddress);
values.put(NEW, Integer.valueOf(1));
if (callType == MISSED_TYPE) {
values.put(IS_READ, Integer.valueOf(is_read ? 1 : 0));
}
...
Uri result = null;
if (addForAllUsers) {
// Insert the entry for all currently running users, in order to trigger any
// ContentObservers currently set on the call log.
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
List users = userManager.getUsers(true);
final int currentUserId = userManager.getUserHandle();
final int count = users.size();
for (int i = 0; i < count; i++) {
final UserInfo user = users.get(i);
final UserHandle userHandle = user.getUserHandle();
if (userManager.isUserRunning(userHandle)
&& !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS,
userHandle)
&& !user.isManagedProfile()) {
Uri uri = addEntryAndRemoveExpiredEntries(context,
ContentProvider.maybeAddUserId(CONTENT_URI, user.id), values);
if (user.id == currentUserId) {
result = uri;
}
}
}
} else {
result = addEntryAndRemoveExpiredEntries(context, CONTENT_URI, values);
}
return result;
}
addEntryAndRemoveExpiredEntries
//packages/services/Telecomm/src/com/android/server/telecom/CallLogManager.java
private static Uri addEntryAndRemoveExpiredEntries(Context context, Uri uri,
ContentValues values) {
final ContentResolver resolver = context.getContentResolver();
Uri result = resolver.insert(uri, values);
resolver.delete(uri, "_id IN " +
"(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ " LIMIT -1 OFFSET 500)", null);
return result;
}
通过ContentResolver调用insert方法,上面用到CONTENT_URI定义如下:
//frameworks/base/core/java/android/provider/CallLog.java
public static final Uri CONTENT_URI =
Uri.parse("content://call_log/calls");
//packages/providers/ContactsProvider/src/com/android/providers/contacts/CallLogProvider.java
public Uri insert(Uri uri, ContentValues values) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" +
" CPID=" + Binder.getCallingPid());
}
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) {
// private CallLogDatabaseHelper mDbHelper;
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 = getDatabaseModifier(mCallsInserter).insert(copiedValues);
if (rowId > 0) {
return ContentUris.withAppendedId(uri, rowId);
}
return null;
}
getDatabaseModifier方法如下:
//packages/providers/ContactsProvider/src/com/android/providers/contacts/CallLogProvider.java
private DatabaseModifier getDatabaseModifier(DatabaseUtils.InsertHelper insertHelper) {
return new DbModifierWithNotification(Tables.CALLS, insertHelper, context());
}
//packages/providers/ContactsProvider/src/com/android/providers/contacts/DbModifierWithNotification.java
@Override
public long insert(ContentValues values) {
Set packagesModified = getModifiedPackages(values);
if (mIsCallsTable) {
values.put(Calls.LAST_MODIFIED, System.currentTimeMillis());
}
long rowId = mInsertHelper.insert(values);
if (rowId > 0 && packagesModified.size() != 0) {
notifyVoicemailChangeOnInsert(
ContentUris.withAppendedId(mBaseUri, rowId), packagesModified);
}
if (rowId > 0 && mIsCallsTable) {
notifyCallLogChange();
}
return rowId;
}
在notifyCallLogChange中通过发送广播CALL_LOG_CHANGE,通知通话记录发生改变
private void notifyCallLogChange() {
mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
Intent intent = new Intent("android.intent.action.CALL_LOG_CHANGE");
intent.setComponent(new ComponentName("com.android.calllogbackup",
"com.android.calllogbackup.CallLogChangeReceiver"));
if (!mContext.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
mContext.sendBroadcast(intent);
}
}
跟踪到这,来电或去电的通话记录就插入到数据库了,简单总结下分为两步:
下面附上Tables.CALLS的创表语句,当然你要学习DataBase的使用,ContactsDatabaseHelper和CallLogDatabaseHelper里有大量的创表语句,有需要的可以自己去下载Google源码ContactsProvider学习,具体下载方法见《Android 7.1.1源码下载》里面就是拿ContactsProvider举例的
//packages/providers/ContactsProvider/src/com/android/providers/contacts/CallLogDatabaseHelper.java
db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Calls.NUMBER + " TEXT," +
Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT " +
Calls.PRESENTATION_ALLOWED + "," +
Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," +
Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," +
Calls.DATE + " INTEGER," +
Calls.DURATION + " INTEGER," +
Calls.DATA_USAGE + " INTEGER," +
Calls.TYPE + " INTEGER," +
Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
Calls.PHONE_ACCOUNT_ID + " TEXT," +
Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
Calls.SUB_ID + " INTEGER DEFAULT -1," +
Calls.NEW + " INTEGER," +
Calls.CACHED_NAME + " TEXT," +
Calls.CACHED_NUMBER_TYPE + " INTEGER," +
Calls.CACHED_NUMBER_LABEL + " TEXT," +
Calls.COUNTRY_ISO + " TEXT," +
Calls.VOICEMAIL_URI + " TEXT," +
Calls.IS_READ + " INTEGER," +
Calls.GEOCODED_LOCATION + " TEXT," +
Calls.CACHED_LOOKUP_URI + " TEXT," +
Calls.CACHED_MATCHED_NUMBER + " TEXT," +
Calls.CACHED_NORMALIZED_NUMBER + " TEXT," +
Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," +
Calls.CACHED_PHOTO_URI + " TEXT," +
Calls.CACHED_FORMATTED_NUMBER + " TEXT," +
Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," +
Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," +
Voicemails._DATA + " TEXT," +
Voicemails.HAS_CONTENT + " INTEGER," +
Voicemails.MIME_TYPE + " TEXT," +
Voicemails.SOURCE_DATA + " TEXT," +
Voicemails.SOURCE_PACKAGE + " TEXT," +
Voicemails.TRANSCRIPTION + " TEXT," +
Voicemails.STATE + " INTEGER," +
Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0" +
");");