Android 6.0 通话记录生成保存和读取显示

这段时间在搞一个隐私联系人功能,没办法又重新梳理了一下通话记录的生成和展现过程,顺便贴出来跟大家分享一下。

这里写图片描述

一、通话记录的保存分为七步:

1、CallsManager.java 的构造函数中

// PhoneStateListener 监听电话是否断开连接
mListeners.add(mCallLogManager);

2、CallLogManager.java

// onCallStateChanged用于监听通话状态,设置状态 OUTGOING_TYPE、MISSED_TYPE和INCOMING_TYPE
public void onCallStateChanged(Call call, int oldState, int newState) {
logCall(call, type);
}

3、CallLogManager.java —> logCall()

// 插入一个通话记录

    void logCall(Call call, int callLogType) {
        final long creationTime = call.getCreationTimeMillis();
        final long age = call.getAgeMillis();
        final String logNumber = getLogNumber(call);
        Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
        final int presentation = getPresentation(call);
        final PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
        // TODO(vt): Once data usage is available, wire it up here.
        int callFeatures = getCallFeatures(call.getVideoStateHistory());
// 插入一个通话记录
        logCall(call.getCallerInfo(), logNumber, presentation, callLogType, callFeatures,
                accountHandle, creationTime, age, null);
    }

4、AddCallArgs —> AddCallArgs()

// CallLogAsync.AddCallArgs这个类即为管理增加通话记录的类;

 //    CallLogManager.java

    private void logCall(
            CallerInfo callerInfo,
            String number,
            int presentation,
            int callType,
            int features,
            PhoneAccountHandle accountHandle,
            long start,
            long duration,
            Long dataUsage) {
        boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(mContext, number);
        // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
        // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
        // on carrier requirements.)
        final boolean okToLogEmergencyNumber =
                mContext.getResources().getBoolean(R.bool.allow_emergency_numbers_in_call_log);
        // Don't log emergency numbers if the device doesn't allow it.
        final boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
        sendAddCallBroadcast(callType, duration);
        if (isOkToLogThisCall) {
            Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
                    + Log.pii(number) + "," + presentation + ", " + callType
                    + ", " + start + ", " + duration);
// CallLogAsync.AddCallArgs这个类即为管理增加通话记录的类;
            AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, presentation,
                    callType, features, accountHandle, start, duration, dataUsage);
            logCallAsync(args);
        } else {
          Log.d(TAG, "Not adding emergency call to call log.");
        }
    }
// AddCallArgs.java

    public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
        return new LogCallAsyncTask().execute(args);
    }
    /**
     * Helper AsyncTask to access the call logs database asynchronously since database operations
     * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
     * its own thread pool.
     */
    private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
        @Override
        protected Uri[] doInBackground(AddCallArgs... callList) {
            int count = callList.length;
            Uri[] result = new Uri[count];
            for (int i = 0; i < count; i++) {
                AddCallArgs c = callList[i];
                try {
                    // May block.
// 这个方法即为初始化的证据。插入的证据在这:   addCall(),即为插入DB的证据;
                    result[i] = Calls.addCall(c.callerInfo, c.context, c.number, c.presentation,
                            c.callType, c.features, c.accountHandle, c.timestamp, c.durationInSec,
                            c.dataUsage, true /* addForAllUsers */);
                } 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;
        }
        /**
         * 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.
         */
        @Override
        protected void onPostExecute(Uri[] result) {
            for (Uri uri : result) {
                if (uri == null) {
                    Log.w(TAG, "Failed to write call to the log.");
                }
            }
        }
    }

5、CallLog —> addCall()

// 这个方法即为初始化的证据。插入的证据在这: addCall(),即为插入DB的证据;
frameworks/base/core/java/android/provider/CallLog.java.

