Put call log related internal utils into a telecom static lib
Flag: EXEMPT make file
Bug: 307788617
Bug: 308474740
Test: make
Test: manual
Test: atest TelecomUnitTests
Test: atest ContactsProviderTests
Change-Id: I5c08697bd9a667737eb71f31274e6cd0c64173f8
diff --git a/Android.bp b/Android.bp
index 65e4402..6411c68 100644
--- a/Android.bp
+++ b/Android.bp
@@ -62,6 +62,14 @@
},
}
+// Build telecom util static lib
+java_library_static {
+ name: "telecom-util-lib",
+ srcs: [
+ "src/com/android/server/telecom/util/*.java",
+ ],
+}
+
android_test {
name: "TelecomUnitTests",
static_libs: [
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 4d0a126..abd48bc 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -50,7 +50,6 @@
import android.telecom.CallDiagnosticService;
import android.telecom.CallDiagnostics;
import android.telecom.CallException;
-import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.ConnectionService;
@@ -84,6 +83,7 @@
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.VerifyCallStateChangeTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.util.CallerInfo;
import java.io.IOException;
import java.text.SimpleDateFormat;
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
index de952ce..eccfe77 100644
--- a/src/com/android/server/telecom/CallLogManager.java
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -16,9 +16,9 @@
package com.android.server.telecom;
-import static android.provider.CallLog.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;
+import static com.android.server.telecom.util.CallLogUtils.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -57,6 +57,7 @@
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
+import com.android.server.telecom.util.CallLogUtils;
import java.util.Arrays;
import java.util.Locale;
@@ -80,7 +81,7 @@
* Parameter object to hold the arguments to add a call in the call log DB.
*/
private static class AddCallArgs {
- public AddCallArgs(Context context, CallLog.AddCallParams params,
+ public AddCallArgs(Context context, CallLogUtils.AddCallParams params,
@Nullable LogCallCompletedListener logCallCompletedListener,
@NonNull Call call) {
this.context = context;
@@ -92,7 +93,7 @@
// Since the members are accessed directly, we don't use the
// mXxxx notation.
public final Context context;
- public final CallLog.AddCallParams params;
+ public final CallLogUtils.AddCallParams params;
public final Call call;
@Nullable
public final LogCallCompletedListener logCallCompletedListener;
@@ -322,8 +323,8 @@
void logCall(Call call, int callLogType,
@Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {
- CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
- new CallLog.AddCallParams.AddCallParametersBuilder();
+ CallLogUtils.AddCallParams.AddCallParametersBuilder paramBuilder =
+ new CallLogUtils.AddCallParams.AddCallParametersBuilder();
if (call.getConnectTimeMillis() != 0
&& call.getConnectTimeMillis() < call.getCreationTimeMillis()) {
// If connected time is available, use connected time. The connected time might be
@@ -597,7 +598,7 @@
AddCallArgs c = callList[i];
mListeners[i] = c.logCallCompletedListener;
try {
- result[i] = Calls.addCall(c.context, c.params);
+ result[i] = CallLogUtils.addCall(c.context, c.params);
Log.i(TAG, "LogCall; logged callId=%s, uri=%s",
c.call.getId(), result[i]);
if (result[i] == null) {
diff --git a/src/com/android/server/telecom/CallerInfoAsyncQueryFactory.java b/src/com/android/server/telecom/CallerInfoAsyncQueryFactory.java
index cfb9f6d..e45feb3 100644
--- a/src/com/android/server/telecom/CallerInfoAsyncQueryFactory.java
+++ b/src/com/android/server/telecom/CallerInfoAsyncQueryFactory.java
@@ -15,10 +15,10 @@
*/
package com.android.server.telecom;
-import android.telecom.CallerInfoAsyncQuery;
-
import android.content.Context;
+import com.android.server.telecom.util.CallerInfoAsyncQuery;
+
public interface CallerInfoAsyncQueryFactory {
CallerInfoAsyncQuery startQuery(int token, Context context, String number,
CallerInfoAsyncQuery.OnQueryCompleteListener listener, Object cookie);
diff --git a/src/com/android/server/telecom/CallerInfoLookupHelper.java b/src/com/android/server/telecom/CallerInfoLookupHelper.java
index e9f1c16..13d8db6 100644
--- a/src/com/android/server/telecom/CallerInfoLookupHelper.java
+++ b/src/com/android/server/telecom/CallerInfoLookupHelper.java
@@ -28,8 +28,9 @@
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
-import android.telecom.CallerInfo;
-import android.telecom.CallerInfoAsyncQuery;
+
+import com.android.server.telecom.util.CallerInfo;
+import com.android.server.telecom.util.CallerInfoAsyncQuery;
import java.util.HashMap;
import java.util.LinkedList;
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index b97c098..751fc96 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -89,7 +89,6 @@
import android.telecom.CallEndpoint;
import android.telecom.CallException;
import android.telecom.CallScreeningService;
-import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
@@ -151,6 +150,7 @@
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.util.CallerInfo;
import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
import com.android.server.telecom.callsequencing.TransactionManager;
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
index b0a7c8e..a5694d8 100644
--- a/src/com/android/server/telecom/MissedCallNotifier.java
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -21,7 +21,7 @@
import android.os.UserHandle;
import android.telecom.PhoneAccountHandle;
-import android.telecom.CallerInfo;
+import com.android.server.telecom.util.CallerInfo;
/**
* Creates a notification for calls that the user missed (neither answered nor rejected).
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index 9df76be..bb5f88d 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -34,8 +34,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.flags.FeatureFlags;
+import com.android.server.telecom.util.CallerInfo;
-import android.telecom.CallerInfo;
import android.util.Pair;
import java.util.List;
diff --git a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
index 65b7731..3557eb2 100644
--- a/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
+++ b/src/com/android/server/telecom/callfiltering/BlockCheckerFilter.java
@@ -24,7 +24,6 @@
import android.os.UserManager;
import android.provider.BlockedNumberContract;
import android.provider.CallLog;
-import android.telecom.CallerInfo;
import android.telecom.Log;
import android.telecom.TelecomManager;
@@ -34,6 +33,7 @@
import com.android.server.telecom.LogUtils;
import com.android.server.telecom.LoggedHandlerExecutor;
import com.android.server.telecom.settings.BlockedNumbersUtil;
+import com.android.server.telecom.util.CallerInfo;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
diff --git a/src/com/android/server/telecom/callfiltering/DirectToVoicemailFilter.java b/src/com/android/server/telecom/callfiltering/DirectToVoicemailFilter.java
index ab24d94..08bf64c 100644
--- a/src/com/android/server/telecom/callfiltering/DirectToVoicemailFilter.java
+++ b/src/com/android/server/telecom/callfiltering/DirectToVoicemailFilter.java
@@ -18,12 +18,12 @@
import android.net.Uri;
import android.provider.CallLog;
-import android.telecom.CallerInfo;
import android.telecom.Log;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.util.CallerInfo;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index f47a32b..2dac8fa 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -38,7 +38,6 @@
import android.provider.BlockedNumbersManager;
import android.telecom.Log;
-import android.telecom.CallerInfoAsyncQuery;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.telecom.IInternalServiceRetriever;
@@ -76,6 +75,7 @@
import com.android.server.telecom.ui.IncomingCallNotifier;
import com.android.server.telecom.ui.MissedCallNotifierImpl;
import com.android.server.telecom.ui.NotificationChannelManager;
+import com.android.server.telecom.util.CallerInfoAsyncQuery;
import java.util.concurrent.Executors;
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index e93e28f..1f1a549 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -46,7 +46,6 @@
import android.os.UserHandle;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
-import android.telecom.CallerInfo;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.PhoneAccount;
@@ -75,6 +74,7 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
+import com.android.server.telecom.util.CallerInfo;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/server/telecom/util/CallLogUtils.java b/src/com/android/server/telecom/util/CallLogUtils.java
new file mode 100644
index 0000000..45181dd
--- /dev/null
+++ b/src/com/android/server/telecom/util/CallLogUtils.java
@@ -0,0 +1,1115 @@
+/*
+ * Copyright (C) 2025 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.server.telecom.util;
+
+import static android.provider.CallLog.Calls.ADD_FOR_ALL_USERS;
+import static android.provider.CallLog.Calls.ASSERTED_DISPLAY_NAME;
+import static android.provider.CallLog.Calls.BLOCK_REASON;
+import static android.provider.CallLog.Calls.CACHED_NAME;
+import static android.provider.CallLog.Calls.CALL_SCREENING_APP_NAME;
+import static android.provider.CallLog.Calls.CALL_SCREENING_COMPONENT_NAME;
+import static android.provider.CallLog.Calls.COMPOSER_PHOTO_URI;
+import static android.provider.CallLog.Calls.CONTENT_URI;
+import static android.provider.CallLog.Calls.DATA_USAGE;
+import static android.provider.CallLog.Calls.DATE;
+import static android.provider.CallLog.Calls.DEFAULT_SORT_ORDER;
+import static android.provider.CallLog.Calls.DURATION;
+import static android.provider.CallLog.Calls.FEATURES;
+import static android.provider.CallLog.Calls.IS_BUSINESS_CALL;
+import static android.provider.CallLog.Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING;
+import static android.provider.CallLog.Calls.IS_READ;
+import static android.provider.CallLog.Calls.MISSED_REASON;
+import static android.provider.CallLog.Calls.MISSED_TYPE;
+import static android.provider.CallLog.Calls.NEW;
+import static android.provider.CallLog.Calls.NUMBER;
+import static android.provider.CallLog.Calls.NUMBER_PRESENTATION;
+import static android.provider.CallLog.Calls.PHONE_ACCOUNT_ADDRESS;
+import static android.provider.CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME;
+import static android.provider.CallLog.Calls.PHONE_ACCOUNT_ID;
+import static android.provider.CallLog.Calls.POST_DIAL_DIGITS;
+import static android.provider.CallLog.Calls.PRESENTATION_ALLOWED;
+import static android.provider.CallLog.Calls.PRESENTATION_UNAVAILABLE;
+import static android.provider.CallLog.Calls.PRESENTATION_UNKNOWN;
+import static android.provider.CallLog.Calls.PRIORITY;
+import static android.provider.CallLog.Calls.SUBJECT;
+import static android.provider.CallLog.Calls.TYPE;
+import static android.provider.CallLog.Calls.VIA_NUMBER;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.location.Country;
+import android.location.CountryDetector;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.CallLog;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DataUsageFeedback;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.telecom.flags.Flags;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Encapsulates the util methods to update the call log
+ */
+public class CallLogUtils {
+
+ private static final String LOG_TAG = "CallLogUtils";
+ private static final boolean VERBOSE_LOG = false; // DON'T SUBMIT WITH TRUE.
+ /**
+ * If a successful call is made that is longer than this duration, update the phone number in
+ * the ContactsProvider with the normalized version of the number, based on the user's current
+ * country code.
+ */
+ private static final int MIN_DURATION_FOR_NORMALIZED_NUMBER_UPDATE_MS = 1000 * 10;
+ /**
+ * The default maximum number of call log entries stored in the call log provider for each
+ * {@link PhoneAccountHandle}.
+ */
+ private static final int DEFAULT_MAX_CALL_LOG_SIZE = 500;
+ /**
+ * Expected component name of Telephony phone accounts.
+ */
+ private static final ComponentName TELEPHONY_COMPONENT_NAME =
+ new ComponentName("com.android.phone",
+ "com.android.services.telephony.TelephonyConnectionService");
+
+ /**
+ * The "shadow" provider stores calllog when the real calllog provider is encrypted. The real
+ * provider will alter copy from it when it starts, and remove the entries in the shadow.
+ *
+ * <p>See the comment in {@link Calls#addCall} for the details.
+ * TODO: make them as System API in CallLog?
+ */
+ private static final String SHADOW_AUTHORITY = "call_log_shadow";
+ private static final Uri SHADOW_CONTENT_URI = Uri.parse("content://call_log_shadow/calls");
+
+ /**
+ * Adds a call to the call log.
+ *
+ * @param ci the CallerInfo object to get the target contact from. Can be null if the
+ * contact is unknown.
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number to be added to the calls db
+ * @param presentation enum value from TelecomManager.PRESENTATION_xxx, which is set by the
+ * network and denotes the number presenting rules for "allowed", "payphone", "restricted"
+ * or "unknown"
+ * @param callType enumerated values for "incoming", "outgoing", or "missed"
+ * @param features features of the call (e.g. Video).
+ * @param accountHandle The accountHandle object identifying the provider of the call
+ * @param start time stamp for the call in milliseconds
+ * @param duration call duration in seconds
+ * @param dataUsage data usage for the call in bytes, null if data usage was not tracked for
+ * the call.
+ * @param isPhoneAccountMigrationPending whether the PhoneAccountHandle ID need to migrate
+ * @return The URI of the call log entry belonging to the user that made or received this
+ * call.
+ */
+ public static Uri addCall(CallerInfo ci, Context context, String number,
+ int presentation, int callType, int features,
+ PhoneAccountHandle accountHandle,
+ long start, int duration, Long dataUsage, long missedReason,
+ int isPhoneAccountMigrationPending) {
+ return addCall(ci, context, number, "" /* postDialDigits */, "" /* viaNumber */,
+ presentation, callType, features, accountHandle, start, duration,
+ dataUsage, false /* addForAllUsers */, null /* userToBeInsertedTo */,
+ false /* isRead */, CallLog.Calls.BLOCK_REASON_NOT_BLOCKED /* callBlockReason */,
+ null /* callScreeningAppName */, null /* callScreeningComponentName */,
+ missedReason, isPhoneAccountMigrationPending);
+ }
+
+ /**
+ * Adds a call to the call log.
+ *
+ * @param ci the CallerInfo object to get the target contact from. Can be null if the
+ * contact is unknown.
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number to be added to the calls db
+ * @param viaNumber the secondary number that the incoming call received with. If the call
+ * was received with the SIM assigned number, then this field must be ''.
+ * @param presentation enum value from TelecomManager.PRESENTATION_xxx, which is set by the
+ * network and denotes the number presenting rules for "allowed", "payphone", "restricted"
+ * or "unknown"
+ * @param callType enumerated values for "incoming", "outgoing", or "missed"
+ * @param features features of the call (e.g. Video).
+ * @param accountHandle The accountHandle object identifying the provider of the call
+ * @param start time stamp for the call in milliseconds
+ * @param duration call duration in seconds
+ * @param dataUsage data usage for the call in bytes, null if data usage was not tracked for
+ * the call.
+ * @param addForAllUsers If true, the call is added to the call log of all currently running
+ * users. The caller must have the MANAGE_USERS permission if this is true.
+ * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be
+ * inserted to. null if it is inserted to the current user. The value is ignored if
+ * addForAllUsers is true.
+ * @param isPhoneAccountMigrationPending whether the PhoneAccountHandle ID need to migrate
+ * @return The URI of the call log entry belonging to the user that made or received this
+ * call.
+ */
+ public static Uri addCall(CallerInfo ci, Context context, String number,
+ String postDialDigits, String viaNumber, int presentation, int callType,
+ int features, PhoneAccountHandle accountHandle, long start, int duration,
+ Long dataUsage, boolean addForAllUsers, UserHandle userToBeInsertedTo,
+ long missedReason, int isPhoneAccountMigrationPending) {
+ return addCall(ci, context, number, postDialDigits, viaNumber, presentation, callType,
+ features, accountHandle, start, duration, dataUsage, addForAllUsers,
+ userToBeInsertedTo, false /* isRead */, CallLog.Calls.BLOCK_REASON_NOT_BLOCKED
+ /* callBlockReason */, null /* callScreeningAppName */,
+ null /* callScreeningComponentName */, missedReason,
+ isPhoneAccountMigrationPending);
+ }
+
+ /**
+ * Adds a call to the call log.
+ *
+ * @param ci the CallerInfo object to get the target contact from. Can be null if the
+ * contact is unknown.
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number to be added to the calls db
+ * @param postDialDigits the post-dial digits that were dialed after the number, if it was
+ * outgoing. Otherwise it is ''.
+ * @param viaNumber the secondary number that the incoming call received with. If the call
+ * was received with the SIM assigned number, then this field must be ''.
+ * @param presentation enum value from TelecomManager.PRESENTATION_xxx, which is set by the
+ * network and denotes the number presenting rules for "allowed", "payphone", "restricted"
+ * or "unknown"
+ * @param callType enumerated values for "incoming", "outgoing", or "missed"
+ * @param features features of the call (e.g. Video).
+ * @param accountHandle The accountHandle object identifying the provider of the call
+ * @param start time stamp for the call in milliseconds
+ * @param duration call duration in seconds
+ * @param dataUsage data usage for the call in bytes, null if data usage was not tracked for
+ * the call.
+ * @param addForAllUsers If true, the call is added to the call log of all currently running
+ * users. The caller must have the MANAGE_USERS permission if this is true.
+ * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be
+ * inserted to. null if it is inserted to the current user. The value is ignored if
+ * addForAllUsers is true.
+ * @param isRead Flag to show if the missed call log has been read by the user or not. Used
+ * for call log restore of missed calls.
+ * @param callBlockReason The reason why the call is blocked.
+ * @param callScreeningAppName The call screening application name which block the call.
+ * @param callScreeningComponentName The call screening component name which block the
+ * call.
+ * @param missedReason The encoded missed information of the call.
+ * @param isPhoneAccountMigrationPending whether the PhoneAccountHandle ID need to migrate
+ * @return The URI of the call log entry belonging to the user that made or received this
+ * call. This could be of the shadow provider. Do not return it to non-system apps, as
+ * they don't have permissions.
+ */
+ public static Uri addCall(CallerInfo ci, Context context, String number,
+ String postDialDigits, String viaNumber, int presentation, int callType,
+ int features, PhoneAccountHandle accountHandle, long start, int duration,
+ Long dataUsage, boolean addForAllUsers, UserHandle userToBeInsertedTo,
+ boolean isRead, int callBlockReason, CharSequence callScreeningAppName,
+ String callScreeningComponentName, long missedReason,
+ int isPhoneAccountMigrationPending) {
+ AddCallParams.AddCallParametersBuilder builder =
+ new AddCallParams.AddCallParametersBuilder();
+ builder.setCallerInfo(ci);
+ builder.setNumber(number);
+ builder.setPostDialDigits(postDialDigits);
+ builder.setViaNumber(viaNumber);
+ builder.setPresentation(presentation);
+ builder.setCallType(callType);
+ builder.setFeatures(features);
+ builder.setAccountHandle(accountHandle);
+ builder.setStart(start);
+ builder.setDuration(duration);
+ builder.setDataUsage(dataUsage == null ? Long.MIN_VALUE : dataUsage);
+ builder.setAddForAllUsers(addForAllUsers);
+ builder.setUserToBeInsertedTo(userToBeInsertedTo);
+ builder.setIsRead(isRead);
+ builder.setCallBlockReason(callBlockReason);
+ builder.setCallScreeningAppName(callScreeningAppName);
+ builder.setCallScreeningComponentName(callScreeningComponentName);
+ builder.setMissedReason(missedReason);
+ builder.setIsPhoneAccountMigrationPending(isPhoneAccountMigrationPending);
+
+ return addCall(context, builder.build());
+ }
+
+ /**
+ * Adds a call to the call log, using the provided parameters
+ *
+ * @return The URI of the call log entry belonging to the user that made or received this
+ * call. This could be of the shadow provider. Do not return it to non-system apps, as
+ * they don't have permissions.
+ */
+ public static @NonNull Uri addCall(
+ @NonNull Context context, @NonNull AddCallParams params) {
+ if (VERBOSE_LOG) {
+ Log.v(LOG_TAG, String.format("Add call: number=%s, user=%s, for all=%s",
+ params.mNumber, params.mUserToBeInsertedTo, params.mAddForAllUsers));
+ }
+ final ContentResolver resolver = context.getContentResolver();
+
+ String accountAddress = getLogAccountAddress(context, params.mAccountHandle);
+
+ int numberPresentation = getLogNumberPresentation(params.mNumber, params.mPresentation);
+ String name = (params.mCallerInfo != null) ? params.mCallerInfo.getName() : "";
+ if (numberPresentation != PRESENTATION_ALLOWED) {
+ params.mNumber = "";
+ if (params.mCallerInfo != null) {
+ name = "";
+ }
+ }
+
+ // accountHandle information
+ String accountComponentString = null;
+ String accountId = null;
+ if (params.mAccountHandle != null) {
+ accountComponentString = params.mAccountHandle.getComponentName().flattenToString();
+ accountId = params.mAccountHandle.getId();
+ }
+
+ ContentValues values = new ContentValues(14);
+ values.put(NUMBER, params.mNumber);
+ values.put(POST_DIAL_DIGITS, params.mPostDialDigits);
+ values.put(VIA_NUMBER, params.mViaNumber);
+ values.put(NUMBER_PRESENTATION, Integer.valueOf(numberPresentation));
+ values.put(TYPE, Integer.valueOf(params.mCallType));
+ values.put(FEATURES, params.mFeatures);
+ values.put(DATE, Long.valueOf(params.mStart));
+ values.put(DURATION, Long.valueOf(params.mDuration));
+ if (params.mDataUsage != Long.MIN_VALUE) {
+ values.put(DATA_USAGE, params.mDataUsage);
+ }
+ values.put(PHONE_ACCOUNT_COMPONENT_NAME, accountComponentString);
+ values.put(PHONE_ACCOUNT_ID, accountId);
+ values.put(PHONE_ACCOUNT_ADDRESS, accountAddress);
+ values.put(NEW, Integer.valueOf(1));
+ values.put(CACHED_NAME, name);
+ values.put(ADD_FOR_ALL_USERS, params.mAddForAllUsers ? 1 : 0);
+
+ if (params.mCallType == MISSED_TYPE) {
+ values.put(IS_READ, Integer.valueOf(params.mIsRead ? 1 : 0));
+ }
+
+ values.put(BLOCK_REASON, params.mCallBlockReason);
+ values.put(CALL_SCREENING_APP_NAME, charSequenceToString(params.mCallScreeningAppName));
+ values.put(CALL_SCREENING_COMPONENT_NAME, params.mCallScreeningComponentName);
+ values.put(MISSED_REASON, Long.valueOf(params.mMissedReason));
+ values.put(PRIORITY, params.mPriority);
+ values.put(SUBJECT, params.mSubject);
+ if (params.mPictureUri != null) {
+ values.put(COMPOSER_PHOTO_URI, params.mPictureUri.toString());
+ }
+ values.put(IS_PHONE_ACCOUNT_MIGRATION_PENDING, params.mIsPhoneAccountMigrationPending);
+ if (Flags.businessCallComposer()) {
+ values.put(IS_BUSINESS_CALL, Integer.valueOf(params.mIsBusinessCall ? 1 : 0));
+ values.put(ASSERTED_DISPLAY_NAME, params.mAssertedDisplayName);
+ }
+ if ((params.mCallerInfo != null) && (params.mCallerInfo.getContactId() > 0)) {
+ // Update usage information for the number associated with the contact ID.
+ // We need to use both the number and the ID for obtaining a data ID since other
+ // contacts may have the same number.
+
+ final Cursor cursor;
+
+ // We should prefer normalized one (probably coming from
+ // Phone.NORMALIZED_NUMBER column) first. If it isn't available try others.
+ if (params.mCallerInfo.normalizedNumber != null) {
+ final String normalizedPhoneNumber = params.mCallerInfo.normalizedNumber;
+ cursor = resolver.query(Phone.CONTENT_URI,
+ new String[]{Phone._ID},
+ Phone.CONTACT_ID + " =? AND " + Phone.NORMALIZED_NUMBER + " =?",
+ new String[]{String.valueOf(params.mCallerInfo.getContactId()),
+ normalizedPhoneNumber},
+ null);
+ } else {
+ final String phoneNumber = params.mCallerInfo.getPhoneNumber() != null
+ ? params.mCallerInfo.getPhoneNumber() : params.mNumber;
+ cursor = resolver.query(
+ Uri.withAppendedPath(Callable.CONTENT_FILTER_URI,
+ Uri.encode(phoneNumber)),
+ new String[]{Phone._ID},
+ Phone.CONTACT_ID + " =?",
+ new String[]{String.valueOf(params.mCallerInfo.getContactId())},
+ null);
+ }
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() > 0 && cursor.moveToFirst()) {
+ final String dataId = cursor.getString(0);
+ updateDataUsageStatForData(resolver, dataId);
+ if (params.mDuration >= MIN_DURATION_FOR_NORMALIZED_NUMBER_UPDATE_MS
+ && params.mCallType == CallLog.Calls.OUTGOING_TYPE
+ && TextUtils.isEmpty(params.mCallerInfo.normalizedNumber)) {
+ updateNormalizedNumber(context, resolver, dataId, params.mNumber);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ /*
+ Writing the calllog works in the following way:
+ - All user entries
+ - if user-0 is encrypted, insert to user-0's shadow only.
+ (other users should also be encrypted, so nothing to do for other users.)
+ - if user-0 is decrypted, insert to user-0's real provider, as well as
+ all other users that are running and decrypted and should have calllog.
+
+ - Single user entry.
+ - If the target user is encrypted, insert to its shadow.
+ - Otherwise insert to its real provider.
+
+ When the (real) calllog provider starts, it copies entries that it missed from
+ elsewhere.
+ - When user-0's (real) provider starts, it copies from user-0's shadow, and clears
+ the shadow.
+
+ - When other users (real) providers start, unless it shouldn't have calllog entries,
+ - Copy from the user's shadow, and clears the shadow.
+ - Copy from user-0's entries that are FOR_ALL_USERS = 1. (and don't clear it.)
+ */
+
+ Uri result = null;
+
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ final int currentUserId = userManager.getProcessUserId();
+
+ if (params.mAddForAllUsers) {
+ if (userManager.isUserUnlocked(UserHandle.SYSTEM)) {
+ // If the user is unlocked, insert to the location provider if a location is
+ // provided. Do not store location if the device is still locked -- this
+ // puts it into device-encrypted storage instead of credential-encrypted
+ // storage.
+ Uri locationUri = maybeInsertLocation(params, resolver, UserHandle.SYSTEM);
+ if (locationUri != null) {
+ values.put(CallLog.Calls.LOCATION, locationUri.toString());
+ }
+ }
+
+ // First, insert to the system user.
+ final Uri uriForSystem = addEntryAndRemoveExpiredEntries(
+ context, userManager, UserHandle.SYSTEM, values);
+ if (uriForSystem == null
+ || SHADOW_AUTHORITY.equals(uriForSystem.getAuthority())) {
+ // This means the system user is still encrypted and the entry has inserted
+ // into the shadow. This means other users are still all encrypted.
+ // Nothing further to do; just return null.
+ return null;
+ }
+ if (UserHandle.USER_SYSTEM == currentUserId) {
+ result = uriForSystem;
+ }
+
+ // Otherwise, insert to all other users that are running and unlocked.
+
+ final List<UserHandle> users = userManager.getUserHandles(true);
+
+ final int count = users.size();
+ for (int i = 0; i < count; i++) {
+ final UserHandle userHandle = users.get(i);
+ final int userId = userHandle.getIdentifier();
+
+ if (userHandle.isSystem()) {
+ // Already written.
+ continue;
+ }
+
+ if (!shouldHaveSharedCallLogEntries(context, userManager, userHandle)) {
+ // Shouldn't have calllog entries.
+ continue;
+ }
+
+ // For other users, we write only when they're running *and* decrypted.
+ // Other providers will copy from the system user's real provider, when they
+ // start.
+ if (userManager.isUserRunning(userHandle)
+ && userManager.isUserUnlocked(userHandle)) {
+ Uri locationUri = maybeInsertLocation(params, resolver, userHandle);
+ if (locationUri != null) {
+ values.put(CallLog.Calls.LOCATION, locationUri.toString());
+ } else {
+ values.put(CallLog.Calls.LOCATION, (String) null);
+ }
+ final Uri uri = addEntryAndRemoveExpiredEntries(context, userManager,
+ userHandle, values);
+ if (userId == currentUserId) {
+ result = uri;
+ }
+ }
+ }
+ } else {
+ // Single-user entry. Just write to that user, assuming it's running. If the
+ // user is encrypted, we write to the shadow calllog.
+ final UserHandle targetUserHandle = params.mUserToBeInsertedTo != null
+ ? params.mUserToBeInsertedTo
+ : UserHandle.of(currentUserId);
+
+ if (userManager.isUserRunning(targetUserHandle)
+ && userManager.isUserUnlocked(targetUserHandle)) {
+ Uri locationUri = maybeInsertLocation(params, resolver, targetUserHandle);
+ if (locationUri != null) {
+ values.put(CallLog.Calls.LOCATION, locationUri.toString());
+ } else {
+ values.put(CallLog.Calls.LOCATION, (String) null);
+ }
+ }
+
+ result = addEntryAndRemoveExpiredEntries(context, userManager, targetUserHandle,
+ values);
+ }
+ return result;
+ }
+
+ private static String charSequenceToString(CharSequence sequence) {
+ return sequence == null ? null : sequence.toString();
+ }
+
+ private static boolean shouldHaveSharedCallLogEntries(
+ Context context, UserManager userManager, UserHandle userHandle) {
+ if (userManager.hasUserRestrictionForUser(
+ UserManager.DISALLOW_OUTGOING_CALLS, userHandle)) {
+ return false;
+ }
+ return isProfile(context, userHandle);
+ }
+
+ private static boolean isProfile(Context context, UserHandle userHandle) {
+ try {
+ return context.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager.class).isProfile();
+ } catch (IllegalStateException e) {
+ Log.e(LOG_TAG, e + "Error while creating context as user = " + userHandle);
+ }
+ return false;
+ }
+
+ private static Uri addEntryAndRemoveExpiredEntries(Context context, UserManager userManager,
+ UserHandle user, ContentValues values) {
+ final ContentResolver resolver = context.getContentResolver();
+
+ // Since we're doing this operation on behalf of an app, we only
+ // want to use the actual "unlocked" state.
+ final Uri uri = ContentProvider.maybeAddUserId(
+ userManager.isUserUnlocked(user) ? CONTENT_URI : SHADOW_CONTENT_URI,
+ user.getIdentifier());
+
+ Log.i(LOG_TAG, String.format(Locale.getDefault(),
+ "addEntryAndRemoveExpiredEntries: provider uri=%s", uri));
+
+ try {
+ // When cleaning up the call log, try to delete older call long entries on a per
+ // PhoneAccount basis first. There can be multiple ConnectionServices causing
+ // the addition of entries in the call log. With the introduction of Self-Managed
+ // ConnectionServices, we want to ensure that a misbehaving self-managed CS cannot
+ // spam the call log with its own entries, causing entries from Telephony to be
+ // removed.
+ final Uri result = resolver.insert(uri, values);
+ if (result != null) {
+ String lastPathSegment = result.getLastPathSegment();
+ // When inserting into the call log, if ContentProvider#insert detect an appops
+ // denial a non-null "silent rejection" URI is returned which ends in 0.
+ // Example: content://call_log/calls/0
+ // The 0 in the last part of the path indicates a fake call id of 0.
+ // A denial when logging calls from the platform is bad; there is no other
+ // logging to indicate that this has happened so we will check for that scenario
+ // here and log a warning so we have a hint as to what is going on.
+ if (lastPathSegment != null && lastPathSegment.equals("0")) {
+ Log.w(LOG_TAG, "Failed to insert into call log due to appops denial;"
+ + " resultUri=" + result);
+ }
+ } else {
+ Log.w(LOG_TAG, "Failed to insert into call log; null result uri.");
+ }
+
+ int numDeleted;
+ final String phoneAccountId =
+ values.containsKey(PHONE_ACCOUNT_ID)
+ ? values.getAsString(PHONE_ACCOUNT_ID) : null;
+ final String phoneAccountComponentName =
+ values.containsKey(PHONE_ACCOUNT_COMPONENT_NAME)
+ ? values.getAsString(PHONE_ACCOUNT_COMPONENT_NAME) : null;
+ int maxCallLogSize = DEFAULT_MAX_CALL_LOG_SIZE;
+ if (!TextUtils.isEmpty(phoneAccountId)
+ && !TextUtils.isEmpty(phoneAccountComponentName)) {
+ if (android.provider.Flags.allowConfigMaximumCallLogEntriesPerSim()
+ && TELEPHONY_COMPONENT_NAME
+ .flattenToString().equals(phoneAccountComponentName)) {
+ maxCallLogSize = context.getResources().getInteger(
+ com.android.internal.R.integer.config_maximumCallLogEntriesPerSim);
+ }
+ // Only purge entries for the same phone account.
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls"
+ + " WHERE " + PHONE_ACCOUNT_COMPONENT_NAME + " = ?"
+ + " AND " + PHONE_ACCOUNT_ID + " = ?"
+ + " ORDER BY " + DEFAULT_SORT_ORDER
+ + " LIMIT -1 OFFSET " + maxCallLogSize + ")",
+ new String[]{phoneAccountComponentName, phoneAccountId}
+ );
+ } else {
+ // No valid phone account specified, so default to the old behavior.
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ + " LIMIT -1 OFFSET " + maxCallLogSize + ")", null);
+ }
+ Log.i(LOG_TAG, "addEntry: cleaned up " + numDeleted + " old entries");
+
+ return result;
+ } catch (IllegalArgumentException e) {
+ Log.e(LOG_TAG, "Failed to insert calllog", e);
+ // Even though we make sure the target user is running and decrypted before calling
+ // this method, there's a chance that the user just got shut down, in which case
+ // we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
+ return null;
+ }
+ }
+
+ private static Uri maybeInsertLocation(AddCallParams params, ContentResolver resolver,
+ UserHandle user) {
+ if (Double.isNaN(params.mLatitude) || Double.isNaN(params.mLongitude)) {
+ return null;
+ }
+ ContentValues locationValues = new ContentValues();
+ locationValues.put(CallLog.Locations.LATITUDE, params.mLatitude);
+ locationValues.put(CallLog.Locations.LONGITUDE, params.mLongitude);
+ Uri locationUri = ContentProvider.maybeAddUserId(CallLog.Locations.CONTENT_URI,
+ user.getIdentifier());
+ try {
+ return resolver.insert(locationUri, locationValues);
+ } catch (SecurityException e) {
+ // This can happen if the caller doesn't have location permissions. If that's the
+ // case just skip the insertion.
+ Log.w(LOG_TAG, "Skipping inserting location for " + e);
+ return null;
+ }
+ }
+
+ private static void updateDataUsageStatForData(ContentResolver resolver, String dataId) {
+ final Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon()
+ .appendPath(dataId)
+ .appendQueryParameter(DataUsageFeedback.USAGE_TYPE,
+ DataUsageFeedback.USAGE_TYPE_CALL)
+ .build();
+ resolver.update(feedbackUri, new ContentValues(), null, null);
+ }
+
+ /*
+ * Update the normalized phone number for the given dataId in the ContactsProvider, based
+ * on the user's current country.
+ */
+ private static void updateNormalizedNumber(Context context, ContentResolver resolver,
+ String dataId, String number) {
+ if (TextUtils.isEmpty(number) || TextUtils.isEmpty(dataId)) {
+ return;
+ }
+ final String countryIso = getCurrentCountryIso(context);
+ if (TextUtils.isEmpty(countryIso)) {
+ return;
+ }
+ final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (TextUtils.isEmpty(normalizedNumber)) {
+ return;
+ }
+ final ContentValues values = new ContentValues();
+ values.put(Phone.NORMALIZED_NUMBER, normalizedNumber);
+ resolver.update(Data.CONTENT_URI, values, Data._ID + "=?", new String[]{dataId});
+ }
+
+ /**
+ * Remap network specified number presentation types TelecomManager.PRESENTATION_xxx to calllog
+ * number presentation types Calls.PRESENTATION_xxx, in order to insulate the persistent calllog
+ * from any future radio changes. If the number field is empty set the presentation type to
+ * Unknown.
+ */
+ private static int getLogNumberPresentation(String number, int presentation) {
+ if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ return presentation;
+ }
+
+ if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
+ return presentation;
+ }
+
+ if (presentation == TelecomManager.PRESENTATION_UNAVAILABLE) {
+ return PRESENTATION_UNAVAILABLE;
+ }
+
+ if (TextUtils.isEmpty(number)
+ || presentation == TelecomManager.PRESENTATION_UNKNOWN) {
+ return PRESENTATION_UNKNOWN;
+ }
+
+ return PRESENTATION_ALLOWED;
+ }
+
+ private static String getLogAccountAddress(Context context,
+ PhoneAccountHandle accountHandle) {
+ TelecomManager tm = null;
+ try {
+ tm = context.getSystemService(TelecomManager.class);
+ } catch (UnsupportedOperationException e) {
+ if (VERBOSE_LOG) {
+ Log.v(LOG_TAG, "No TelecomManager found to get account address.");
+ }
+ }
+
+ String accountAddress = null;
+ if (tm != null && accountHandle != null) {
+ PhoneAccount account = tm.getPhoneAccount(accountHandle);
+ if (account != null) {
+ Uri address = account.getSubscriptionAddress();
+ if (address != null) {
+ accountAddress = address.getSchemeSpecificPart();
+ }
+ }
+ }
+ return accountAddress;
+ }
+
+ private static String getCurrentCountryIso(Context context) {
+ String countryIso = null;
+ final CountryDetector detector = context.getSystemService(CountryDetector.class);
+ if (detector != null) {
+ final Country country = detector.detectCountry();
+ if (country != null) {
+ countryIso = country.getCountryIso();
+ }
+ }
+ return countryIso;
+ }
+
+ /**
+ * Used as an argument to {@link Calls#addCall(Context, AddCallParams)}.
+ *
+ * Contains details to log about a call.
+ */
+ public static class AddCallParams {
+
+ private final CallerInfo mCallerInfo;
+ private final String mPostDialDigits;
+ private final String mViaNumber;
+ private final int mPresentation;
+ private final int mCallType;
+ private final int mFeatures;
+ private final PhoneAccountHandle mAccountHandle;
+ private final long mStart;
+ private final int mDuration;
+ private final long mDataUsage;
+ private final boolean mAddForAllUsers;
+ private final UserHandle mUserToBeInsertedTo;
+ private final boolean mIsRead;
+ private final int mCallBlockReason;
+ private final CharSequence mCallScreeningAppName;
+ private final String mCallScreeningComponentName;
+ private final long mMissedReason;
+ private final int mPriority;
+ private final String mSubject;
+ private final Uri mPictureUri;
+ private final int mIsPhoneAccountMigrationPending;
+ private String mNumber;
+ private double mLatitude = Double.NaN;
+ private double mLongitude = Double.NaN;
+ private boolean mIsBusinessCall;
+ private String mAssertedDisplayName;
+
+ private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
+ String viaNumber, int presentation, int callType, int features,
+ PhoneAccountHandle accountHandle, long start, int duration, long dataUsage,
+ boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean isRead,
+ int callBlockReason,
+ CharSequence callScreeningAppName, String callScreeningComponentName,
+ long missedReason,
+ int priority, String subject, double latitude, double longitude, Uri pictureUri,
+ int isPhoneAccountMigrationPending) {
+ mCallerInfo = callerInfo;
+ mNumber = number;
+ mPostDialDigits = postDialDigits;
+ mViaNumber = viaNumber;
+ mPresentation = presentation;
+ mCallType = callType;
+ mFeatures = features;
+ mAccountHandle = accountHandle;
+ mStart = start;
+ mDuration = duration;
+ mDataUsage = dataUsage;
+ mAddForAllUsers = addForAllUsers;
+ mUserToBeInsertedTo = userToBeInsertedTo;
+ mIsRead = isRead;
+ mCallBlockReason = callBlockReason;
+ mCallScreeningAppName = callScreeningAppName;
+ mCallScreeningComponentName = callScreeningComponentName;
+ mMissedReason = missedReason;
+ mPriority = priority;
+ mSubject = subject;
+ mLatitude = latitude;
+ mLongitude = longitude;
+ mPictureUri = pictureUri;
+ mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
+ }
+
+ private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
+ String viaNumber, int presentation, int callType, int features,
+ PhoneAccountHandle accountHandle, long start, int duration, long dataUsage,
+ boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean isRead,
+ int callBlockReason,
+ CharSequence callScreeningAppName, String callScreeningComponentName,
+ long missedReason,
+ int priority, String subject, double latitude, double longitude, Uri pictureUri,
+ int isPhoneAccountMigrationPending, boolean isBusinessCall,
+ String assertedDisplayName) {
+ mCallerInfo = callerInfo;
+ mNumber = number;
+ mPostDialDigits = postDialDigits;
+ mViaNumber = viaNumber;
+ mPresentation = presentation;
+ mCallType = callType;
+ mFeatures = features;
+ mAccountHandle = accountHandle;
+ mStart = start;
+ mDuration = duration;
+ mDataUsage = dataUsage;
+ mAddForAllUsers = addForAllUsers;
+ mUserToBeInsertedTo = userToBeInsertedTo;
+ mIsRead = isRead;
+ mCallBlockReason = callBlockReason;
+ mCallScreeningAppName = callScreeningAppName;
+ mCallScreeningComponentName = callScreeningComponentName;
+ mMissedReason = missedReason;
+ mPriority = priority;
+ mSubject = subject;
+ mLatitude = latitude;
+ mLongitude = longitude;
+ mPictureUri = pictureUri;
+ mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
+ mIsBusinessCall = isBusinessCall;
+ mAssertedDisplayName = assertedDisplayName;
+ }
+
+ /**
+ * Builder for the add-call parameters.
+ */
+ public static final class AddCallParametersBuilder {
+
+ public static final int MAX_NUMBER_OF_CHARACTERS = 256;
+ private CallerInfo mCallerInfo;
+ private String mNumber;
+ private String mPostDialDigits;
+ private String mViaNumber;
+ private int mPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ private int mCallType = CallLog.Calls.INCOMING_TYPE;
+ private int mFeatures;
+ private PhoneAccountHandle mAccountHandle;
+ private long mStart;
+ private int mDuration;
+ private Long mDataUsage = Long.MIN_VALUE;
+ private boolean mAddForAllUsers;
+ private UserHandle mUserToBeInsertedTo;
+ private boolean mIsRead;
+ private int mCallBlockReason = CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
+ private CharSequence mCallScreeningAppName;
+ private String mCallScreeningComponentName;
+ private long mMissedReason = CallLog.Calls.MISSED_REASON_NOT_MISSED;
+ private int mPriority = CallLog.Calls.PRIORITY_NORMAL;
+ private String mSubject;
+ private double mLatitude = Double.NaN;
+ private double mLongitude = Double.NaN;
+ private Uri mPictureUri;
+ private int mIsPhoneAccountMigrationPending;
+ private boolean mIsBusinessCall;
+ private String mAssertedDisplayName;
+
+ /**
+ * @param callerInfo the CallerInfo object to get the target contact from.
+ */
+ public @NonNull AddCallParametersBuilder setCallerInfo(
+ @NonNull CallerInfo callerInfo) {
+ mCallerInfo = callerInfo;
+ return this;
+ }
+
+ /**
+ * @param number the phone number to be added to the calls db
+ */
+ public @NonNull AddCallParametersBuilder setNumber(@NonNull String number) {
+ mNumber = number;
+ return this;
+ }
+
+ /**
+ * @param postDialDigits the post-dial digits that were dialed after the number, if
+ * it was outgoing. Otherwise it is ''.
+ */
+ public @NonNull AddCallParametersBuilder setPostDialDigits(
+ @NonNull String postDialDigits) {
+ mPostDialDigits = postDialDigits;
+ return this;
+ }
+
+ /**
+ * @param viaNumber the secondary number that the incoming call received with. If
+ * the call was received with the SIM assigned number, then this field must be ''.
+ */
+ public @NonNull AddCallParametersBuilder setViaNumber(@NonNull String viaNumber) {
+ mViaNumber = viaNumber;
+ return this;
+ }
+
+ /**
+ * @param presentation enum value from TelecomManager.PRESENTATION_xxx, which is set
+ * by the network and denotes the number presenting rules for "allowed", "payphone",
+ * "restricted" or "unknown"
+ */
+ public @NonNull AddCallParametersBuilder setPresentation(int presentation) {
+ mPresentation = presentation;
+ return this;
+ }
+
+ /**
+ * @param callType enumerated values for "incoming", "outgoing", or "missed"
+ */
+ public @NonNull AddCallParametersBuilder setCallType(int callType) {
+ mCallType = callType;
+ return this;
+ }
+
+ /**
+ * @param features features of the call (e.g. Video).
+ */
+ public @NonNull AddCallParametersBuilder setFeatures(int features) {
+ mFeatures = features;
+ return this;
+ }
+
+ /**
+ * @param accountHandle The accountHandle object identifying the provider of the
+ * call
+ */
+ public @NonNull AddCallParametersBuilder setAccountHandle(
+ @NonNull PhoneAccountHandle accountHandle) {
+ mAccountHandle = accountHandle;
+ return this;
+ }
+
+ /**
+ * @param start time stamp for the call in milliseconds
+ */
+ public @NonNull AddCallParametersBuilder setStart(long start) {
+ mStart = start;
+ return this;
+ }
+
+ /**
+ * @param duration call duration in seconds
+ */
+ public @NonNull AddCallParametersBuilder setDuration(int duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * @param dataUsage data usage for the call in bytes or {@link Long#MIN_VALUE} if
+ * data usage was not tracked for the call.
+ */
+ public @NonNull AddCallParametersBuilder setDataUsage(long dataUsage) {
+ mDataUsage = dataUsage;
+ return this;
+ }
+
+ /**
+ * @param addForAllUsers If true, the call is added to the call log of all currently
+ * running users. The caller must have the MANAGE_USERS permission if this is true.
+ */
+ public @NonNull AddCallParametersBuilder setAddForAllUsers(
+ boolean addForAllUsers) {
+ mAddForAllUsers = addForAllUsers;
+ return this;
+ }
+
+ /**
+ * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be
+ * inserted to. null if it is inserted to the current user. The value is ignored if
+ * {@link #setAddForAllUsers} is called with {@code true}.
+ */
+ @SuppressLint("UserHandleName")
+ public @NonNull AddCallParametersBuilder setUserToBeInsertedTo(
+ @NonNull UserHandle userToBeInsertedTo) {
+ mUserToBeInsertedTo = userToBeInsertedTo;
+ return this;
+ }
+
+ /**
+ * @param isRead Flag to show if the missed call log has been read by the user or
+ * not. Used for call log restore of missed calls.
+ */
+ public @NonNull AddCallParametersBuilder setIsRead(boolean isRead) {
+ mIsRead = isRead;
+ return this;
+ }
+
+ /**
+ * @param callBlockReason The reason why the call is blocked.
+ */
+ public @NonNull AddCallParametersBuilder setCallBlockReason(int callBlockReason) {
+ mCallBlockReason = callBlockReason;
+ return this;
+ }
+
+ /**
+ * @param callScreeningAppName The call screening application name which block the
+ * call.
+ */
+ public @NonNull AddCallParametersBuilder setCallScreeningAppName(
+ @NonNull CharSequence callScreeningAppName) {
+ mCallScreeningAppName = callScreeningAppName;
+ return this;
+ }
+
+ /**
+ * @param callScreeningComponentName The call screening component name which blocked
+ * the call.
+ */
+ public @NonNull AddCallParametersBuilder setCallScreeningComponentName(
+ @NonNull String callScreeningComponentName) {
+ mCallScreeningComponentName = callScreeningComponentName;
+ return this;
+ }
+
+ /**
+ * @param missedReason The encoded missed information of the call.
+ */
+ public @NonNull AddCallParametersBuilder setMissedReason(long missedReason) {
+ mMissedReason = missedReason;
+ return this;
+ }
+
+ /**
+ * @param priority The priority of the call, either {@link Calls#PRIORITY_NORMAL} or
+ * {@link Calls#PRIORITY_URGENT} as sent via call composer
+ */
+ public @NonNull AddCallParametersBuilder setPriority(int priority) {
+ mPriority = priority;
+ return this;
+ }
+
+ /**
+ * @param subject The subject as sent via call composer.
+ */
+ public @NonNull AddCallParametersBuilder setSubject(@NonNull String subject) {
+ mSubject = subject;
+ return this;
+ }
+
+ /**
+ * @param latitude Latitude of the location sent via call composer.
+ */
+ public @NonNull AddCallParametersBuilder setLatitude(double latitude) {
+ mLatitude = latitude;
+ return this;
+ }
+
+ /**
+ * @param longitude Longitude of the location sent via call composer.
+ */
+ public @NonNull AddCallParametersBuilder setLongitude(double longitude) {
+ mLongitude = longitude;
+ return this;
+ }
+
+ /**
+ * @param pictureUri {@link Uri} returned from {@link #storeCallComposerPicture}.
+ * Associates that stored picture with this call in the log.
+ */
+ public @NonNull AddCallParametersBuilder setPictureUri(@NonNull Uri pictureUri) {
+ mPictureUri = pictureUri;
+ return this;
+ }
+
+ /**
+ * @param isPhoneAccountMigrationPending whether the phone account migration is
+ * pending
+ */
+ public @NonNull AddCallParametersBuilder setIsPhoneAccountMigrationPending(
+ int isPhoneAccountMigrationPending) {
+ mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
+ return this;
+ }
+
+ /**
+ * @param isBusinessCall should be set if the caller is a business call
+ */
+ public @NonNull AddCallParametersBuilder setIsBusinessCall(boolean isBusinessCall) {
+ mIsBusinessCall = isBusinessCall;
+ return this;
+ }
+
+ /**
+ * @param assertedDisplayName the asserted display name associated with the business
+ * call
+ * @throws IllegalArgumentException if the assertedDisplayName is over 256
+ * characters
+ */
+ public @NonNull AddCallParametersBuilder setAssertedDisplayName(
+ String assertedDisplayName) {
+ if (assertedDisplayName != null
+ && assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) {
+ throw new IllegalArgumentException("assertedDisplayName exceeds the character"
+ + " limit of " + MAX_NUMBER_OF_CHARACTERS + ".");
+ }
+ mAssertedDisplayName = assertedDisplayName;
+ return this;
+ }
+
+ /**
+ * Builds the object
+ */
+ public @NonNull AddCallParams build() {
+ if (Flags.businessCallComposer()) {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending, mIsBusinessCall, mAssertedDisplayName);
+ } else {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/util/CallerInfo.java b/src/com/android/server/telecom/util/CallerInfo.java
new file mode 100644
index 0000000..0cebaae
--- /dev/null
+++ b/src/com/android/server/telecom/util/CallerInfo.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2025 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.server.telecom.util;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.location.Country;
+import android.location.CountryDetector;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.RawContacts;
+import android.telecom.Log;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.android.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+/**
+ * Looks up caller information for the given phone number.
+ */
+public class CallerInfo {
+
+ public static final long USER_TYPE_CURRENT = 0;
+ public static final long USER_TYPE_WORK = 1;
+ private static final String TAG = "CallerInfo";
+ private static final boolean VDBG = Log.VERBOSE;
+ public String normalizedNumber;
+ public String geoDescription;
+ public String cnapName;
+ public int numberPresentation;
+ public int namePresentation;
+ public boolean contactExists;
+ public String phoneLabel;
+ /**
+ * Split up the phoneLabel into number type and label name.
+ */
+ public int numberType;
+ public String numberLabel;
+ public int photoResource;
+ public boolean needUpdate;
+ public Uri contactRefUri;
+ public String lookupKey;
+ public ComponentName preferredPhoneAccountComponent;
+ public String preferredPhoneAccountId;
+ public long userType;
+ // fields to hold individual contact preference data,
+ // including the send to voicemail flag and the ringtone
+ // uri reference.
+ public Uri contactRingtoneUri;
+ public boolean shouldSendToVoicemail;
+ /**
+ * Drawable representing the caller image. This is essentially a cache for the image data tied
+ * into the connection / callerinfo object.
+ *
+ * This might be a high resolution picture which is more suitable for full-screen image view
+ * than for smaller icons used in some kinds of notifications.
+ *
+ * The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Drawable cachedPhoto;
+ /**
+ * Bitmap representing the caller image which has possibly lower resolution than
+ * {@link #cachedPhoto} and thus more suitable for icons (like notification icons).
+ *
+ * In usual cases this is just down-scaled image of {@link #cachedPhoto}. If the down-scaling
+ * fails, this will just become null.
+ *
+ * The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Bitmap cachedPhotoIcon;
+ /**
+ * Boolean which indicates if {@link #cachedPhoto} and {@link #cachedPhotoIcon} is fresh enough.
+ * If it is false, those images aren't pointing to valid objects.
+ */
+ public boolean isCachedPhotoCurrent;
+ /**
+ * Please note that, any one of these member variables can be null, and any accesses to them
+ * should be prepared to handle such a case.
+ *
+ * Also, it is implied that phoneNumber is more often populated than name is, (think of calls
+ * being dialed/received using numbers where names are not known to the device), so phoneNumber
+ * should serve as a dependable fallback when name is unavailable.
+ *
+ * One other detail here is that this CallerInfo object reflects information found on a
+ * connection, it is an OUTPUT that serves mainly to display information to the user. In no way
+ * is this object used as input to make a connection, so we can choose to display whatever
+ * human-readable text makes sense to the user for a connection. This is especially relevant
+ * for the phone number field, since it is the one field that is most likely exposed to the
+ * user.
+ *
+ * As an example: 1. User dials "911" 2. Device recognizes that this is an emergency number 3.
+ * We use the "Emergency Number" string instead of "911" in the phoneNumber field.
+ *
+ * What we're really doing here is treating phoneNumber as an essential field here, NOT name.
+ * We're NOT always guaranteed to have a name for a connection, but the number should be
+ * displayable.
+ */
+ private String name;
+ private String phoneNumber;
+ // Contact ID, which will be 0 if a contact comes from the corp CP2.
+ private long contactIdOrZero;
+ /**
+ * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be the
+ * thumbnail URI instead.
+ */
+ private Uri contactDisplayPhotoUri;
+ private boolean mIsEmergency;
+ private boolean mIsVoiceMail;
+
+ public CallerInfo() {
+ // TODO: Move all the basic initialization here?
+ mIsEmergency = false;
+ mIsVoiceMail = false;
+ userType = USER_TYPE_CURRENT;
+ }
+
+ /**
+ * getCallerInfo given a Cursor.
+ *
+ * @param context the context used to retrieve string constants
+ * @param contactRef the URI to attach to this CallerInfo object
+ * @param cursor the first object in the cursor is used to build the CallerInfo object.
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) {
+ CallerInfo info = new CallerInfo();
+ info.photoResource = 0;
+ info.phoneLabel = null;
+ info.numberType = 0;
+ info.numberLabel = null;
+ info.cachedPhoto = null;
+ info.isCachedPhotoCurrent = false;
+ info.contactExists = false;
+ info.userType = USER_TYPE_CURRENT;
+
+ if (VDBG) {
+ Log.v(TAG, "getCallerInfo() based on cursor...");
+ }
+
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ // TODO: photo_id is always available but not taken
+ // care of here. Maybe we should store it in the
+ // CallerInfo object as well.
+
+ int columnIndex;
+
+ // Look for the name
+ columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+ if (columnIndex != -1) {
+ info.name = cursor.getString(columnIndex);
+ }
+
+ // Look for the number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
+ if (columnIndex != -1) {
+ info.phoneNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the normalized number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
+ if (columnIndex != -1) {
+ info.normalizedNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the label/type combo
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL);
+ if (columnIndex != -1) {
+ int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE);
+ if (typeColumnIndex != -1) {
+ info.numberType = cursor.getInt(typeColumnIndex);
+ info.numberLabel = cursor.getString(columnIndex);
+ info.phoneLabel = Phone.getDisplayLabel(context,
+ info.numberType, info.numberLabel)
+ .toString();
+ }
+ }
+
+ // Look for the person_id.
+ columnIndex = getColumnIndexForPersonId(contactRef, cursor);
+ if (columnIndex != -1) {
+ final long contactId = cursor.getLong(columnIndex);
+ if (contactId != 0 && !Contacts.isEnterpriseContactId(contactId)) {
+ info.contactIdOrZero = contactId;
+ if (VDBG) {
+ Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero);
+ }
+ }
+ if (Contacts.isEnterpriseContactId(contactId)) {
+ info.userType = USER_TYPE_WORK;
+ }
+ } else {
+ // No valid columnIndex, so we can't look up person_id.
+ Log.w(TAG, "Couldn't find contact_id column for " + contactRef);
+ // Watch out: this means that anything that depends on
+ // person_id will be broken (like contact photo lookups in
+ // the in-call UI, for example.)
+ }
+
+ // Contact lookupKey
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+ if (columnIndex != -1) {
+ info.lookupKey = cursor.getString(columnIndex);
+ }
+
+ // Display photo URI.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex));
+ } else {
+ info.contactDisplayPhotoUri = null;
+ }
+
+ columnIndex = cursor.getColumnIndex(Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ info.preferredPhoneAccountComponent =
+ ComponentName.unflattenFromString(cursor.getString(columnIndex));
+ }
+
+ columnIndex = cursor.getColumnIndex(Data.PREFERRED_PHONE_ACCOUNT_ID);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ info.preferredPhoneAccountId = cursor.getString(columnIndex);
+ }
+
+ // look for the custom ringtone, create from the string stored
+ // in the database.
+ // An empty string ("") in the database indicates a silent ringtone,
+ // and we set contactRingtoneUri = Uri.EMPTY, so that no ringtone will be played.
+ // {null} in the database indicates the default ringtone,
+ // and we set contactRingtoneUri = null, so that default ringtone will be played.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
+ info.contactRingtoneUri = Uri.EMPTY;
+ } else {
+ info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex));
+ }
+ } else {
+ info.contactRingtoneUri = null;
+ }
+
+ // look for the send to voicemail flag, set it to true only
+ // under certain circumstances.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL);
+ info.shouldSendToVoicemail = (columnIndex != -1) &&
+ ((cursor.getInt(columnIndex)) == 1);
+ info.contactExists = true;
+ }
+ cursor.close();
+ cursor = null;
+ }
+
+ info.needUpdate = false;
+ info.name = normalize(info.name);
+ info.contactRefUri = contactRef;
+
+ return info;
+ }
+
+ /**
+ * getCallerInfo given a URI, look up in the call-log database for the uri unique key.
+ *
+ * @param context the context used to get the ContentResolver
+ * @param contactRef the URI used to lookup caller id
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ public static CallerInfo getCallerInfo(Context context, Uri contactRef) {
+ CallerInfo info = null;
+ ContentResolver cr = CallerInfoAsyncQuery.getCurrentProfileContentResolver(context);
+ if (cr != null) {
+ try {
+ info = getCallerInfo(context, contactRef,
+ cr.query(contactRef, null, null, null, null));
+ } catch (RuntimeException re) {
+ Log.e(TAG, re, "Error getting caller info.");
+ }
+ }
+ return info;
+ }
+
+ /**
+ * getCallerInfo given a phone number, look up in the call-log database for the matching caller
+ * id info.
+ *
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number used to lookup caller id
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied. If a matching number is not found, then a
+ * generic caller info is returned, with all relevant fields empty or null.
+ */
+ public static CallerInfo getCallerInfo(Context context, String number) {
+ if (VDBG) {
+ Log.v(TAG, "getCallerInfo() based on number...");
+ }
+
+ int subId = SubscriptionManager.getDefaultSubscriptionId();
+ return getCallerInfo(context, number, subId);
+ }
+
+ /**
+ * getCallerInfo given a phone number and subscription, look up in the call-log database for the
+ * matching caller id info.
+ *
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number used to lookup caller id
+ * @param subId the subscription for checking for if voice mail number or not
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied. If a matching number is not found, then a
+ * generic caller info is returned, with all relevant fields empty or null.
+ */
+ public static CallerInfo getCallerInfo(Context context, String number, int subId) {
+
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ // Change the callerInfo number ONLY if it is an emergency number
+ // or if it is the voicemail number. If it is either, take a
+ // shortcut and skip the query.
+ TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+ if (tm.isEmergencyNumber(number)) {
+ return new CallerInfo().markAsEmergency(context);
+ } else if (PhoneNumberUtils.isVoiceMailNumber(null, subId, number)) {
+ return new CallerInfo().markAsVoiceMail(context, subId);
+ }
+
+ Uri contactUri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
+ Uri.encode(number));
+
+ CallerInfo info = getCallerInfo(context, contactUri);
+ info = doSecondaryLookupIfNecessary(context, number, info);
+
+ // if no query results were returned with a viable number,
+ // fill in the original number value we used to query with.
+ if (TextUtils.isEmpty(info.phoneNumber)) {
+ info.phoneNumber = number;
+ }
+
+ return info;
+ }
+
+ /**
+ * Performs another lookup if previous lookup fails and it's a SIP call and the peer's username
+ * is all numeric. Look up the username as it could be a PSTN number in the contact database.
+ *
+ * @param context the query context
+ * @param number the original phone number, could be a SIP URI
+ * @param previousResult the result of previous lookup
+ * @return previousResult if it's not the case
+ */
+ static CallerInfo doSecondaryLookupIfNecessary(Context context,
+ String number, CallerInfo previousResult) {
+ if (!previousResult.contactExists
+ && PhoneNumberUtils.isUriNumber(number)) {
+ String username = PhoneNumberUtils.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ previousResult = getCallerInfo(context,
+ Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
+ Uri.encode(username)));
+ }
+ }
+ return previousResult;
+ }
+
+ private static String normalize(String s) {
+ if (s == null || s.length() > 0) {
+ return s;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the column index to use to find the "person_id" field in the specified cursor, based
+ * on the contact URI that was originally queried.
+ *
+ * This is a helper function for the getCallerInfo() method that takes a Cursor. Looking up the
+ * person_id is nontrivial (compared to all the other CallerInfo fields) since the column we
+ * need to use depends on what query we originally ran.
+ *
+ * Watch out: be sure to not do any database access in this method, since it's run from the UI
+ * thread (see comments below for more info.)
+ *
+ * @return the columnIndex to use (with cursor.getLong()) to get the person_id, or -1 if we
+ * couldn't figure out what column to use.
+ *
+ * TODO: Add a unittest for this method. (This is a little tricky to
+ * test, since we'll need a live contacts database to test against,
+ * preloaded with at least some phone numbers and SIP addresses. And
+ * we'll probably have to hardcode the column indexes we expect, so
+ * the test might break whenever the contacts schema changes. But we
+ * can at least make sure we handle all the URI patterns we claim to,
+ * and that the mime types match what we expect...)
+ */
+ private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) {
+ // TODO: This is pretty ugly now, see bug 2269240 for
+ // more details. The column to use depends upon the type of URL:
+ // - content://com.android.contacts/data/phones ==> use the "contact_id" column
+ // - content://com.android.contacts/phone_lookup ==> use the "_ID" column
+ // - content://com.android.contacts/data ==> use the "contact_id" column
+ // If it's none of the above, we leave columnIndex=-1 which means
+ // that the person_id field will be left unset.
+ //
+ // The logic here *used* to be based on the mime type of contactRef
+ // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the
+ // RawContacts.CONTACT_ID column). But looking up the mime type requires
+ // a call to context.getContentResolver().getType(contactRef), which
+ // isn't safe to do from the UI thread since it can cause an ANR if
+ // the contacts provider is slow or blocked (like during a sync.)
+ //
+ // So instead, figure out the column to use for person_id by just
+ // looking at the URI itself.
+
+ if (VDBG) {
+ Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '"
+ + contactRef + "'...");
+ }
+ // Warning: Do not enable the following logging (due to ANR risk.)
+ // if (VDBG) Log.v(TAG, "- MIME type: "
+ // + context.getContentResolver().getType(contactRef));
+
+ String url = contactRef.toString();
+ String columnName = null;
+ if (url.startsWith("content://com.android.contacts/data/phones")) {
+ // Direct lookup in the Phone table.
+ // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2")
+ if (VDBG) {
+ Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID");
+ }
+ columnName = RawContacts.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/data")) {
+ // Direct lookup in the Data table.
+ // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data")
+ if (VDBG) {
+ Log.v(TAG, "'data' URI; using Data.CONTACT_ID");
+ }
+ // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.)
+ columnName = Data.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/phone_lookup")) {
+ // Lookup in the PhoneLookup table, which provides "fuzzy matching"
+ // for phone numbers.
+ // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup")
+ if (VDBG) {
+ Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID");
+ }
+ columnName = PhoneLookup._ID;
+ } else {
+ Log.w(TAG, "Unexpected prefix for contactRef '" + url + "'");
+ }
+ int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1;
+ if (VDBG) {
+ Log.v(TAG, "==> Using column '" + columnName
+ + "' (columnIndex = " + columnIndex + ") for person_id lookup...");
+ }
+ return columnIndex;
+ }
+
+ /**
+ * @return a geographical description string for the specified number.
+ * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+ */
+ public static String getGeoDescription(Context context, String number) {
+ if (VDBG) {
+ Log.v(TAG, "getGeoDescription('" + number + "')...");
+ }
+
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+
+ Locale locale = context.getResources().getConfiguration().locale;
+ String countryIso = getCurrentCountryIso(context, locale);
+ PhoneNumber pn = null;
+ try {
+ if (VDBG) {
+ Log.v(TAG, "parsing '" + number
+ + "' for countryIso '" + countryIso + "'...");
+ }
+ pn = util.parse(number, countryIso);
+ if (VDBG) {
+ Log.v(TAG, "- parsed number: " + pn);
+ }
+ } catch (NumberParseException e) {
+ Log.w(TAG, "getGeoDescription: NumberParseException for incoming number '"
+ + Log.pii(number) + "'");
+ }
+
+ if (pn != null) {
+ String description = geocoder.getDescriptionForNumber(pn, locale);
+ if (VDBG) {
+ Log.v(TAG, "- got description: '" + description + "'");
+ }
+ return description;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return The ISO 3166-1 two letters country code of the country the user is in.
+ */
+ private static String getCurrentCountryIso(Context context, Locale locale) {
+ String countryIso = null;
+ CountryDetector detector = context.getSystemService(CountryDetector.class);
+ if (detector != null) {
+ Country country = detector.detectCountry();
+ if (country != null) {
+ countryIso = country.getCountryIso();
+ } else {
+ Log.e(TAG, new Exception(), "CountryDetector.detectCountry() returned null.");
+ }
+ }
+ if (countryIso == null) {
+ countryIso = locale.getCountry();
+ Log.w(TAG, "No CountryDetector; falling back to countryIso based on locale: "
+ + countryIso);
+ }
+ return countryIso;
+ }
+
+ protected static String getCurrentCountryIso(Context context) {
+ return getCurrentCountryIso(context, Locale.getDefault());
+ }
+
+ /**
+ * @return Name associated with this caller.
+ */
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set caller Info Name.
+ *
+ * @param name caller Info Name
+ */
+ public void setName(@Nullable String name) {
+ this.name = name;
+ }
+
+ // Accessors
+
+ /**
+ * @return Phone number associated with this caller.
+ */
+ @Nullable
+ public String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ public void setPhoneNumber(String number) {
+ phoneNumber = number;
+ }
+
+ /**
+ * @return Contact ID, which will be 0 if a contact comes from the corp Contacts Provider.
+ */
+ public long getContactId() {
+ return contactIdOrZero;
+ }
+
+ /**
+ * @return Contact display photo URI. If a contact has no display photo but a thumbnail, it'll
+ * the thumbnail URI instead.
+ */
+ @Nullable
+ public Uri getContactDisplayPhotoUri() {
+ return contactDisplayPhotoUri;
+ }
+
+ @VisibleForTesting
+ public void SetContactDisplayPhotoUri(Uri photoUri) {
+ contactDisplayPhotoUri = photoUri;
+ }
+
+ /**
+ * @return true if the caller info is an emergency number.
+ */
+ public boolean isEmergencyNumber() {
+ return mIsEmergency;
+ }
+
+ /**
+ * @return true if the caller info is a voicemail number
+ */
+ public boolean isVoiceMailNumber() {
+ return mIsVoiceMail;
+ }
+
+ /**
+ * Mark this CallerInfo as an emergency call.
+ *
+ * @param context To lookup the localized 'Emergency Number' string.
+ * @return this instance.
+ */
+ // TODO: Note we're setting the phone number here (refer to
+ // javadoc comments at the top of CallerInfo class) to a localized
+ // string 'Emergency Number'. This is pretty bad because we are
+ // making UI work here instead of just packaging the data. We
+ // should set the phone number to the dialed number and name to
+ // 'Emergency Number' and let the UI make the decision about what
+ // should be displayed.
+ /* package */ CallerInfo markAsEmergency(Context context) {
+ phoneNumber = context.getString(
+ com.android.internal.R.string.emergency_call_dialog_number_for_display);
+ photoResource = com.android.internal.R.drawable.picture_emergency;
+ mIsEmergency = true;
+ return this;
+ }
+
+ /* package */ CallerInfo markAsVoiceMail(Context context, int subId) {
+ mIsVoiceMail = true;
+
+ try {
+ phoneNumber = context.getSystemService(TelephonyManager.class)
+ .createForSubscriptionId(subId)
+ .getVoiceMailAlphaTag();
+ } catch (SecurityException se) {
+ // Should never happen: if this process does not have
+ // permission to retrieve VM tag, it should not have
+ // permission to retrieve VM number and would not call
+ // this method.
+ // Leave phoneNumber untouched.
+ Log.e(TAG, se, "Cannot access VoiceMail.");
+ }
+ // TODO: There is no voicemail picture?
+ // FIXME: FIND ANOTHER ICON
+ // photoResource = android.R.drawable.badge_voicemail;
+ return this;
+ }
+
+ /**
+ * Updates this CallerInfo's geoDescription field, based on the raw phone number in the
+ * phoneNumber field.
+ *
+ * (Note that the various getCallerInfo() methods do *not* set the geoDescription automatically;
+ * you need to call this method explicitly to get it.)
+ *
+ * @param context the context used to look up the current locale / country
+ * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, this specifies a
+ * fallback number to use instead.
+ */
+ public void updateGeoDescription(Context context, String fallbackNumber) {
+ String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber;
+ geoDescription = getGeoDescription(context, number);
+ }
+
+ /**
+ * @return a string debug representation of this instance.
+ */
+ @Override
+ public String toString() {
+ // Warning: never check in this file with VERBOSE_DEBUG = true
+ // because that will result in PII in the system log.
+ final boolean VERBOSE_DEBUG = false;
+
+ if (VERBOSE_DEBUG) {
+ return super.toString() + " { "
+ + "\nname: " + name
+ + "\nphoneNumber: " + phoneNumber
+ + "\nnormalizedNumber: " + normalizedNumber
+ + "\ngeoDescription: " + geoDescription
+ + "\ncnapName: " + cnapName
+ + "\nnumberPresentation: " + numberPresentation
+ + "\nnamePresentation: " + namePresentation
+ + "\ncontactExits: " + contactExists
+ + "\nphoneLabel: " + phoneLabel
+ + "\nnumberType: " + numberType
+ + "\nnumberLabel: " + numberLabel
+ + "\nphotoResource: " + photoResource
+ + "\ncontactIdOrZero: " + contactIdOrZero
+ + "\nneedUpdate: " + needUpdate
+ + "\ncontactRingtoneUri: " + contactRingtoneUri
+ + "\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri
+ + "\nshouldSendToVoicemail: " + shouldSendToVoicemail
+ + "\ncachedPhoto: " + cachedPhoto
+ + "\nisCachedPhotoCurrent: " + isCachedPhotoCurrent
+ + "\nemergency: " + mIsEmergency
+ + "\nvoicemail " + mIsVoiceMail
+ + "\ncontactExists " + contactExists
+ + "\nuserType " + userType
+ + " }";
+ } else {
+ return super.toString() + " { "
+ + "name " + ((name == null) ? "null" : "non-null")
+ + ", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null")
+ + " }";
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/util/CallerInfoAsyncQuery.java b/src/com/android/server/telecom/util/CallerInfoAsyncQuery.java
new file mode 100644
index 0000000..7109db9
--- /dev/null
+++ b/src/com/android/server/telecom/util/CallerInfoAsyncQuery.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2014 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.server.telecom.util;
+
+import android.app.ActivityManager;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telecom.Log;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to make it easier to run asynchronous caller-id lookup queries.
+ *
+ * @see CallerInfo
+ */
+public class CallerInfoAsyncQuery {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "CallerInfoAsyncQuery";
+
+ private static final int EVENT_NEW_QUERY = 1;
+ private static final int EVENT_ADD_LISTENER = 2;
+ private static final int EVENT_END_OF_QUEUE = 3;
+ private static final int EVENT_EMERGENCY_NUMBER = 4;
+ private static final int EVENT_VOICEMAIL_NUMBER = 5;
+ private static final int EVENT_GET_GEO_DESCRIPTION = 6;
+ // If the CallerInfo query finds no contacts, should we use the
+ // PhoneNumberOfflineGeocoder to look up a "geo description"?
+ // (TODO: This could become a flag in config.xml if it ever needs to be
+ // configured on a per-product basis.)
+ private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
+ private CallerInfoAsyncQueryHandler mHandler;
+
+ /**
+ * Private constructor for factory methods.
+ */
+ private CallerInfoAsyncQuery() {
+ }
+
+ /**
+ * @return {@link ContentResolver} for the "current" user.
+ */
+ static ContentResolver getCurrentProfileContentResolver(Context context) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "Trying to get current content resolver...");
+ }
+
+ final int currentUser = ActivityManager.getCurrentUser();
+ final int myUser = UserHandle.myUserId();
+
+ if (DBG) {
+ Log.d(LOG_TAG, "myUser=" + myUser + "currentUser=" + currentUser);
+ }
+
+ if (myUser != currentUser) {
+ final Context otherContext;
+ try {
+ otherContext = context.createPackageContextAsUser(context.getPackageName(),
+ /* flags =*/ 0, UserHandle.of(currentUser));
+ return otherContext.getContentResolver();
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, e, "Can't find self package");
+ // Fall back to the primary user.
+ }
+ }
+ return context.getContentResolver();
+ }
+
+ /**
+ * Factory method to start query with a Uri query spec
+ */
+ public static CallerInfoAsyncQuery startQuery(int token, Context context, Uri contactRef,
+ OnQueryCompleteListener listener, Object cookie) {
+
+ CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
+ c.allocate(context, contactRef);
+
+ if (DBG) {
+ Log.d(LOG_TAG, "starting query for URI: " + contactRef + " handler: " + c);
+ }
+
+ //create cookieWrapper, start query
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.event = EVENT_NEW_QUERY;
+
+ c.mHandler.startQuery(token, cw, contactRef, null, null, null, null);
+
+ return c;
+ }
+
+ /**
+ * Factory method to start the query based on a number.
+ *
+ * Note: if the number contains an "@" character we treat it as a SIP address, and look it up
+ * directly in the Data table rather than using the PhoneLookup table.
+ * TODO: But eventually we should expose two separate methods, one for
+ * numbers and one for SIP addresses, and then have
+ * PhoneUtils.startGetCallerInfo() decide which one to call based on
+ * the phone type of the incoming connection.
+ */
+ public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
+ OnQueryCompleteListener listener, Object cookie) {
+
+ int subId = SubscriptionManager.getDefaultSubscriptionId();
+ return startQuery(token, context, number, listener, cookie, subId);
+ }
+
+ /**
+ * Factory method to start the query based on a number with specific subscription.
+ *
+ * Note: if the number contains an "@" character we treat it as a SIP address, and look it up
+ * directly in the Data table rather than using the PhoneLookup table.
+ * TODO: But eventually we should expose two separate methods, one for
+ * numbers and one for SIP addresses, and then have
+ * PhoneUtils.startGetCallerInfo() decide which one to call based on
+ * the phone type of the incoming connection.
+ */
+ public static CallerInfoAsyncQuery startQuery(int token, Context context, String number,
+ OnQueryCompleteListener listener, Object cookie, int subId) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startQuery()... #####");
+ Log.d(LOG_TAG, "- number: " + /*number*/ "xxxxxxx");
+ Log.d(LOG_TAG, "- cookie: " + cookie);
+ }
+
+ // Construct the URI object and query params, and start the query.
+
+ final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon()
+ .appendPath(number)
+ .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(PhoneNumberUtils.isUriNumber(number)))
+ .build();
+
+ if (DBG) {
+ Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
+ }
+
+ CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
+ c.allocate(context, contactRef);
+
+ //create cookieWrapper, start query
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.number = number;
+ cw.subId = subId;
+
+ // check to see if these are recognized numbers, and use shortcuts if we can.
+ TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+
+ boolean isEmergencyNumber = false;
+ try {
+ isEmergencyNumber = tm.isEmergencyNumber(number);
+ } catch (IllegalStateException | UnsupportedOperationException ise) {
+ // Ignore the exception that Telephony is not up. Use PhoneNumberUtils API now.
+ // Ideally the PhoneNumberUtils API needs to be removed once the
+ // telphony service not up issue can be fixed (b/187412989)
+ // UnsupportedOperationException: telephony.calling may not be supported on this device
+ isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(context, number);
+ }
+
+ boolean isVoicemailNumber;
+ try {
+ isVoicemailNumber = PhoneNumberUtils.isVoiceMailNumber(context, subId, number);
+ } catch (UnsupportedOperationException ex) {
+ isVoicemailNumber = false;
+ }
+
+ if (isEmergencyNumber) {
+ cw.event = EVENT_EMERGENCY_NUMBER;
+ } else if (isVoicemailNumber) {
+ cw.event = EVENT_VOICEMAIL_NUMBER;
+ } else {
+ cw.event = EVENT_NEW_QUERY;
+ }
+
+ c.mHandler.startQuery(token,
+ cw, // cookie
+ contactRef, // uri
+ null, // projection
+ null, // selection
+ null, // selectionArgs
+ null); // orderBy
+ return c;
+ }
+
+ private static String sanitizeUriToString(Uri uri) {
+ if (uri != null) {
+ String uriString = uri.toString();
+ int indexOfLastSlash = uriString.lastIndexOf('/');
+ if (indexOfLastSlash > 0) {
+ return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
+ } else {
+ return uriString;
+ }
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Method to add listeners to a currently running query
+ */
+ public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "adding listener to query: "
+ + sanitizeUriToString(mHandler.mQueryUri) + " handler: " + mHandler.toString());
+ }
+
+ //create cookieWrapper, add query request to end of queue.
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.event = EVENT_ADD_LISTENER;
+
+ mHandler.startQuery(token, cw, null, null, null, null, null);
+ }
+
+ /**
+ * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct state of context
+ * and uri.
+ */
+ private void allocate(Context context, Uri contactRef) {
+ if ((context == null) || (contactRef == null)) {
+ throw new QueryPoolException("Bad context or query uri.");
+ }
+ mHandler = new CallerInfoAsyncQueryHandler(context);
+ mHandler.mQueryUri = contactRef;
+ }
+
+ /**
+ * Releases the relevant data.
+ */
+ private void release() {
+ mHandler.mContext = null;
+ mHandler.mQueryUri = null;
+ mHandler.mCallerInfo = null;
+ mHandler = null;
+ }
+
+ /**
+ * Interface for a CallerInfoAsyncQueryHandler result return.
+ */
+ public interface OnQueryCompleteListener {
+
+ /**
+ * Called when the query is complete.
+ */
+ void onQueryComplete(int token, Object cookie, CallerInfo ci);
+ }
+
+ /**
+ * Wrap the cookie from the WorkerArgs with additional information needed by our classes.
+ */
+ private static final class CookieWrapper {
+
+ public OnQueryCompleteListener listener;
+ public Object cookie;
+ public int event;
+ public String number;
+ public String geoDescription;
+ public int subId;
+
+ private CookieWrapper() {
+ }
+ }
+
+ /**
+ * Simple exception used to communicate problems with the query pool.
+ */
+ public static class QueryPoolException extends SQLException {
+
+ public QueryPoolException(String error) {
+ super(error);
+ }
+ }
+
+ /**
+ * Our own implementation of the AsyncQueryHandler.
+ */
+ private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
+
+ /*
+ * The information relevant to each CallerInfo query. Each query may have multiple
+ * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper
+ * objects in the queue (one with a new query event, and one with a end event, with
+ * 0 or more additional listeners in between).
+ */
+
+ /**
+ * Context passed by the caller.
+ *
+ * NOTE: The actual context we use for query may *not* be this context; since we query
+ * against the "current" contacts provider. In the constructor we pass the "current"
+ * context resolver (obtained via
+ * {@link #getCurrentProfileContentResolver} and pass it to the super class.
+ */
+ private Context mContext;
+ private Uri mQueryUri;
+ private CallerInfo mCallerInfo;
+ private final List<Runnable> mPendingListenerCallbacks = new ArrayList<>();
+
+ /**
+ * Asynchronous query handler class for the contact / callerinfo object.
+ */
+ private CallerInfoAsyncQueryHandler(Context context) {
+ super(getCurrentProfileContentResolver(context));
+ mContext = context;
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ return new CallerInfoWorkerHandler(looper);
+ }
+
+ /**
+ * Overrides onQueryComplete from AsyncQueryHandler.
+ *
+ * This method takes into account the state of this class; we construct the CallerInfo
+ * object only once for each set of listeners. When the query thread has done its work and
+ * calls this method, we inform the remaining listeners in the queue, until we're out of
+ * listeners. Once we get the message indicating that we should expect no new listeners for
+ * this CallerInfo object, we release the AsyncCursorInfo back into the pool.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ Log.d(LOG_TAG, "##### onQueryComplete() ##### query complete for token: " + token);
+
+ //get the cookie and notify the listener.
+ CookieWrapper cw = (CookieWrapper) cookie;
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that calls this method, we should
+ // check the parameters to make sure they're viable.
+ Log.i(LOG_TAG, "Cookie is null, ignoring onQueryComplete() request.");
+ if (cursor != null) {
+ cursor.close();
+ }
+ return;
+ }
+
+ if (cw.event == EVENT_END_OF_QUEUE) {
+ for (Runnable r : mPendingListenerCallbacks) {
+ r.run();
+ }
+ mPendingListenerCallbacks.clear();
+
+ release();
+ if (cursor != null) {
+ cursor.close();
+ }
+ return;
+ }
+
+ // If the cw.event == EVENT_GET_GEO_DESCRIPTION, means it would not be the 1st
+ // time entering the onQueryComplete(), mCallerInfo should not be null.
+ if (cw.event == EVENT_GET_GEO_DESCRIPTION) {
+ if (mCallerInfo != null) {
+ mCallerInfo.geoDescription = cw.geoDescription;
+ }
+ // notify that we can clean up the queue after this.
+ CookieWrapper endMarker = new CookieWrapper();
+ endMarker.event = EVENT_END_OF_QUEUE;
+ startQuery(token, endMarker, null, null, null, null, null);
+ }
+
+ // check the token and if needed, create the callerinfo object.
+ if (mCallerInfo == null) {
+ if ((mContext == null) || (mQueryUri == null)) {
+ throw new QueryPoolException
+ ("Bad context or query uri, or CallerInfoAsyncQuery already released.");
+ }
+
+ // adjust the callerInfo data as needed, and only if it was set from the
+ // initial query request.
+ // Change the callerInfo number ONLY if it is an emergency number or the
+ // voicemail number, and adjust other data (including photoResource)
+ // accordingly.
+ if (cw.event == EVENT_EMERGENCY_NUMBER) {
+ // Note we're setting the phone number here (refer to javadoc
+ // comments at the top of CallerInfo class).
+ mCallerInfo = new CallerInfo().markAsEmergency(mContext);
+ } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
+ mCallerInfo = new CallerInfo().markAsVoiceMail(mContext, cw.subId);
+ } else {
+ mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor);
+ if (DBG) {
+ Log.d(LOG_TAG, "==> Got mCallerInfo: " + mCallerInfo);
+ }
+
+ CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary(
+ mContext, cw.number, mCallerInfo);
+ if (newCallerInfo != mCallerInfo) {
+ mCallerInfo = newCallerInfo;
+ if (DBG) {
+ Log.d(LOG_TAG, "#####async contact look up with numeric username"
+ + mCallerInfo);
+ }
+ }
+
+ // Use the number entered by the user for display.
+ if (!TextUtils.isEmpty(cw.number)) {
+ mCallerInfo.setPhoneNumber(PhoneNumberUtils.formatNumber(cw.number,
+ mCallerInfo.normalizedNumber,
+ CallerInfo.getCurrentCountryIso(mContext)));
+ }
+
+ // This condition refer to the google default code for geo.
+ // If the number exists in Contacts, the CallCard would never show
+ // the geo description, so it would be unnecessary to query it.
+ if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
+ if (TextUtils.isEmpty(mCallerInfo.getName())) {
+ if (DBG) {
+ Log.d(LOG_TAG, "start querying geo description");
+ }
+ cw.event = EVENT_GET_GEO_DESCRIPTION;
+ startQuery(token, cw, null, null, null, null, null);
+ return;
+ }
+ }
+ }
+
+ if (DBG) {
+ Log.d(LOG_TAG, "constructing CallerInfo object for token: " + token);
+ }
+
+ //notify that we can clean up the queue after this.
+ CookieWrapper endMarker = new CookieWrapper();
+ endMarker.event = EVENT_END_OF_QUEUE;
+ startQuery(token, endMarker, null, null, null, null, null);
+ }
+
+ //notify the listener that the query is complete.
+ if (cw.listener != null) {
+ mPendingListenerCallbacks.add(new Runnable() {
+ @Override
+ public void run() {
+ if (DBG) {
+ Log.d(LOG_TAG, "notifying listener: "
+ + cw.listener.getClass() + " for token: " + token
+ + mCallerInfo);
+ }
+ cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
+ }
+ });
+ } else {
+ Log.w(LOG_TAG, "There is no listener to notify for this query.");
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Our own query worker thread.
+ *
+ * This thread handles the messages enqueued in the looper. The normal sequence of events
+ * is that a new query shows up in the looper queue, followed by 0 or more add listener
+ * requests, and then an end request. Of course, these requests can be interlaced with
+ * requests from other tokens, but is irrelevant to this handler since the handler has no
+ * state.
+ *
+ * Note that we depend on the queue to keep things in order; in other words, the looper
+ * queue must be FIFO with respect to input from the synchronous startQuery calls and output
+ * to this handleMessage call.
+ *
+ * This use of the queue is required because CallerInfo objects may be accessed multiple
+ * times before the query is complete. All accesses (listeners) must be queued up and
+ * informed in order when the query is complete.
+ */
+ protected class CallerInfoWorkerHandler extends WorkerHandler {
+
+ public CallerInfoWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ CookieWrapper cw = (CookieWrapper) args.cookie;
+
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that this Handler calls (such as in
+ // super.handleMessage) that DOES place unexpected messages on the
+ // queue, then we need pass these messages on.
+ Log.i(LOG_TAG, "Unexpected command (CookieWrapper is null): " + msg.what +
+ " ignored by CallerInfoWorkerHandler, passing onto parent.");
+
+ super.handleMessage(msg);
+ } else {
+
+ Log.d(LOG_TAG, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 +
+ " command: " + msg.what + " query URI: " + sanitizeUriToString(args.uri));
+
+ switch (cw.event) {
+ case EVENT_NEW_QUERY:
+ //start the sql command.
+ super.handleMessage(msg);
+ break;
+
+ // shortcuts to avoid query for recognized numbers.
+ case EVENT_EMERGENCY_NUMBER:
+ case EVENT_VOICEMAIL_NUMBER:
+
+ case EVENT_ADD_LISTENER:
+ case EVENT_END_OF_QUEUE:
+ // query was already completed, so just send the reply.
+ // passing the original token value back to the caller
+ // on top of the event values in arg1.
+ Message reply = args.handler.obtainMessage(msg.what);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+
+ reply.sendToTarget();
+
+ break;
+ case EVENT_GET_GEO_DESCRIPTION:
+ handleGeoDescription(msg);
+ break;
+ default:
+ }
+ }
+ }
+
+ private void handleGeoDescription(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ CookieWrapper cw = (CookieWrapper) args.cookie;
+ if (!TextUtils.isEmpty(cw.number) && cw.cookie != null && mContext != null) {
+ final long startTimeMillis = SystemClock.elapsedRealtime();
+ cw.geoDescription = CallerInfo.getGeoDescription(mContext, cw.number);
+ final long duration = SystemClock.elapsedRealtime() - startTimeMillis;
+ if (duration > 500) {
+ if (DBG) {
+ Log.d(LOG_TAG, "[handleGeoDescription]" +
+ "Spends long time to retrieve Geo description: " + duration);
+ }
+ }
+ }
+ Message reply = args.handler.obtainMessage(msg.what);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+ reply.sendToTarget();
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 41646a3..42f58e3 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -54,7 +54,6 @@
import android.provider.BlockedNumberContract;
import android.telecom.Call;
import android.telecom.CallAudioState;
-import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
@@ -72,6 +71,7 @@
import androidx.test.filters.SmallTest;
import com.android.internal.telecom.IInCallAdapter;
+import com.android.server.telecom.util.CallerInfo;
import com.google.common.base.Predicate;
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 6e39069..e3d6291 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -44,7 +44,6 @@
import android.os.UserHandle;
import android.telecom.CallAttributes;
import android.telecom.CallEndpoint;
-import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.ParcelableConference;
@@ -76,6 +75,7 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.TransactionalServiceWrapper;
import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.util.CallerInfo;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java b/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
index 68db09c..e45da67 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoAsyncQueryFactoryFixture.java
@@ -16,8 +16,8 @@
package com.android.server.telecom.tests;
-import android.telecom.CallerInfo;
-import android.telecom.CallerInfoAsyncQuery;
+import com.android.server.telecom.util.CallerInfo;
+import com.android.server.telecom.util.CallerInfoAsyncQuery;
import com.android.server.telecom.CallerInfoAsyncQueryFactory;
import android.content.Context;
diff --git a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
index 645e2e4..08fb49b 100644
--- a/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallerInfoLookupHelperTest.java
@@ -32,8 +32,6 @@
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.telecom.CallerInfo;
-import android.telecom.CallerInfoAsyncQuery;
import android.telecom.Logging.Session;
import androidx.test.InstrumentationRegistry;
@@ -43,6 +41,8 @@
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.ContactsAsyncHelper;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.util.CallerInfo;
+import com.android.server.telecom.util.CallerInfoAsyncQuery;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 9db061a..a862675 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -70,7 +70,6 @@
import android.provider.BlockedNumbersManager;
import android.telecom.CallException;
import android.telecom.CallScreeningService;
-import android.telecom.CallerInfo;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
@@ -146,6 +145,7 @@
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.util.CallerInfo;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
index 097061b..0eef602 100644
--- a/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
+++ b/tests/src/com/android/server/telecom/tests/DirectToVoicemailFilterTest.java
@@ -25,7 +25,6 @@
import android.net.Uri;
import android.provider.CallLog;
-import android.telecom.CallerInfo;
import androidx.test.filters.SmallTest;
@@ -33,6 +32,7 @@
import com.android.server.telecom.CallerInfoLookupHelper;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.callfiltering.DirectToVoicemailFilter;
+import com.android.server.telecom.util.CallerInfo;
import org.junit.Before;
import org.junit.Test;
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
index 29bedcc..1820aba 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierImplTest.java
@@ -60,7 +60,6 @@
import android.os.Looper;
import android.os.UserHandle;
import android.provider.CallLog;
-import android.telecom.CallerInfo;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -79,6 +78,7 @@
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import com.android.server.telecom.ui.MissedCallNotifierImpl;
import com.android.server.telecom.ui.MissedCallNotifierImpl.NotificationBuilderFactory;
+import com.android.server.telecom.util.CallerInfo;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
index c0e3435..28b30f4 100644
--- a/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
+++ b/tests/src/com/android/server/telecom/tests/MissedCallNotifierTest.java
@@ -21,12 +21,12 @@
import android.content.ComponentName;
import android.net.Uri;
-import android.telecom.CallerInfo;
import android.telecom.PhoneAccountHandle;
import androidx.test.filters.SmallTest;
import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.util.CallerInfo;
import org.junit.After;
import org.junit.Before;