/* * Copyright (C) 2015 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.dialer.app.contactinfo; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.logging.ContactSource.Type; import com.android.dialer.oem.CequintCallerIdManager; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.ContactInfoHelper; import com.android.dialer.util.ExpirableCache; import java.lang.ref.WeakReference; import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.PriorityBlockingQueue; /** * This is a cache of contact details for the phone numbers in the call log. The key is the phone * number with the country in which the call was placed or received. The content of the cache is * expired (but not purged) whenever the application comes to the foreground. * *
This cache queues request for information and queries for information on a background thread, * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction * as needed. * *
TODO: Explore whether there is a pattern to remove external dependencies for starting and
* stopping the query thread.
*/
public class ContactInfoCache {
private static final int REDRAW = 1;
private static final int START_THREAD = 2;
private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000;
private final ExpirableCache 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(ContactInfoRequest request) {
LogUtil.d(
"ContactInfoCache.queryContactInfo",
"request number: %s, type: %d",
LogUtil.sanitizePhoneNumber(request.number),
request.type);
ContactInfo info;
if (request.isLocalRequest()) {
info = contactInfoHelper.lookupNumber(request.number, request.countryIso);
if (info != null && !info.contactExists) {
// TODO(wangqi): Maybe skip look up if it's already available in cached number lookup
// service.
long start = SystemClock.elapsedRealtime();
contactInfoHelper.updateFromCequintCallerId(cequintCallerIdManager, info, request.number);
long time = SystemClock.elapsedRealtime() - start;
LogUtil.d(
"ContactInfoCache.queryContactInfo", "Cequint Caller Id look up takes %d ms", time);
}
if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) {
if (!contactInfoHelper.hasName(info)) {
enqueueRequest(
request.number,
request.countryIso,
request.callLogInfo,
true,
ContactInfoRequest.TYPE_REMOTE);
return false;
}
}
} else {
info = contactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.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.
NumberWithCountryIso numberCountryIso =
new NumberWithCountryIso(request.number, request.countryIso);
ContactInfo existingInfo = cache.getPossiblyExpired(numberCountryIso);
final boolean isRemoteSource = info.sourceType != Type.UNKNOWN_SOURCE_TYPE;
// Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
// to avoid updating the data set for every new row that is scrolled into view.
// Exception: Photo uris for contacts from remote sources are not cached in the call log
// cache, so we have to force a redraw for these contacts regardless.
boolean updated =
(!Objects.equals(existingInfo, ContactInfo.EMPTY) || isRemoteSource)
&& !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.
cache.put(numberCountryIso, 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.
contactInfoHelper.updateCallLogContactInfo(
request.number, request.countryIso, info, request.callLogInfo);
if (!request.isLocalRequest()) {
contactInfoHelper.updateCachedNumberLookupService(info);
}
return updated;
}
/**
* After a delay, start the thread to begin processing requests. We perform lookups on a
* background thread, but this must be called to indicate the thread should be running.
*/
public void start() {
// Schedule a thread-creation message if the thread hasn't been created yet, as an
// optimization to queue fewer messages.
if (contactInfoQueryThread == null) {
// TODO: Check whether this delay before starting to process is necessary.
handler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS);
}
}
/**
* Stops the thread and clears the queue of messages to process. This cleans up the thread for
* lookups so that it is not perpetually running.
*/
public void stop() {
stopRequestProcessing();
}
/**
* Starts a background thread to process contact-lookup requests, unless one has already been
* started.
*/
private synchronized void startRequestProcessing() {
// For unit-testing.
if (requestProcessingDisabled) {
return;
}
// If a thread is already started, don't start another.
if (contactInfoQueryThread != null) {
return;
}
contactInfoQueryThread = new QueryThread();
contactInfoQueryThread.setPriority(Thread.MIN_PRIORITY);
contactInfoQueryThread.start();
}
public void invalidate() {
cache.expireAll();
stopRequestProcessing();
}
/**
* Stops the background thread that processes updates and cancels any pending requests to start
* it.
*/
private synchronized void stopRequestProcessing() {
// Remove any pending requests to start the processing thread.
handler.removeMessages(START_THREAD);
if (contactInfoQueryThread != null) {
// Stop the thread; we are finished with it.
contactInfoQueryThread.stopProcessing();
contactInfoQueryThread.interrupt();
contactInfoQueryThread = 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_MS}.
*/
private void enqueueRequest(
String number,
String countryIso,
ContactInfo callLogInfo,
boolean immediate,
@ContactInfoRequest.TYPE int type) {
ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type);
if (!updateRequests.contains(request)) {
updateRequests.offer(request);
}
if (immediate) {
startRequestProcessing();
}
}
/** 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);
}
/** Sets whether processing of requests for contact details should be enabled. */
public void disableRequestProcessing() {
requestProcessingDisabled = true;
}
@VisibleForTesting
public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
cache.put(numberCountryIso, contactInfo);
}
public interface OnContactInfoChangedListener {
void onContactInfoChanged();
}
/*
* Handles requests for contact name and number type.
*/
private class QueryThread extends Thread {
private volatile boolean done = false;
public QueryThread() {
super("ContactInfoCache.QueryThread");
}
public void stopProcessing() {
done = true;
}
@Override
public void run() {
boolean shouldRedraw = false;
while (true) {
// Check if thread is finished, and if so return immediately.
if (done) {
return;
}
try {
ContactInfoRequest request = updateRequests.take();
shouldRedraw |= queryContactInfo(request);
if (shouldRedraw
&& (updateRequests.isEmpty()
|| (request.isLocalRequest() && !updateRequests.peek().isLocalRequest()))) {
shouldRedraw = false;
handler.sendEmptyMessage(REDRAW);
}
} catch (InterruptedException e) {
// Ignore and attempt to continue processing requests
}
}
}
}
}