// CallLog.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) {
            final ContentResolver resolver = context.getContentResolver();
            int numberPresentation = PRESENTATION_ALLOWED;
            // Remap network specified number presentation types
            // PhoneConstants.PRESENTATION_xxx to calllog number presentation types
            // Calls.PRESENTATION_xxx, in order to insulate the persistent calllog
            // from any future radio changes.
            // If the number field is empty set the presentation type to Unknown.
            if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
                numberPresentation = PRESENTATION_RESTRICTED;
            } else if (presentation == PhoneConstants.PRESENTATION_PAYPHONE) {
                numberPresentation = PRESENTATION_PAYPHONE;
            } else if (TextUtils.isEmpty(number)
                    || presentation == PhoneConstants.PRESENTATION_UNKNOWN) {
                numberPresentation = PRESENTATION_UNKNOWN;
            }
            if (numberPresentation != PRESENTATION_ALLOWED) {
                number = "";
                if (ci != null) {
                    ci.name = "";
                }
            }
            // accountHandle information
            String accountComponentString = null;
            String accountId = null;
            if (accountHandle != null) {
                accountComponentString = accountHandle.getComponentName().flattenToString();
                accountId = accountHandle.getId();
            }
            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(NEW, Integer.valueOf(1));
            if (callType == MISSED_TYPE) {
                values.put(IS_READ, Integer.valueOf(0));
            }
            if (ci != null) {
                values.put(CACHED_NAME, ci.name);
                values.put(CACHED_NUMBER_TYPE, ci.numberType);
                values.put(CACHED_NUMBER_LABEL, ci.numberLabel);
            }
            if ((ci != null) && (ci.contactIdOrZero > 0)) {
                // Update usage information for the number associated with the contact ID.
                // We need to use both the number and the ID for obtaining a data ID since other
                // contacts may have the same number.
                final Cursor cursor;
                // We should prefer normalized one (probably coming from
                // Phone.NORMALIZED_NUMBER column) first. If it isn't available try others.
                if (ci.normalizedNumber != null) {
                    final String normalizedPhoneNumber = ci.normalizedNumber;
                    cursor = resolver.query(Phone.CONTENT_URI,
                            new String[] { Phone._ID },
                            Phone.CONTACT_ID + " =? AND " + Phone.NORMALIZED_NUMBER + " =?",
                            new String[] { String.valueOf(ci.contactIdOrZero),
                                    normalizedPhoneNumber},
                            null);
                } else {
                    final String phoneNumber = ci.phoneNumber != null ? ci.phoneNumber : number;
                    cursor = resolver.query(
                            Uri.withAppendedPath(Callable.CONTENT_FILTER_URI,
                                    Uri.encode(phoneNumber)),
                            new String[] { Phone._ID },
                            Phone.CONTACT_ID + " =?",
                            new String[] { String.valueOf(ci.contactIdOrZero) },
                            null);
                }
// 更新通话记录操作
                if (cursor != null) {
                    try {
                        if (cursor.getCount() > 0 && cursor.moveToFirst()) {
                            final String dataId = cursor.getString(0);
                            updateDataUsageStatForData(resolver, dataId);
                            if (duration >= MIN_DURATION_FOR_NORMALIZED_NUMBER_UPDATE_MS
                                    && callType == Calls.OUTGOING_TYPE
                                    && TextUtils.isEmpty(ci.normalizedNumber)) {
                                updateNormalizedNumber(context, resolver, dataId, number);
                            }
                        }
                    } finally {
                        cursor.close();
                    }
                }
            }
            /// M: new feature:IP dial enhancement start @{
            String ipPrefix = null;
            ipPrefix = Settings.System.getString(resolver, "ipprefix" + accountId);
            if (null != ipPrefix && null != number && number.startsWith(ipPrefix)
                    && !number.equals(ipPrefix) && callType == Calls.OUTGOING_TYPE) {
                values.put(IP_PREFIX, ipPrefix);
                String tmpNumber = number.substring(ipPrefix.length(), number.length());
                values.put(NUMBER, tmpNumber);
            }
            /// @}
            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<UserInfo> 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;
        }

6、CallLog —> addEntryAndRemoveExpiredEntries()

// 插入通话记录操作

// CallLog.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;
        }

7、CallLog —> resolver.insert(uri, values)

// 通话记录插入操作

二、通话记录的显示总共25步

这里写图片描述

1、CallLogFragment —>onResume()

// 每次切换导航栏、切换语言、点击Home键都会重新刷新通话记录

// CallLogFragment.java
    @Override
    public void onResume() {
        super.onResume();
// 用于查询数据库
        refreshData();
    }

2、CallLogFragment —>refreshData()

// 申请更新数据用于显示

 // CallLogFragment.java 
