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;