/* * 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.android.common.widget.GroupingListAdapter; import com.android.contacts.ContactPhotoManager; import com.android.contacts.PhoneCallDetails; import com.android.contacts.PhoneCallDetailsHelper; import com.android.contacts.R; import com.android.contacts.util.ExpirableCache; import com.android.contacts.util.UriUtils; import com.google.common.annotations.VisibleForTesting; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.provider.CallLog.Calls; import android.provider.ContactsContract.PhoneLookup; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.util.LinkedList; import libcore.util.Objects; /** * Adapter class to fill in data for the Call Log. */ public class CallLogAdapter extends GroupingListAdapter implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { /** Interface used to initiate a refresh of the content. */ public interface CallFetcher { public void fetchCalls(); } /** The time in millis to delay starting the thread processing requests. */ private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; /** The size of the cache of contact info. */ private static final int CONTACT_INFO_CACHE_SIZE = 100; private final Context mContext; private final ContactInfoHelper mContactInfoHelper; private final CallFetcher mCallFetcher; /** * A cache of the contact details for the phone numbers in the call log. *
* The content of the cache is expired (but not purged) whenever the application comes to
* the foreground.
*/
private ExpirableCache
* Each request is made of a phone number to look up, and the contact info currently stored in
* the call log for this number.
*
* The requests are added when displaying the contacts and are processed by a background
* thread.
*/
private final LinkedList
* Should be called from the main thread to prevent a race condition between the request to
* start the thread being processed and stopping the thread.
*/
public void stopRequestProcessing() {
// Remove any pending requests to start the processing thread.
mHandler.removeMessages(START_THREAD);
mDone = true;
if (mCallerIdThread != null) mCallerIdThread.interrupt();
}
public void invalidateCache() {
mContactInfoCache.expireAll();
// Let it restart the thread after next draw
mPreDrawListener = null;
}
/**
* Enqueues a request to look up the contact details for the given phone number.
*
* It also provides the current contact info stored in the call log for this number.
*
* 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, String countryIso, ContactInfo callLogInfo,
boolean immediate) {
ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
synchronized (mRequests) {
if (!mRequests.contains(request)) {
mRequests.add(request);
mRequests.notifyAll();
}
}
if (mFirst && immediate) {
startRequestProcessing();
mFirst = false;
}
}
/**
* Queries the appropriate content provider for the contact associated with the number.
*
* Upon completion it also updates the cache in the call log, if it is different from
* {@code callLogInfo}.
*
* The number might be either a SIP address or a phone number.
*
* It returns true if it updated the content of the cache and we should therefore tell the
* view to update its content.
*/
private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
if (info == null) {
// The lookup failed, just return without requesting to update the view.
return false;
}
// Check the existing entry in the cache: only if it has changed we should update the
// view.
ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
boolean updated = !info.equals(existingInfo);
// 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.
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;
}
/*
* Handles requests for contact name and number type
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
boolean needNotify = false;
while (!mDone) {
ContactInfoRequest request = null;
synchronized (mRequests) {
if (!mRequests.isEmpty()) {
request = mRequests.removeFirst();
} else {
if (needNotify) {
needNotify = false;
mHandler.sendEmptyMessage(REDRAW);
}
try {
mRequests.wait(1000);
} catch (InterruptedException ie) {
// Ignore and continue processing requests
Thread.currentThread().interrupt();
}
}
}
if (!mDone && request != null
&& queryContactInfo(request.number, request.countryIso, request.callLogInfo)) {
needNotify = true;
}
}
}
@Override
protected void addGroups(Cursor cursor) {
mCallLogGroupBuilder.addGroups(cursor);
}
@VisibleForTesting
@Override
public View newStandAloneView(Context context, ViewGroup parent) {
LayoutInflater inflater =
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
findAndCacheViews(view);
return view;
}
@VisibleForTesting
@Override
public void bindStandAloneView(View view, Context context, Cursor cursor) {
bindView(view, cursor, 1);
}
@VisibleForTesting
@Override
public View newChildView(Context context, ViewGroup parent) {
LayoutInflater inflater =
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
findAndCacheViews(view);
return view;
}
@VisibleForTesting
@Override
public void bindChildView(View view, Context context, Cursor cursor) {
bindView(view, cursor, 1);
}
@VisibleForTesting
@Override
public View newGroupView(Context context, ViewGroup parent) {
LayoutInflater inflater =
(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
findAndCacheViews(view);
return view;
}
@VisibleForTesting
@Override
public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
boolean expanded) {
bindView(view, cursor, groupSize);
}
private void findAndCacheViews(View view) {
// Get the views to bind to.
CallLogListItemViews views = CallLogListItemViews.fromView(view);
views.primaryActionView.setOnClickListener(mPrimaryActionListener);
views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
view.setTag(views);
}
/**
* Binds the views in the entry to the data in the call log.
*
* @param view the view corresponding to this entry
* @param c the cursor pointing to the entry in the call log
* @param count the number of entries in the current item, greater than 1 if it is a group
*/
private void bindView(View view, Cursor c, int count) {
final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
final int section = c.getInt(CallLogQuery.SECTION);
// This might be a header: check the value of the section column in the cursor.
if (section == CallLogQuery.SECTION_NEW_HEADER
|| section == CallLogQuery.SECTION_OLD_HEADER) {
views.primaryActionView.setVisibility(View.GONE);
views.bottomDivider.setVisibility(View.GONE);
views.listHeaderTextView.setVisibility(View.VISIBLE);
views.listHeaderTextView.setText(
section == CallLogQuery.SECTION_NEW_HEADER
? R.string.call_log_new_header
: R.string.call_log_old_header);
// Nothing else to set up for a header.
return;
}
// Default case: an item in the call log.
views.primaryActionView.setVisibility(View.VISIBLE);
views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE);
views.listHeaderTextView.setVisibility(View.GONE);
final String number = c.getString(CallLogQuery.NUMBER);
final long date = c.getLong(CallLogQuery.DATE);
final long duration = c.getLong(CallLogQuery.DURATION);
final int callType = c.getInt(CallLogQuery.CALL_TYPE);
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
views.primaryActionView.setTag(
IntentProvider.getCallDetailIntentProvider(
this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
// Store away the voicemail information so we can play it directly.
if (callType == Calls.VOICEMAIL_TYPE) {
String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
final long rowId = c.getLong(CallLogQuery.ID);
views.secondaryActionView.setTag(
IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
} else if (!TextUtils.isEmpty(number)) {
// Store away the number so we can call it directly if you click on the call icon.
views.secondaryActionView.setTag(
IntentProvider.getReturnCallIntentProvider(number));
} else {
// No action enabled.
views.secondaryActionView.setTag(null);
}
// Lookup contacts with this number
ExpirableCache.CachedValue
* It uses the next {@code count} rows in the cursor to extract the types.
*
* It position in the cursor is unchanged by this function.
*/
private int[] getCallTypes(Cursor cursor, int count) {
int position = cursor.getPosition();
int[] callTypes = new int[count];
for (int index = 0; index < count; ++index) {
callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
cursor.moveToNext();
}
cursor.moveToPosition(position);
return callTypes;
}
private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
views.quickContactView.assignContactUri(contactUri);
mContactPhotoManager.loadPhoto(views.quickContactView, photoId, false, true);
}
/**
* Sets whether processing of requests for contact details should be enabled.
*
* This method should be called in tests to disable such processing of requests when not
* needed.
*/
public void disableRequestProcessingForTest() {
mRequestProcessingDisabled = true;
}
public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
mContactInfoCache.put(number, contactInfo);
}
@Override
public void addGroup(int cursorPosition, int size, boolean expanded) {
super.addGroup(cursorPosition, size, expanded);
}
/*
* Get the number from the Contacts, if available, since sometimes
* the number provided by caller id may not be formatted properly
* depending on the carrier (roaming) in use at the time of the
* incoming call.
* Logic : If the caller-id number starts with a "+", use it
* Else if the number in the contacts starts with a "+", use that one
* Else if the number in the contacts is longer, use that one
*/
public String getBetterNumberFromContacts(String number) {
String matchingNumber = null;
// Look in the cache first. If it's not found then query the Phones db
ContactInfo ci = mContactInfoCache.getPossiblyExpired(number);
if (ci != null && ci != ContactInfo.EMPTY) {
matchingNumber = ci.number;
} else {
try {
Cursor phonesCursor = mContext.getContentResolver().query(
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
PhoneQuery._PROJECTION, null, null, null);
if (phonesCursor != null) {
if (phonesCursor.moveToFirst()) {
matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
}
phonesCursor.close();
}
} catch (Exception e) {
// Use the number from the call log
}
}
if (!TextUtils.isEmpty(matchingNumber) &&
(matchingNumber.startsWith("+")
|| matchingNumber.length() > number.length())) {
number = matchingNumber;
}
return number;
}
}