// 申请更新数据用于显示
    private void refreshData() {
// 是否需要刷新数据
        if (mRefreshDataRequired) {
/* 在接触信息缓存中的所有条目都会被记录出来,这样他们就会被人看到
再次显示一次。*/
            mAdapter.invalidateCache();

            startCallsQuery();
            startVoicemailStatusQuery();
            updateOnEntry();
            mRefreshDataRequired = false;
        /// M: for ALPS01772987 @{
        // need to update data without re-query
        } else {
            mAdapter.notifyDataSetChanged();
        }
        /// @}
        if (mNeedAccountFilter) {
            updateNotice();
        }
    }

3、CallLogAdapter—>invalidateCache()

/* 在接触信息缓存中的所有条目都会被记录出来,这样他们就会被人看到
再次显示一次。*/

// CallLogAdapter.java 
public void invalidateCache() {
        mContactInfoCache.expireAll();
        // Restart the request-processing thread after the next draw.
        stopRequestProcessing();
        unregisterPreDrawListener();
    }

4、CallLogFragment —>startCallsQuery()

// 读取sim卡过滤设置、通话类型设置(来电?去电?未接?全部? ),开始查询

//  CallLogFragment.java 
//   读取sim卡过滤设置、通话类型设置,开始查询
public void startCallsQuery() {
// 正在加载联系人,此时联系人列表不显示为 空
        mAdapter.setLoading(true);
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
// 通话类型:来电?去电?未接?全部? 
        mCallTypeFilter = prefs.getInt(Constants.TYPE_FILTER_PREF, CallLogQueryHandler.CALL_TYPE_ALL);

        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit, getAccountFilterId());
    }

5、CallLogAdapter —>setLoading(true)

// 正在加载联系人,此时联系人列表不显示为 空

//   CallLogAdapter.java 
public void setLoading(boolean loading) {
        mLoading = loading;
    }

6、CallLogQueryHandler —>fetchCalls()

// 获取列表中的通话记录

// CallLogQueryHandler.java
public void fetchCalls(int callType, long newerThan, String accountId) {
        cancelFetch();
        fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan, accountId);
    }

注:一步一步将添加查询条件,将查询请求提交给ContentProvider。

7、NoNullCursorAsyncQueryHandler—>startQuery

// 设置查询Uri,

// CallLogQueryHandler.java

    /** Fetches the list of calls in the call log. */
    private void fetchCalls(int token, int callType, boolean newOnly,
            long newerThan, String accountId) {
        // We need to check for NULL explicitly otherwise entries with where READ is NULL
        // may not match either the query or its negation.
        // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
        StringBuilder where = new StringBuilder();
        List<String> selectionArgs = Lists.newArrayList();
        if (newOnly) {
            where.append(Calls.NEW);
            where.append(" = 1");
        }
        if (callType > CALL_TYPE_ALL) {
            if (where.length() > 0) {
                where.append(" AND ");
            }
            // Add a clause to fetch only items of type voicemail.
            where.append(String.format("(%s = ?)", Calls.TYPE));
            // Add a clause to fetch only items newer than the requested date
            selectionArgs.add(Integer.toString(callType));
        }
        if (newerThan > 0) {
            if (where.length() > 0) {
                where.append(" AND ");
            }
            where.append(String.format("(%s > ?)", Calls.DATE));
            selectionArgs.add(Long.toString(newerThan));
        }
        if (!PhoneAccountInfoHelper.FILTER_ALL_ACCOUNT_ID.equals(accountId)) {
            if (where.length() > 0) {
                where.append(" AND ");
            }
            // query the Call Log by account id
            where.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID));
            selectionArgs.add(accountId);
        }
        /// M: for Plug-in @{
        ExtensionManager.getInstance().getCallLogExtension().appendQuerySelection(callType, where, selectionArgs);
        /// @}
        final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
        final String selection = where.length() > 0 ? where.toString() : null;
        Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
                .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
                .build();
        /// M: [Union Query] MTK CallLog Query @{
        if (DialerFeatureOptions.CALL_LOG_UNION_QUERY) {
            // change CallLog query data source to calls join data view
            uri = Uri.parse("content://call_log/callsjoindataview").buildUpon()
                    .appendQueryParameter(Calls.ALLOW_VOICEMAILS_PARAM_KEY, "true")
                    .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
                    .build();
        }
        LogUtils.d(TAG, "fetchCalls(), queryUri = " + uri.toString() + ", selection = " + selection
                + ", selectionArgs = " + selectionArgs);
        /// @}
