375 lines
14 KiB
Java
375 lines
14 KiB
Java
/*
|
|
* 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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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<NumberWithCountryIso, ContactInfo> cache;
|
|
private final ContactInfoHelper contactInfoHelper;
|
|
private final OnContactInfoChangedListener onContactInfoChangedListener;
|
|
private final BlockingQueue<ContactInfoRequest> updateRequests;
|
|
private final Handler handler;
|
|
private CequintCallerIdManager cequintCallerIdManager;
|
|
private QueryThread contactInfoQueryThread;
|
|
private volatile boolean requestProcessingDisabled = false;
|
|
|
|
private static class InnerHandler extends Handler {
|
|
|
|
private final WeakReference<ContactInfoCache> contactInfoCacheWeakReference;
|
|
|
|
public InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference) {
|
|
this.contactInfoCacheWeakReference = contactInfoCacheWeakReference;
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
ContactInfoCache reference = contactInfoCacheWeakReference.get();
|
|
if (reference == null) {
|
|
return;
|
|
}
|
|
switch (msg.what) {
|
|
case REDRAW:
|
|
reference.onContactInfoChangedListener.onContactInfoChanged();
|
|
break;
|
|
case START_THREAD:
|
|
reference.startRequestProcessing();
|
|
break;
|
|
default: // fall out
|
|
}
|
|
}
|
|
}
|
|
|
|
public ContactInfoCache(
|
|
@NonNull ExpirableCache<NumberWithCountryIso, ContactInfo> internalCache,
|
|
@NonNull ContactInfoHelper contactInfoHelper,
|
|
@NonNull OnContactInfoChangedListener listener) {
|
|
cache = internalCache;
|
|
this.contactInfoHelper = contactInfoHelper;
|
|
onContactInfoChangedListener = listener;
|
|
updateRequests = new PriorityBlockingQueue<>();
|
|
handler = new InnerHandler(new WeakReference<>(this));
|
|
}
|
|
|
|
public void setCequintCallerIdManager(CequintCallerIdManager cequintCallerIdManager) {
|
|
this.cequintCallerIdManager = cequintCallerIdManager;
|
|
}
|
|
|
|
public ContactInfo getValue(
|
|
String number,
|
|
String countryIso,
|
|
ContactInfo callLogContactInfo,
|
|
boolean remoteLookupIfNotFoundLocally) {
|
|
NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
|
|
ExpirableCache.CachedValue<ContactInfo> cachedInfo = cache.getCachedValue(numberCountryIso);
|
|
ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
|
|
int requestType =
|
|
remoteLookupIfNotFoundLocally
|
|
? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE
|
|
: ContactInfoRequest.TYPE_LOCAL;
|
|
if (cachedInfo == null) {
|
|
cache.put(numberCountryIso, ContactInfo.EMPTY);
|
|
// Use the cached contact info from the call log.
|
|
info = callLogContactInfo;
|
|
// The db request should happen on a non-UI thread.
|
|
// Request the contact details immediately since they are currently missing.
|
|
enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType);
|
|
// We will format the phone number when we make the background request.
|
|
} else {
|
|
if (cachedInfo.isExpired()) {
|
|
// The contact info is no longer up to date, we should request it. However, we
|
|
// do not need to request them immediately.
|
|
enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ false, requestType);
|
|
} else if (!callLogInfoMatches(callLogContactInfo, 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, countryIso, callLogContactInfo, /* immediate */ false, requestType);
|
|
}
|
|
|
|
if (Objects.equals(info, ContactInfo.EMPTY)) {
|
|
// Use the cached contact info from the call log.
|
|
info = callLogContactInfo;
|
|
}
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* Queries the appropriate content provider for the contact associated with the number.
|
|
*
|
|
* <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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <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_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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|