Android 7.1.1 通话记录数据库详解

      在拨打或接听来电时,都会在Dialer中看到新增的通话记录,前两天遇到了个通话记录中类型显示得不对的问题,就跟踪了下Android中CallLog的相关流程,在这记录下,以后查起来方便些,顺便分享给大家,欢迎指正错误,共同学习!

   Android中电话分接来电(MT)和去电(MO),拨打电话流程可以看看《Android 6.0 Marshmallow 拨打电话流程》,这里就以来电或去电当call状态改变后,往数据库Contacts.db中插入通话记录开始分析

下面先来看看总体的时序图,再一步步介绍

Android 7.1.1 通话记录数据库详解_第1张图片

1、CallsManager.java

在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());
        }
    }

2、CallLogManager.java

由于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;
}

3、LogCallAsyncTask

当确定type之后,就调用本类的logCall(),接着调用logCallAsync利用LogCallAsyncTask向数据库里添加数据,在doInBackground里调用android.provider.CallLog.Calls.addCall方法,添加通话记录

//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);
                }
            }
        }
    }

4、CallLog

在这个类中主要是调用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");

5、CallLogProvider

CallLogProvider是ContentProvider的子类,在insert方法中通过CallLogDatabaseHelper的getWritableDatabase获取db,利用系统工具类DatabaseUtils.InsertHelper获取DatabaseUtils.InsertHelper的对象mCallsInserter
,再调用getDatabaseModifier获取DbModifierWithNotification对象,并调用insert方法插入到Tables.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());
    }

6、DbModifierWithNotification

在DbModifierWithNotification的insert方法中,当插入数据成功后会调用notifyCallLogChange,通知calllog发生改变

//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);
        }
    }
跟踪到这,来电或去电的通话记录就插入到数据库了,简单总结下分为两步:
1、当调用CallsManager的setCallState之后,就会调用到CallLogManager的onCallStateChanged。
2、在onCallStateChanged判断当前call的状态来决定是否需要写入数据库中,剩下的就是ContentResolver插入数据的操作了。
需要注意的是在这里Adnroid 7.0以前保存通话记录的表是保存在contacts2.db的CALLS表里的,在最新版本的android系统上Google提供CallLogDatabaseHelper.java类,专门用来管理CallLogProvider和VoicemailContentProvider,在这个类中创建了calllog.db这个数据库,用来保存通话记录。

下面附上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" +
                    ");");

你可能感兴趣的:(Android源码分析)