// 设置查询Uri,
        startQuery(token, null, uri,
                CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
                Calls.DEFAULT_SORT_ORDER);
    }
// NoNullCursorAsyncQueryHandler.java
    @Override
    public void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection,
            String[] selectionArgs, String orderBy) {
        final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection);
        super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs,
                orderBy);
    }

注:CallLogQueryHandlerEx、NoNullCursorAsyncQueryHandler抽象类、AsyncQueryHandler抽象类是继承关系,继承自Handler

9、AsyncQueryHandler—>super.startQuery

//AsyncQueryHandler是Framework中提供的异步查询类,开始一个异步查询
定义在\frameworks\base\core\java\android\content,step10将查询请求交给它

//开始一个异步查询
public void startQuery(int token, Object cookie, Uri uri,
            String[] projection, String selection, String[] selectionArgs,
            String orderBy) {
        // Use the token as what so cancelOperations works properly
//mWorkerThreadHandler是WorkerHandler的对象,也是一个Handler,与工作线程通信  
        Message msg = mWorkerThreadHandler.obtainMessage(token);
        msg.arg1 = EVENT_ARG_QUERY;
        WorkerArgs args = new WorkerArgs();
//this即AsyncQueryHandler,用于工作线程返回查询结果Cursor  
        args.handler = this;
        args.uri = uri;
        args.projection = projection;
        args.selection = selection;
        args.selectionArgs = selectionArgs;
        args.orderBy = orderBy;
        args.cookie = cookie;
        msg.obj = args;
//查询将在工作线程中进行 
        mWorkerThreadHandler.sendMessage(msg);
    }

10、WorkerHandler—>mWorkerThreadHandler.sendMessage(msg)

//mWorkerThreadHandler是WorkerHandler的对象,也是一个Handler,与工作线程通信查询将在工作线程中进行
注:11~14步 工作线程将查询结果返回给AsyncQueryHandler的handleMessage()处理。

11、WorkerHandler—>handleMessage()

// 工作线程将查询结果返回给AsyncQueryHandler的handleMessage()处理。

12、WorkerHandler—>resolver.query()

// 使用query查询获得cursor,

13、WorkerHandler—>reply.sendToTarget()

1)将查询的cursor 赋值 args.result = cursor;
2)//args.handler就是上文提到的this ,this即AsyncQueryHandler,用于工作线程返回查询结果Cursor
Message reply = args.handler.obtainMessage(token);
// 发送message
reply.sendToTarget();

// AsyncQueryHandler.java
    protected class WorkerHandler extends Handler {
        public WorkerHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            final ContentResolver resolver = mResolver.get();
            if (resolver == null) return;
            WorkerArgs args = (WorkerArgs) msg.obj;
            int token = msg.what;
            int event = msg.arg1;
            switch (event) {
                case EVENT_ARG_QUERY:
                    Cursor cursor;
                    try {
/*
selection表示查找的列,
selectionArgs为where条件
*/
                        cursor = resolver.query(args.uri, args.projection,
                                args.selection, args.selectionArgs,
                                args.orderBy);
                        // Calling getCount() causes the cursor window to be filled,
                        // which will make the first access on the main thread a lot faster.
                        if (cursor != null) {
                            cursor.getCount();
                        }
                    } catch (Exception e) {
                        Log.w(TAG, "Exception thrown during handling EVENT_ARG_QUERY", e);
                        cursor = null;
                    }
//查询结果cursor  
                    args.result = cursor;
                    break;
            }
            // passing the original token value back to the caller
            // on top of the event values in arg1.
//args.handler就是上文提到的this 
            Message reply = args.handler.obtainMessage(token);
            reply.obj = args;
            reply.arg1 = msg.arg1;
            if (localLOGV) {
                Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1
                        + ", reply.what=" + reply.what);
            }
/*
你好,Message.sendToTarget() ---> Message.target.sendMessage(), 这里target是个Handler,所以你说的消息是发送到这个Message内部持有的Handler对象,加入他的MessageQueue。所以你要找到赋给该Message Handler的地方,在它的handleMessage里就可以看到处理了。至于在什么地方赋的值,可以看在什么地方调用了Message.obtain(Hanlder)或者Handler.obtainMessage()这类的函数
*/

            reply.sendToTarget();
        }
    }

14、NoNullCursorAsyncQueryHandler—>onQueryComplete()

