Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f78a5907 authored by Flavio Lerda's avatar Flavio Lerda
Browse files

Update the call log contact info cache.

Now that we use the call log cache again, we should also update it when
we find more up-to-date information when looking up in the contacts
database.

Bug: 5101753
Change-Id: I408c1d5c8ec3752d5c15e761274eb06fb9242de6
parent e39934d5
Loading
Loading
Loading
Loading
+88 −11
Original line number Original line Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.contacts.R;
import com.android.contacts.util.ExpirableCache;
import com.android.contacts.util.ExpirableCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.annotations.VisibleForTesting;


import android.content.ContentValues;
import android.content.Context;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.Cursor;
@@ -37,6 +38,7 @@ import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.PhoneNumberUtils;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;
@@ -47,7 +49,7 @@ import java.util.LinkedList;
/**
/**
 * Adapter class to fill in data for the Call Log.
 * Adapter class to fill in data for the Call Log.
 */
 */
public final class CallLogAdapter extends GroupingListAdapter
public class CallLogAdapter extends GroupingListAdapter
        implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
        implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
    /** Interface used to initiate a refresh of the content. */
    /** Interface used to initiate a refresh of the content. */
    public interface CallFetcher {
    public interface CallFetcher {
@@ -75,10 +77,13 @@ public final class CallLogAdapter extends GroupingListAdapter
    /**
    /**
     * List of requests to update contact details.
     * List of requests to update contact details.
     * <p>
     * <p>
     * Each request is made of a phone number to look up, and the contact info currently stored in
     * the call log for this number.
     * <p>
     * The requests are added when displaying the contacts and are processed by a background
     * The requests are added when displaying the contacts and are processed by a background
     * thread.
     * thread.
     */
     */
    private final LinkedList<String> mRequests;
    private final LinkedList<Pair<String, ContactInfo>> mRequests;


    private volatile boolean mDone;
    private volatile boolean mDone;
    private boolean mLoading = true;
    private boolean mLoading = true;
@@ -155,7 +160,7 @@ public final class CallLogAdapter extends GroupingListAdapter
        mCallFetcher = callFetcher;
        mCallFetcher = callFetcher;


        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
        mRequests = new LinkedList<String>();
        mRequests = new LinkedList<Pair<String,ContactInfo>>();
        mPreDrawListener = null;
        mPreDrawListener = null;


        Resources resources = mContext.getResources();
        Resources resources = mContext.getResources();
@@ -230,10 +235,21 @@ public final class CallLogAdapter extends GroupingListAdapter
        mPreDrawListener = null;
        mPreDrawListener = null;
    }
    }


    private void enqueueRequest(String number, boolean immediate) {
    /**
     * Enqueues a request to look up the contact details for the given phone number.
     * <p>
     * It also provides the current contact info stored in the call log for this number.
     * <p>
     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
     * up the contact information (if it has not been already started). Otherwise, it will be
     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
     */
    @VisibleForTesting
    void enqueueRequest(String number, ContactInfo callLogInfo, boolean immediate) {
        Pair<String, ContactInfo> request = new Pair<String, ContactInfo>(number, callLogInfo);
        synchronized (mRequests) {
        synchronized (mRequests) {
            if (!mRequests.contains(number)) {
            if (!mRequests.contains(request)) {
                mRequests.add(number);
                mRequests.add(request);
                mRequests.notifyAll();
                mRequests.notifyAll();
            }
            }
        }
        }
@@ -378,12 +394,15 @@ public final class CallLogAdapter extends GroupingListAdapter
    /**
    /**
     * Queries the appropriate content provider for the contact associated with the number.
     * Queries the appropriate content provider for the contact associated with the number.
     * <p>
     * <p>
     * Upon completion it also updates the cache in the call log, if it is different from
     * {@code callLogInfo}.
     * <p>
     * The number might be either a SIP address or a phone number.
     * The number might be either a SIP address or a phone number.
     * <p>
     * <p>
     * It returns true if it updated the content of the cache and we should therefore tell the
     * It returns true if it updated the content of the cache and we should therefore tell the
     * view to update its content.
     * view to update its content.
     */
     */
    private boolean queryContactInfo(String number) {
    private boolean queryContactInfo(String number, ContactInfo callLogInfo) {
        final ContactInfo info;
        final ContactInfo info;


        // Determine the contact info.
        // Determine the contact info.
@@ -411,6 +430,9 @@ public final class CallLogAdapter extends GroupingListAdapter
        // Store the data in the cache so that the UI thread can use to display it. Store it
        // Store the data in the cache so that the UI thread can use to display it. Store it
        // even if it has not changed so that it is marked as not expired.
        // even if it has not changed so that it is marked as not expired.
        mContactInfoCache.put(number, info);
        mContactInfoCache.put(number, info);
        // Update the call log even if the cache it is up-to-date: it is possible that the cache
        // contains the value from a different call log entry.
        updateCallLogContactInfoCache(number, info, callLogInfo);
        return updated;
        return updated;
    }
    }


@@ -423,9 +445,12 @@ public final class CallLogAdapter extends GroupingListAdapter
        boolean needNotify = false;
        boolean needNotify = false;
        while (!mDone) {
        while (!mDone) {
            String number = null;
            String number = null;
            ContactInfo callLogInfo = null;
            synchronized (mRequests) {
            synchronized (mRequests) {
                if (!mRequests.isEmpty()) {
                if (!mRequests.isEmpty()) {
                    number = mRequests.removeFirst();
                    Pair<String, ContactInfo> request = mRequests.removeFirst();
                    number = request.first;
                    callLogInfo  = request.second;
                } else {
                } else {
                    if (needNotify) {
                    if (needNotify) {
                        needNotify = false;
                        needNotify = false;
@@ -439,7 +464,7 @@ public final class CallLogAdapter extends GroupingListAdapter
                    }
                    }
                }
                }
            }
            }
            if (!mDone && number != null && queryContactInfo(number)) {
            if (!mDone && number != null && queryContactInfo(number, callLogInfo)) {
                needNotify = true;
                needNotify = true;
            }
            }
        }
        }
@@ -571,14 +596,20 @@ public final class CallLogAdapter extends GroupingListAdapter
            info = ContactInfo.EMPTY;
            info = ContactInfo.EMPTY;
            mContactInfoCache.put(number, info);
            mContactInfoCache.put(number, info);
            // Request the contact details immediately since they are currently missing.
            // Request the contact details immediately since they are currently missing.
            enqueueRequest(number, true);
            enqueueRequest(number, cachedContactInfo, true);
            // Format the phone number in the call log as best as we can.
            // Format the phone number in the call log as best as we can.
            formattedNumber = formatPhoneNumber(number, null, countryIso);
            formattedNumber = formatPhoneNumber(number, null, countryIso);
        } else {
        } else {
            if (cachedInfo.isExpired()) {
            if (cachedInfo.isExpired()) {
                // The contact info is no longer up to date, we should request it. However, we
                // The contact info is no longer up to date, we should request it. However, we
                // do not need to request them immediately.
                // do not need to request them immediately.
                enqueueRequest(number, false);
                enqueueRequest(number, cachedContactInfo, false);
            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
                // The call log information does not match the one we have, look it up again.
                // We could simply update the call log directly, but that needs to be done in a
                // background thread, so it is easier to simply request a new lookup, which will, as
                // a side-effect, update the call log.
                enqueueRequest(number, cachedContactInfo, false);
            }
            }


            if (info != ContactInfo.EMPTY) {
            if (info != ContactInfo.EMPTY) {
@@ -629,6 +660,52 @@ public final class CallLogAdapter extends GroupingListAdapter
        }
        }
    }
    }


    /** Checks whether the contact info from the call log matches the one from the contacts db. */
    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
        // The call log only contains a subset of the fields in the contacts db.
        // Only check those.
        return TextUtils.equals(callLogInfo.name, info.name)
                && callLogInfo.type == info.type
                && TextUtils.equals(callLogInfo.label, info.label);
    }

    /** Stores the updated contact info in the call log if it is different from the current one. */
    private void updateCallLogContactInfoCache(String number, ContactInfo updatedInfo,
            ContactInfo callLogInfo) {
        final ContentValues values = new ContentValues();
        boolean needsUpdate = false;

        if (callLogInfo != null) {
            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
                values.put(Calls.CACHED_NAME, updatedInfo.name);
                needsUpdate = true;
            }

            if (updatedInfo.type != callLogInfo.type) {
                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
                needsUpdate = true;
            }

            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
                needsUpdate = true;
            }
        } else {
            needsUpdate = true;
        }

        if (!needsUpdate) {
            return;
        }

        StringBuilder where = new StringBuilder();
        where.append(Calls.NUMBER);
        where.append(" = ?");

        mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
                where.toString(), new String[]{ number });
    }

    /** Returns the contact information as stored in the call log. */
    /** Returns the contact information as stored in the call log. */
    private ContactInfo getContactInfoFromCallLog(Cursor c) {
    private ContactInfo getContactInfoFromCallLog(Cursor c) {
        ContactInfo info = new ContactInfo();
        ContactInfo info = new ContactInfo();
+206 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2011 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.contacts.calllog;

import com.google.common.collect.Lists;

import android.content.Context;
import android.database.MatrixCursor;
import android.test.AndroidTestCase;
import android.view.View;

import java.util.List;

/**
 * Unit tests for {@link CallLogAdapter}.
 */
public class CallLogAdapterTest extends AndroidTestCase {
    private static final String TEST_NUMBER = "12345678";
    private static final String TEST_NAME = "name";
    private static final String TEST_NUMBER_LABEL = "label";
    private static final int TEST_NUMBER_TYPE = 1;
    private static final String TEST_COUNTRY_ISO = "US";
    private static final String TEST_VOICEMAIL_NUMBER = "111";

    /** The object under test. */
    private TestCallLogAdapter mAdapter;

    private MatrixCursor mCursor;
    private View mView;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        // Use a call fetcher that does not do anything.
        CallLogAdapter.CallFetcher fakeCallFetcher = new CallLogAdapter.CallFetcher() {
            @Override
            public void startCallsQuery() {}
        };

        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, TEST_COUNTRY_ISO,
                TEST_VOICEMAIL_NUMBER);
        // The cursor used in the tests to store the entries to display.
        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
        mCursor.moveToFirst();
        // The views into which to store the data.
        mView = new View(getContext());
        mView.setTag(CallLogListItemViews.createForTest(getContext()));
    }

    @Override
    protected void tearDown() throws Exception {
        mAdapter = null;
        mCursor = null;
        mView = null;
        super.tearDown();
    }

    public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() {
        mCursor.addRow(createCallLogEntry());

        // Bind the views of a single row.
        mAdapter.bindStandAloneView(mView, getContext(), mCursor);

        // There is one request for contact details.
        assertEquals(1, mAdapter.requests.size());

        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
        // It is for the number we need to show.
        assertEquals(TEST_NUMBER, request.number);
        // Since there is nothing in the cache, it is an immediate request.
        assertTrue("should be immediate", request.immediate);
    }

    public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() {
        mCursor.addRow(createCallLogEntryWithCachedValues());

        // Bind the views of a single row.
        mAdapter.bindStandAloneView(mView, getContext(), mCursor);

        // There is one request for contact details.
        assertEquals(1, mAdapter.requests.size());

        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
        // The values passed to the request, match the ones in the call log cache.
        assertEquals(TEST_NAME, request.callLogInfo.name);
        assertEquals(1, request.callLogInfo.type);
        assertEquals(TEST_NUMBER_LABEL, request.callLogInfo.label);
    }


    public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
        mCursor.addRow(createCallLogEntry());
        mAdapter.injectContactInfoForTest(TEST_NUMBER, createContactInfo());

        // Bind the views of a single row.
        mAdapter.bindStandAloneView(mView, getContext(), mCursor);

        // There is one request for contact details.
        assertEquals(1, mAdapter.requests.size());

        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
        // Since there is something in the cache, it is not an immediate request.
        assertFalse("should not be immediate", request.immediate);
    }

    public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
        mCursor.addRow(createCallLogEntryWithCachedValues());
        mAdapter.injectContactInfoForTest(TEST_NUMBER, createContactInfo());

        // Bind the views of a single row.
        mAdapter.bindStandAloneView(mView, getContext(), mCursor);

        // Cache and call log are up-to-date: no need to request update.
        assertEquals(0, mAdapter.requests.size());
    }

    public void testBindView_MismatchBetwenCallLogAndMemoryCache_EnqueueRequest() {
        mCursor.addRow(createCallLogEntryWithCachedValues());

        // Contact info contains a different name.
        ContactInfo info = createContactInfo();
        info.name = "new name";
        mAdapter.injectContactInfoForTest(TEST_NUMBER, info);

        // Bind the views of a single row.
        mAdapter.bindStandAloneView(mView, getContext(), mCursor);

        // There is one request for contact details.
        assertEquals(1, mAdapter.requests.size());

        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
        // Since there is something in the cache, it is not an immediate request.
        assertFalse("should not be immediate", request.immediate);
    }

    /** Returns a contact info with default values. */
    private ContactInfo createContactInfo() {
        ContactInfo info = new ContactInfo();
        info.number = TEST_NUMBER;
        info.name = TEST_NAME;
        info.type = TEST_NUMBER_TYPE;
        info.label = TEST_NUMBER_LABEL;
        return info;
    }

    /** Returns a call log entry without cached values. */
    private Object[] createCallLogEntry() {
        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
        values[CallLogQuery.NUMBER] = TEST_NUMBER;
        return values;
    }

    /** Returns a call log entry with a cached values. */
    private Object[] createCallLogEntryWithCachedValues() {
        Object[] values = createCallLogEntry();
        values[CallLogQuery.CACHED_NAME] = TEST_NAME;
        values[CallLogQuery.CACHED_NUMBER_TYPE] = TEST_NUMBER_TYPE;
        values[CallLogQuery.CACHED_NUMBER_LABEL] = TEST_NUMBER_LABEL;
        return values;
    }

    /**
     * Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
     */
    // TODO: This would be better done by splitting the contact lookup into a collaborator class
    // instead.
    private static final class TestCallLogAdapter extends CallLogAdapter {
        public static class Request {
            public final String number;
            public final ContactInfo callLogInfo;
            public final boolean immediate;

            public Request(String number, ContactInfo callLogInfo, boolean immediate) {
                this.number = number;
                this.callLogInfo = callLogInfo;
                this.immediate = immediate;
            }
        }

        public final List<Request> requests = Lists.newArrayList();

        public TestCallLogAdapter(Context context, CallFetcher callFetcher,
                String currentCountryIso, String voicemailNumber) {
            super(context, callFetcher, currentCountryIso, voicemailNumber);
        }

        @Override
        void enqueueRequest(String number, ContactInfo callLogInfo, boolean immediate) {
            requests.add(new Request(number, callLogInfo, immediate));
        }
    }
}
+10 −2
Original line number Original line Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.contacts.calllog;
package com.android.contacts.calllog;


import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertEquals;

import android.provider.CallLog.Calls;

import junit.framework.Assert;
import junit.framework.Assert;


/**
/**
@@ -24,13 +27,18 @@ import junit.framework.Assert;
 */
 */
public class CallLogQueryTestUtils {
public class CallLogQueryTestUtils {
    public static Object[] createTestValues() {
    public static Object[] createTestValues() {
        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null };
        Object[] values = new Object[]{
                -1L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null,
        };
        assertEquals(CallLogQuery._PROJECTION.length, values.length);
        assertEquals(CallLogQuery._PROJECTION.length, values.length);
        return values;
        return values;
    }
    }


    public static Object[] createTestExtendedValues() {
    public static Object[] createTestExtendedValues() {
        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, 0 };
        Object[] values = new Object[]{
                -1L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null,
                CallLogQuery.SECTION_OLD_ITEM
        };
        Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
        Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
        return values;
        return values;
    }
    }