blob: 32f7397ff8b89ae2f425a209b085e174abc289fc [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Android Open Kang Project
* Copyright (C) 2023 The LineageOS 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.callstats;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.CallLog.Calls;
import android.telecom.PhoneAccountHandle;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.util.UriUtils;
import com.google.common.collect.Lists;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Class to handle call-log queries, optionally with a date-range filter
*/
public class CallStatsQueryHandler extends AsyncQueryHandler {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final int EVENT_PROCESS_DATA = 10;
private static final int QUERY_CALLS_TOKEN = 100;
private static final String TAG = "CallStatsQueryHandler";
private final WeakReference<Listener> mListener;
private Handler mWorkerThreadHandler;
/**
* Simple handler that wraps background calls to catch
* {@link SQLiteException}, such as when the disk is full.
*/
protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
public CatchingWorkerHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.arg1 == EVENT_PROCESS_DATA) {
Cursor cursor = (Cursor) msg.obj;
Message reply = CallStatsQueryHandler.this.obtainMessage(msg.what);
reply.obj = processData(cursor);
reply.arg1 = msg.arg1;
reply.sendToTarget();
return;
}
try {
// Perform same query while catching any exceptions
super.handleMessage(msg);
} catch (SQLiteDiskIOException | SQLiteFullException | SQLiteDatabaseCorruptException e) {
Log.w(TAG, "Exception on background worker thread", e);
}
}
}
@Override
protected Handler createHandler(Looper looper) {
// Provide our special handler that catches exceptions
mWorkerThreadHandler = new CatchingWorkerHandler(looper);
return mWorkerThreadHandler;
}
public CallStatsQueryHandler(ContentResolver contentResolver, Listener listener) {
super(contentResolver);
mListener = new WeakReference<>(listener);
}
public void fetchCalls(long from, long to, PhoneAccountHandle account) {
cancelOperation(QUERY_CALLS_TOKEN);
StringBuilder selection = new StringBuilder();
List<String> selectionArgs = Lists.newArrayList();
if (from != -1) {
selection.append(String.format("(%s > ?)", Calls.DATE));
selectionArgs.add(String.valueOf(from));
}
if (to != -1) {
if (selection.length() > 0) {
selection.append(" AND ");
}
selection.append(String.format("(%s < ?)", Calls.DATE));
selectionArgs.add(String.valueOf(to));
}
if (account != null) {
if (selection.length() > 0) {
selection.append(" AND ");
}
selection.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID));
selectionArgs.add(account.getId());
}
startQuery(QUERY_CALLS_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
CallStatsQuery._PROJECTION, selection.toString(),
selectionArgs.toArray(EMPTY_STRING_ARRAY), Calls.NUMBER + " ASC");
}
@Override
protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (token == QUERY_CALLS_TOKEN) {
Message msg = mWorkerThreadHandler.obtainMessage(token);
msg.arg1 = EVENT_PROCESS_DATA;
msg.obj = cursor;
mWorkerThreadHandler.sendMessage(msg);
}
}
@Override
public void handleMessage(Message msg) {
if (msg.arg1 == EVENT_PROCESS_DATA) {
final Map<ContactInfo, CallStatsDetails> calls =
(Map<ContactInfo, CallStatsDetails>) msg.obj;
final Listener listener = mListener.get();
if (listener != null) {
listener.onCallsFetched(calls);
}
} else {
super.handleMessage(msg);
}
}
private Map<ContactInfo, CallStatsDetails> processData(Cursor cursor) {
final Map<ContactInfo, CallStatsDetails> result = new HashMap<>();
final ArrayList<ContactInfo> infos = new ArrayList<>();
final ArrayList<CallStatsDetails> calls = new ArrayList<>();
CallStatsDetails pending = null;
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
final String number = cursor.getString(CallStatsQuery.NUMBER);
final long duration = cursor.getLong(CallStatsQuery.DURATION);
final int callType = cursor.getInt(CallStatsQuery.CALL_TYPE);
if (pending == null || !phoneNumbersEqual(pending.number.toString(), number)) {
final long date = cursor.getLong(CallStatsQuery.DATE);
final int numberPresentation = cursor.getInt(CallStatsQuery.NUMBER_PRESENTATION);
final String countryIso = cursor.getString(CallStatsQuery.COUNTRY_ISO);
final String geocode = cursor.getString(CallStatsQuery.GEOCODED_LOCATION);
final String postDialDigits = cursor.getString(CallStatsQuery.POST_DIAL_DIGITS);
final ContactInfo info = getContactInfoFromCallStats(cursor);
final PhoneAccountHandle accountHandle = TelecomUtil.composePhoneAccountHandle(
cursor.getString(CallStatsQuery.ACCOUNT_COMPONENT_NAME),
cursor.getString(CallStatsQuery.ACCOUNT_ID));
pending = new CallStatsDetails(number, numberPresentation, postDialDigits,
accountHandle, info, countryIso, geocode, date);
infos.add(info);
calls.add(pending);
}
pending.addTimeOrMissed(callType, duration);
cursor.moveToNext();
}
cursor.close();
mergeItemsByNumber(calls, infos);
for (int i = 0; i < calls.size(); i++) {
result.put(infos.get(i), calls.get(i));
}
return result;
}
private void mergeItemsByNumber(List<CallStatsDetails> calls, List<ContactInfo> infos) {
// temporarily store items marked for removal
final ArrayList<CallStatsDetails> callsToRemove = new ArrayList<>();
final ArrayList<ContactInfo> infosToRemove = new ArrayList<>();
for (int i = 0; i < calls.size(); i++) {
final CallStatsDetails outerItem = calls.get(i);
final String currentFormattedNumber = outerItem.number.toString();
for (int j = calls.size() - 1; j > i; j--) {
final CallStatsDetails innerItem = calls.get(j);
final String innerNumber = innerItem.number.toString();
if (phoneNumbersEqual(currentFormattedNumber, innerNumber)) {
outerItem.mergeWith(innerItem);
//make sure we're not counting twice in case we're dealing with
//multiple different formats
innerItem.reset();
callsToRemove.add(innerItem);
infosToRemove.add(infos.get(j));
}
}
}
for (CallStatsDetails call : callsToRemove) {
calls.remove(call);
}
for (ContactInfo info : infosToRemove) {
infos.remove(info);
}
}
private ContactInfo getContactInfoFromCallStats(Cursor c) {
ContactInfo info = new ContactInfo();
info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallStatsQuery.CACHED_LOOKUP_URI));
info.name = c.getString(CallStatsQuery.CACHED_NAME);
info.type = c.getInt(CallStatsQuery.CACHED_NUMBER_TYPE);
info.label = c.getString(CallStatsQuery.CACHED_NUMBER_LABEL);
final String matchedNumber = c.getString(CallStatsQuery.CACHED_MATCHED_NUMBER);
info.number = matchedNumber == null ? c.getString(CallStatsQuery.NUMBER) : matchedNumber;
info.normalizedNumber = c.getString(CallStatsQuery.CACHED_NORMALIZED_NUMBER);
info.formattedNumber = c.getString(CallStatsQuery.CACHED_FORMATTED_NUMBER);
info.photoId = c.getLong(CallStatsQuery.CACHED_PHOTO_ID);
info.photoUri = null; // We do not cache the photo URI.
return info;
}
private static boolean phoneNumbersEqual(String number1, String number2) {
if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) {
return PhoneNumberHelper.compareSipAddresses(number1, number2);
} else {
return PhoneNumberUtils.compare(number1, number2);
}
}
public interface Listener {
void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls);
}
}