//通话记录为空,创建一个空的cursor返回
查询完成,返回cursor,判断cursor是否为空。

// NoNullCursorAsyncQueryHandler.java
    @Override
    protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
        CookieWithProjection projectionCookie = (CookieWithProjection) cookie;
        super.onQueryComplete(token, projectionCookie.originalCookie, cursor);
//通话记录为空,创建一个空的cursor返回  
        if (cursor == null) {
            cursor = new EmptyCursor(projectionCookie.projection);
        }
        onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);
    }

15、CallLogQueryHandler—>onNotNullableQueryComplete()

1)、QUERY_CALLLOG_TOKEN 查询的标记,以从调用通话记录中获取旧的条目
2)、QUERY_SEARCH_TOKEN 用于通话记录全局搜索

// CallLogQueryHandler.java    
@Override  
protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
        if (cursor == null) {
            return;
        }
        try {
            /// M: [CallLog Search] For call log global search. @{
            /*
1)、QUERY_CALLLOG_TOKEN 查询的标记,以从调用通话记录中获取旧的条目
2)、QUERY_SEARCH_TOKEN 用于通话记录全局搜索
            */

            if (token == QUERY_CALLLOG_TOKEN || token == QUERY_SEARCH_TOKEN) {
            /// @}
                if (updateAdapterData(cursor)) {
                    cursor = null;
                }
            } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
                updateVoicemailStatus(cursor);
            } else {
                Log.w(TAG, "Unknown query completed: ignoring: " + token);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

16、CallLogQueryHandler—>updateAdapterData()

// 更新adapter 在calllogfragment中用于显示更新后的数据

// 更新adapter 在calllogfragment中用于显示更新后的数据
    private boolean updateAdapterData(Cursor cursor) {
        final Listener listener = mListener.get();
        if (listener != null) {
            return listener.onCallsFetched(cursor);
        }
        return false;
    }

17、CallLogFragment—>listener.onCallsFetched()

// 当通话记录列表被获取或者更新时被CallLogQueryHandler 调用,将结果cursor返回给CallLogFragment。

//   CallLogFragment.java
// 被CallLogQueryHandler 调用当通话记录列表被获取或者更新时
    @Override
    public boolean onCallsFetched(Cursor cursor) {
// 联系人加载完毕,此时通话记录显示更新后的联系人
        mAdapter.setLoading(false);
//更改CallLogListAdapter的cursor,刷新ListView
        mAdapter.changeCursor(cursor);
        // This will update the state of the "Clear call log" menu item.
        getActivity().invalidateOptionsMenu();
        if (mScrollToTop) {
            final ListView listView = getListView();

            if (listView.getFirstVisiblePosition() > 5) {
                listView.setSelection(5);
            }
            // Workaround for framework issue: the smooth-scroll doesn't
            // occur if setSelection() is called immediately before.
            mHandler.post(new Runnable() {
               @Override
               public void run() {
                   if (getActivity() == null || getActivity().isFinishing()) {
                       return;
                   }
                   listView.smoothScrollToPosition(0);
               }
            });
            mScrollToTop = false;
        }
        mCallLogFetched = true;
        destroyEmptyLoaderIfAllDataFetched();
        return true;
    }

注:step18~step19,将结果cursor返回给CallLogFragment。

18、CallLogFragment—>mAdapter.setLoading(false)

// 联系人加载完毕,此时通话记录显示更新后的联系人

19、CallLogAdapter—>mAdapter.changeCursor()

//更改CallLogListAdapter的cursor,刷新ListView

20~24、主要是处理通话记录的分组显示。

20、GroupingListAdapter—>super.changeCursor

// GroupingListAdapter.java
public void changeCursor(Cursor cursor) {
        if (cursor == mCursor) {
            return;
        }
        if (mCursor != null) {
            mCursor.unregisterContentObserver(mChangeObserver);
            mCursor.unregisterDataSetObserver(mDataSetObserver);
            mCursor.close();
        }
        mCursor = cursor;
        resetCache();
        findGroups();
        if (cursor != null) {
            cursor.registerContentObserver(mChangeObserver);
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
            notifyDataSetChanged();
        } else {
            // notify the observers about the lack of a data set
            notifyDataSetInvalidated();
        }
    }

21、CallLogAdapter—>addGroups()

//  CallLogAdapter.java 
@Override 
protected void addGroups(Cursor cursor) {
        mCallLogGroupBuilder.addGroups(cursor);
    }

22、GroupingListAdapter—>findGroups()

设置mGroupMetadata初始化大小16,用于记录一个Group(ListView的一个item)的开始位置和大小(包含的通话记录数目)

// GroupingListAdapter.java
    private void findGroups() {
        mGroupCount = 0;
        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
        if (mCursor == null) {
            return;
        }
        addGroups(mCursor);
    }

23、CallLogGroupBuilder—>mCallLogGroupBuilder.addGroups(cursor)()

具体的分组规则和分组过程

// CallLogGroupBuilder.java
  public void addGroups(Cursor cursor) {
        final int count = cursor.getCount();
        if (count == 0) {
            return;
        }
        // Clear any previous day grouping information.
        mGroupCreator.clearDayGroups();
        // Get current system time, used for calculating which day group calls belong to.
        long currentTime = System.currentTimeMillis();
        int currentGroupSize = 1;
        cursor.moveToFirst();
        // The number of the first entry in the group.
        String firstNumber = cursor.getString(CallLogQuery.NUMBER);
        // This is the type of the first call in the group.
        int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
        // The account information of the first entry in the group.
        String firstAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
        String firstAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
        // Determine the day group for the first call in the cursor.
        final long firstDate = cursor.getLong(CallLogQuery.DATE);
        final long firstRowId = cursor.getLong(CallLogQuery.ID);
        int currentGroupDayGroup = getDayGroup(firstDate, currentTime);
        mGroupCreator.setDayGroup(firstRowId, currentGroupDayGroup);
        while (cursor.moveToNext()) {
            // The number of the current row in the cursor.
            final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
            final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
            final String currentAccountComponentName = cursor.getString(
                    CallLogQuery.ACCOUNT_COMPONENT_NAME);
            final String currentAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
            final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
            final boolean sameAccountComponentName = Objects.equals(
                    firstAccountComponentName,
                    currentAccountComponentName);
            final boolean sameAccountId = Objects.equals(
                    firstAccountId,
                    currentAccountId);
            final boolean sameAccount = sameAccountComponentName && sameAccountId;
            final boolean shouldGroup;
            final long currentCallId = cursor.getLong(CallLogQuery.ID);
            final long date = cursor.getLong(CallLogQuery.DATE);
            if (!sameNumber || !sameAccount) {
                // Should only group with calls from the same number.
                shouldGroup = false;
            } else if (firstCallType == Calls.VOICEMAIL_TYPE) {
                // never group voicemail.
                shouldGroup = false;
            } else {
                // Incoming, outgoing, and missed calls group together.
                shouldGroup = callType != Calls.VOICEMAIL_TYPE;
            }
            if (shouldGroup) {
                // Increment the size of the group to include the current call, but do not create
                // the group until we find a call that does not match.
                currentGroupSize++;
            } else {
                // The call group has changed, so determine the day group for the new call group.
                // This ensures all calls grouped together in the call log are assigned the same
                // day group.
                currentGroupDayGroup = getDayGroup(date, currentTime);
                // Create a group for the previous set of calls, excluding the current one, but do
                // not create a group for a single call.
                if (currentGroupSize > 1) {
                    addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
                }
                // Start a new group; it will include at least the current call.
                currentGroupSize = 1;
                // The current entry is now the first in the group.
                firstNumber = currentNumber;
                firstCallType = callType;
                firstAccountComponentName = currentAccountComponentName;
                firstAccountId = currentAccountId;
            }
            // Save the day group associated with the current call.
            mGroupCreator.setDayGroup(currentCallId, currentGroupDayGroup);
        }
        // If the last set of calls at the end of the call log was itself a group, create it now.
        if (currentGroupSize > 1) {
            addGroup(count - currentGroupSize, currentGroupSize);
        }
    }

24、CallLogGroupBuilder—>mGroupCreator.setDayGroup()

用于跟踪每一个呼叫所属的天组,同一天组作为第一次呼叫组,分为今天,昨天,上周,和其他几种分组,在setDayGroup()设置getDayGroup()中获取

25、CallLogAdapter —> bindView()

通话记录ListView和Adapter的数据绑定是在GroupingListAdapter中的getView()方法中,具体实现放在CallLogAdapter的bindView()实现,最后,将通话记录加载到列表中

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值