Merge "Listen to the new calls and SMS/MMS messages and store the derived events in the People Service event store"
diff --git a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java
new file mode 100644
index 0000000..d825b6b
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import java.util.function.BiConsumer;
+
+/** A helper class that queries the call log database. */
+class CallLogQueryHelper {
+
+    private static final String TAG = "CallLogQueryHelper";
+
+    private static final SparseIntArray CALL_TYPE_TO_EVENT_TYPE = new SparseIntArray();
+
+    static {
+        CALL_TYPE_TO_EVENT_TYPE.put(Calls.INCOMING_TYPE, Event.TYPE_CALL_INCOMING);
+        CALL_TYPE_TO_EVENT_TYPE.put(Calls.OUTGOING_TYPE, Event.TYPE_CALL_OUTGOING);
+        CALL_TYPE_TO_EVENT_TYPE.put(Calls.MISSED_TYPE, Event.TYPE_CALL_MISSED);
+    }
+
+    private final Context mContext;
+    private final BiConsumer<String, Event> mEventConsumer;
+    private long mLastCallTimestamp;
+
+    /**
+     * @param context Context for accessing the content resolver.
+     * @param eventConsumer Consumes the events created from the call log records. The first input
+     *                      param is the normalized phone number.
+     */
+    CallLogQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+        mContext = context;
+        mEventConsumer = eventConsumer;
+    }
+
+    /**
+     * Queries the call log database for the new data added since {@code sinceTime} and returns
+     * true if the query runs successfully and at least one call log entry is found.
+     */
+    @WorkerThread
+    boolean querySince(long sinceTime) {
+        String[] projection = new String[] {
+                Calls.CACHED_NORMALIZED_NUMBER, Calls.DATE, Calls.DURATION, Calls.TYPE };
+        String selection = Calls.DATE + " > ?";
+        String[] selectionArgs = new String[] { Long.toString(sinceTime) };
+        boolean hasResults = false;
+        try (Cursor cursor = mContext.getContentResolver().query(
+                Calls.CONTENT_URI, projection, selection, selectionArgs,
+                Calls.DEFAULT_SORT_ORDER)) {
+            if (cursor == null) {
+                Slog.w(TAG, "Cursor is null when querying call log.");
+                return false;
+            }
+            while (cursor.moveToNext()) {
+                // Phone number
+                int numberIndex = cursor.getColumnIndex(Calls.CACHED_NORMALIZED_NUMBER);
+                String phoneNumber = cursor.getString(numberIndex);
+
+                // Date
+                int dateIndex = cursor.getColumnIndex(Calls.DATE);
+                long date = cursor.getLong(dateIndex);
+
+                // Duration
+                int durationIndex = cursor.getColumnIndex(Calls.DURATION);
+                long durationSeconds = cursor.getLong(durationIndex);
+
+                // Type
+                int typeIndex = cursor.getColumnIndex(Calls.TYPE);
+                int callType = cursor.getInt(typeIndex);
+
+                mLastCallTimestamp = Math.max(mLastCallTimestamp, date);
+                if (addEvent(phoneNumber, date, durationSeconds, callType)) {
+                    hasResults = true;
+                }
+            }
+        }
+        return hasResults;
+    }
+
+    long getLastCallTimestamp() {
+        return mLastCallTimestamp;
+    }
+
+    private boolean addEvent(String phoneNumber, long date, long durationSeconds, int callType) {
+        if (!validateEvent(phoneNumber, date, callType)) {
+            return false;
+        }
+        @Event.EventType int eventType  = CALL_TYPE_TO_EVENT_TYPE.get(callType);
+        Event event = new Event.Builder(date, eventType)
+                .setCallDetails(new Event.CallDetails(durationSeconds))
+                .build();
+        mEventConsumer.accept(phoneNumber, event);
+        return true;
+    }
+
+    private boolean validateEvent(String phoneNumber, long date, int callType) {
+        return !TextUtils.isEmpty(phoneNumber)
+                && date > 0L
+                && CALL_TYPE_TO_EVENT_TYPE.indexOfKey(callType) >= 0;
+    }
+}
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index 7b8ee5a..79503f7 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -45,7 +45,9 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.CallLog;
 import android.provider.ContactsContract.Contacts;
+import android.provider.Telephony.MmsSms;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.telecom.TelecomManager;
@@ -65,6 +67,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 /**
@@ -76,7 +79,7 @@
     private static final String PLATFORM_PACKAGE_NAME = "android";
     private static final int MY_UID = Process.myUid();
     private static final int MY_PID = Process.myPid();
-    private static final long USAGE_STATS_QUERY_MAX_EVENT_AGE_MS = DateUtils.DAY_IN_MILLIS;
+    private static final long QUERY_EVENTS_MAX_AGE_MS = DateUtils.DAY_IN_MILLIS;
     private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
 
     private final Context mContext;
@@ -89,6 +92,8 @@
     private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
     private final SparseArray<NotificationListenerService> mNotificationListeners =
             new SparseArray<>();
+    private final ContentObserver mCallLogContentObserver;
+    private final ContentObserver mMmsSmsContentObserver;
 
     private ShortcutServiceInternal mShortcutServiceInternal;
     private UsageStatsManagerInternal mUsageStatsManagerInternal;
@@ -96,9 +101,7 @@
     private UserManager mUserManager;
 
     public DataManager(Context context) {
-        mContext = context;
-        mInjector = new Injector();
-        mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
+        this(context, new Injector());
     }
 
     @VisibleForTesting
@@ -106,6 +109,10 @@
         mContext = context;
         mInjector = injector;
         mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
+        mCallLogContentObserver = new CallLogContentObserver(
+                BackgroundThread.getHandler());
+        mMmsSmsContentObserver = new MmsSmsContentObserver(
+                BackgroundThread.getHandler());
     }
 
     /** Initialization. Called when the system services are up running. */
@@ -158,6 +165,18 @@
         } catch (RemoteException e) {
             // Should never occur for local calls.
         }
+
+        if (userId == UserHandle.USER_SYSTEM) {
+            // The call log and MMS/SMS messages are shared across user profiles. So only need to
+            // register the content observers once for the primary user.
+            // TODO: Register observers after the conversations and events being loaded from disk.
+            mContext.getContentResolver().registerContentObserver(
+                    CallLog.CONTENT_URI, /* notifyForDescendants= */ true,
+                    mCallLogContentObserver, UserHandle.USER_SYSTEM);
+            mContext.getContentResolver().registerContentObserver(
+                    MmsSms.CONTENT_URI, /* notifyForDescendants= */ false,
+                    mMmsSmsContentObserver, UserHandle.USER_SYSTEM);
+        }
     }
 
     /** This method is called when a user is stopped. */
@@ -182,6 +201,10 @@
                 // Should never occur for local calls.
             }
         }
+        if (userId == UserHandle.USER_SYSTEM) {
+            mContext.getContentResolver().unregisterContentObserver(mCallLogContentObserver);
+            mContext.getContentResolver().unregisterContentObserver(mMmsSmsContentObserver);
+        }
     }
 
     /**
@@ -274,6 +297,15 @@
                 userId, MY_PID, MY_UID);
     }
 
+    private void forAllUnlockedUsers(Consumer<UserData> consumer) {
+        for (int i = 0; i < mUserDataArray.size(); i++) {
+            UserData userData = mUserDataArray.get(i);
+            if (userData.isUnlocked()) {
+                consumer.accept(userData);
+            }
+        }
+    }
+
     @Nullable
     private UserData getUnlockedUserData(int userId) {
         UserData userData = mUserDataArray.get(userId);
@@ -388,10 +420,25 @@
     }
 
     @VisibleForTesting
+    ContentObserver getCallLogContentObserverForTesting() {
+        return mCallLogContentObserver;
+    }
+
+    @VisibleForTesting
+    ContentObserver getMmsSmsContentObserverForTesting() {
+        return mMmsSmsContentObserver;
+    }
+
+    @VisibleForTesting
     NotificationListenerService getNotificationListenerServiceForTesting(@UserIdInt int userId) {
         return mNotificationListeners.get(userId);
     }
 
+    @VisibleForTesting
+    UserData getUserDataForTesting(@UserIdInt int userId) {
+        return mUserDataArray.get(userId);
+    }
+
     /** Observer that observes the changes in the Contacts database. */
     private class ContactsContentObserver extends ContentObserver {
 
@@ -442,6 +489,88 @@
         }
     }
 
+    /** Observer that observes the changes in the call log database. */
+    private class CallLogContentObserver extends ContentObserver implements
+            BiConsumer<String, Event> {
+
+        private final CallLogQueryHelper mCallLogQueryHelper;
+        private long mLastCallTimestamp;
+
+        private CallLogContentObserver(Handler handler) {
+            super(handler);
+            mCallLogQueryHelper = mInjector.createCallLogQueryHelper(mContext, this);
+            mLastCallTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            if (mCallLogQueryHelper.querySince(mLastCallTimestamp)) {
+                mLastCallTimestamp = mCallLogQueryHelper.getLastCallTimestamp();
+            }
+        }
+
+        @Override
+        public void accept(String phoneNumber, Event event) {
+            forAllUnlockedUsers(userData -> {
+                PackageData defaultDialer = userData.getDefaultDialer();
+                if (defaultDialer == null) {
+                    return;
+                }
+                ConversationStore conversationStore = defaultDialer.getConversationStore();
+                if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
+                    return;
+                }
+                EventStore eventStore = defaultDialer.getEventStore();
+                eventStore.getOrCreateCallEventHistory(phoneNumber).addEvent(event);
+            });
+        }
+    }
+
+    /** Observer that observes the changes in the MMS & SMS database. */
+    private class MmsSmsContentObserver extends ContentObserver implements
+            BiConsumer<String, Event> {
+
+        private final MmsQueryHelper mMmsQueryHelper;
+        private long mLastMmsTimestamp;
+
+        private final SmsQueryHelper mSmsQueryHelper;
+        private long mLastSmsTimestamp;
+
+        private MmsSmsContentObserver(Handler handler) {
+            super(handler);
+            mMmsQueryHelper = mInjector.createMmsQueryHelper(mContext, this);
+            mSmsQueryHelper = mInjector.createSmsQueryHelper(mContext, this);
+            mLastSmsTimestamp = mLastMmsTimestamp =
+                    System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            if (mMmsQueryHelper.querySince(mLastMmsTimestamp)) {
+                mLastMmsTimestamp = mMmsQueryHelper.getLastMessageTimestamp();
+            }
+            if (mSmsQueryHelper.querySince(mLastSmsTimestamp)) {
+                mLastSmsTimestamp = mSmsQueryHelper.getLastMessageTimestamp();
+            }
+        }
+
+        @Override
+        public void accept(String phoneNumber, Event event) {
+            forAllUnlockedUsers(userData -> {
+                PackageData defaultSmsApp = userData.getDefaultSmsApp();
+                if (defaultSmsApp == null) {
+                    return;
+                }
+                ConversationStore conversationStore = defaultSmsApp.getConversationStore();
+                if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
+                    return;
+                }
+                EventStore eventStore = defaultSmsApp.getEventStore();
+                eventStore.getOrCreateSmsEventHistory(phoneNumber).addEvent(event);
+            });
+        }
+    }
+
     /** Listener for the shortcut data changes. */
     private class ShortcutServiceListener implements
             ShortcutServiceInternal.ShortcutChangeListener {
@@ -487,7 +616,7 @@
 
         private UsageStatsQueryRunnable(int userId) {
             mUserId = userId;
-            mLastQueryTime = System.currentTimeMillis() - USAGE_STATS_QUERY_MAX_EVENT_AGE_MS;
+            mLastQueryTime = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
         }
 
         @Override
@@ -535,6 +664,21 @@
             return new ContactsQueryHelper(context);
         }
 
+        CallLogQueryHelper createCallLogQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            return new CallLogQueryHelper(context, eventConsumer);
+        }
+
+        MmsQueryHelper createMmsQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            return new MmsQueryHelper(context, eventConsumer);
+        }
+
+        SmsQueryHelper createSmsQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            return new SmsQueryHelper(context, eventConsumer);
+        }
+
         int getCallingUserId() {
             return Binder.getCallingUserHandle().getIdentifier();
         }
diff --git a/services/people/java/com/android/server/people/data/MmsQueryHelper.java b/services/people/java/com/android/server/people/data/MmsQueryHelper.java
new file mode 100644
index 0000000..1e485c0
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/MmsQueryHelper.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Telephony.BaseMmsColumns;
+import android.provider.Telephony.Mms;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import com.google.android.mms.pdu.PduHeaders;
+
+import java.util.function.BiConsumer;
+
+/** A helper class that queries the MMS database tables. */
+class MmsQueryHelper {
+
+    private static final String TAG = "MmsQueryHelper";
+    private static final long MILLIS_PER_SECONDS = 1000L;
+    private static final SparseIntArray MSG_BOX_TO_EVENT_TYPE = new SparseIntArray();
+
+    static {
+        MSG_BOX_TO_EVENT_TYPE.put(BaseMmsColumns.MESSAGE_BOX_INBOX, Event.TYPE_SMS_INCOMING);
+        MSG_BOX_TO_EVENT_TYPE.put(BaseMmsColumns.MESSAGE_BOX_SENT, Event.TYPE_SMS_OUTGOING);
+    }
+
+    private final Context mContext;
+    private final BiConsumer<String, Event> mEventConsumer;
+    private long mLastMessageTimestamp;
+    private String mCurrentCountryIso;
+
+    /**
+     * @param context Context for accessing the content resolver.
+     * @param eventConsumer Consumes the events created from the message records. The first input
+     *                      param is the normalized phone number.
+     */
+    MmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+        mContext = context;
+        mEventConsumer = eventConsumer;
+        mCurrentCountryIso = Utils.getCurrentCountryIso(mContext);
+    }
+
+    /**
+     * Queries the MMS database tables for the new data added since {@code sinceTime} (in millis)
+     * and returns true if the query runs successfully and at least one message entry is found.
+     */
+    @WorkerThread
+    boolean querySince(long sinceTime) {
+        String[] projection = new String[] { Mms._ID, Mms.DATE, Mms.MESSAGE_BOX };
+        String selection = Mms.DATE + " > ?";
+        // NOTE: The field Mms.DATE is stored in seconds, not milliseconds.
+        String[] selectionArgs = new String[] { Long.toString(sinceTime / MILLIS_PER_SECONDS) };
+        boolean hasResults = false;
+        try (Cursor cursor = mContext.getContentResolver().query(
+                Mms.CONTENT_URI, projection, selection, selectionArgs, null)) {
+            if (cursor == null) {
+                Slog.w(TAG, "Cursor is null when querying MMS table.");
+                return false;
+            }
+            while (cursor.moveToNext()) {
+                // ID
+                int msgIdIndex = cursor.getColumnIndex(Mms._ID);
+                String msgId = cursor.getString(msgIdIndex);
+
+                // Date
+                int dateIndex = cursor.getColumnIndex(Mms.DATE);
+                long date = cursor.getLong(dateIndex) * MILLIS_PER_SECONDS;
+
+                // Message box
+                int msgBoxIndex = cursor.getColumnIndex(Mms.MESSAGE_BOX);
+                int msgBox = cursor.getInt(msgBoxIndex);
+
+                mLastMessageTimestamp = Math.max(mLastMessageTimestamp, date);
+                String address = getMmsAddress(msgId, msgBox);
+                if (address != null && addEvent(address, date, msgBox)) {
+                    hasResults = true;
+                }
+            }
+        }
+        return hasResults;
+    }
+
+    long getLastMessageTimestamp() {
+        return mLastMessageTimestamp;
+    }
+
+    @Nullable
+    private String getMmsAddress(String msgId, int msgBox) {
+        Uri addressUri = Mms.Addr.getAddrUriForMessage(msgId);
+        String[] projection = new String[] { Mms.Addr.ADDRESS, Mms.Addr.TYPE };
+        String address = null;
+        try (Cursor cursor = mContext.getContentResolver().query(
+                addressUri, projection, null, null, null)) {
+            if (cursor == null) {
+                Slog.w(TAG, "Cursor is null when querying MMS address table.");
+                return null;
+            }
+            while (cursor.moveToNext()) {
+                // Type
+                int typeIndex = cursor.getColumnIndex(Mms.Addr.TYPE);
+                int type = cursor.getInt(typeIndex);
+
+                if ((msgBox == BaseMmsColumns.MESSAGE_BOX_INBOX && type == PduHeaders.FROM)
+                        || (msgBox == BaseMmsColumns.MESSAGE_BOX_SENT && type == PduHeaders.TO)) {
+                    // Address
+                    int addrIndex = cursor.getColumnIndex(Mms.Addr.ADDRESS);
+                    address = cursor.getString(addrIndex);
+                }
+            }
+        }
+        if (!Mms.isPhoneNumber(address)) {
+            return null;
+        }
+        return PhoneNumberUtils.formatNumberToE164(address, mCurrentCountryIso);
+    }
+
+    private boolean addEvent(String phoneNumber, long date, int msgBox) {
+        if (!validateEvent(phoneNumber, date, msgBox)) {
+            return false;
+        }
+        @Event.EventType int eventType  = MSG_BOX_TO_EVENT_TYPE.get(msgBox);
+        mEventConsumer.accept(phoneNumber, new Event(date, eventType));
+        return true;
+    }
+
+    private boolean validateEvent(String phoneNumber, long date, int msgBox) {
+        return !TextUtils.isEmpty(phoneNumber)
+                && date > 0L
+                && MSG_BOX_TO_EVENT_TYPE.indexOfKey(msgBox) >= 0;
+    }
+}
diff --git a/services/people/java/com/android/server/people/data/PackageData.java b/services/people/java/com/android/server/people/data/PackageData.java
index 35b65ec..75b870c 100644
--- a/services/people/java/com/android/server/people/data/PackageData.java
+++ b/services/people/java/com/android/server/people/data/PackageData.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** The data associated with a package. */
 public class PackageData {
@@ -38,15 +39,19 @@
     @NonNull
     private final EventStore mEventStore;
 
-    private boolean mIsDefaultDialer;
+    private final Predicate<String> mIsDefaultDialerPredicate;
 
-    private boolean mIsDefaultSmsApp;
+    private final Predicate<String> mIsDefaultSmsAppPredicate;
 
-    PackageData(@NonNull String packageName, @UserIdInt int userId) {
+    PackageData(@NonNull String packageName, @UserIdInt int userId,
+            @NonNull Predicate<String> isDefaultDialerPredicate,
+            @NonNull Predicate<String> isDefaultSmsAppPredicate) {
         mPackageName = packageName;
         mUserId = userId;
         mConversationStore = new ConversationStore();
         mEventStore = new EventStore();
+        mIsDefaultDialerPredicate = isDefaultDialerPredicate;
+        mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate;
     }
 
     @NonNull
@@ -124,11 +129,11 @@
     }
 
     public boolean isDefaultDialer() {
-        return mIsDefaultDialer;
+        return mIsDefaultDialerPredicate.test(mPackageName);
     }
 
     public boolean isDefaultSmsApp() {
-        return mIsDefaultSmsApp;
+        return mIsDefaultSmsAppPredicate.test(mPackageName);
     }
 
     @NonNull
@@ -141,14 +146,6 @@
         return mEventStore;
     }
 
-    void setIsDefaultDialer(boolean value) {
-        mIsDefaultDialer = value;
-    }
-
-    void setIsDefaultSmsApp(boolean value) {
-        mIsDefaultSmsApp = value;
-    }
-
     void onDestroy() {
         // TODO: STOPSHIP: Implements this method for the case of package being uninstalled.
     }
diff --git a/services/people/java/com/android/server/people/data/SmsQueryHelper.java b/services/people/java/com/android/server/people/data/SmsQueryHelper.java
new file mode 100644
index 0000000..c38c846
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/SmsQueryHelper.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.TextBasedSmsColumns;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import java.util.function.BiConsumer;
+
+/** A helper class that queries the SMS database table. */
+class SmsQueryHelper {
+
+    private static final String TAG = "SmsQueryHelper";
+    private static final SparseIntArray SMS_TYPE_TO_EVENT_TYPE = new SparseIntArray();
+
+    static {
+        SMS_TYPE_TO_EVENT_TYPE.put(TextBasedSmsColumns.MESSAGE_TYPE_INBOX, Event.TYPE_SMS_INCOMING);
+        SMS_TYPE_TO_EVENT_TYPE.put(TextBasedSmsColumns.MESSAGE_TYPE_SENT, Event.TYPE_SMS_OUTGOING);
+    }
+
+    private final Context mContext;
+    private final BiConsumer<String, Event> mEventConsumer;
+    private final String mCurrentCountryIso;
+    private long mLastMessageTimestamp;
+
+    /**
+     * @param context Context for accessing the content resolver.
+     * @param eventConsumer Consumes the events created from the message records. The first input
+     *                      param is the normalized phone number.
+     */
+    SmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+        mContext = context;
+        mEventConsumer = eventConsumer;
+        mCurrentCountryIso = Utils.getCurrentCountryIso(mContext);
+    }
+
+    /**
+     * Queries the SMS database tables for the new data added since {@code sinceTime} (in millis)
+     * and returns true if the query runs successfully and at least one message entry is found.
+     */
+    @WorkerThread
+    boolean querySince(long sinceTime) {
+        String[] projection = new String[] { Sms._ID, Sms.DATE, Sms.TYPE, Sms.ADDRESS };
+        String selection = Sms.DATE + " > ?";
+        String[] selectionArgs = new String[] { Long.toString(sinceTime) };
+        boolean hasResults = false;
+        try (Cursor cursor = mContext.getContentResolver().query(
+                Sms.CONTENT_URI, projection, selection, selectionArgs, null)) {
+            if (cursor == null) {
+                Slog.w(TAG, "Cursor is null when querying SMS table.");
+                return false;
+            }
+            while (cursor.moveToNext()) {
+                // ID
+                int msgIdIndex = cursor.getColumnIndex(Sms._ID);
+                String msgId = cursor.getString(msgIdIndex);
+
+                // Date
+                int dateIndex = cursor.getColumnIndex(Sms.DATE);
+                long date = cursor.getLong(dateIndex);
+
+                // Type
+                int typeIndex = cursor.getColumnIndex(Sms.TYPE);
+                int type = cursor.getInt(typeIndex);
+
+                // Address
+                int addressIndex = cursor.getColumnIndex(Sms.ADDRESS);
+                String address = PhoneNumberUtils.formatNumberToE164(
+                        cursor.getString(addressIndex), mCurrentCountryIso);
+
+                mLastMessageTimestamp = Math.max(mLastMessageTimestamp, date);
+                if (address != null && addEvent(address, date, type)) {
+                    hasResults = true;
+                }
+            }
+        }
+        return hasResults;
+    }
+
+    long getLastMessageTimestamp() {
+        return mLastMessageTimestamp;
+    }
+
+    private boolean addEvent(String phoneNumber, long date, int type) {
+        if (!validateEvent(phoneNumber, date, type)) {
+            return false;
+        }
+        @Event.EventType int eventType  = SMS_TYPE_TO_EVENT_TYPE.get(type);
+        mEventConsumer.accept(phoneNumber, new Event(date, eventType));
+        return true;
+    }
+
+    private boolean validateEvent(String phoneNumber, long date, int type) {
+        return !TextUtils.isEmpty(phoneNumber)
+                && date > 0L
+                && SMS_TYPE_TO_EVENT_TYPE.indexOfKey(type) >= 0;
+    }
+}
diff --git a/services/people/java/com/android/server/people/data/UserData.java b/services/people/java/com/android/server/people/data/UserData.java
index 2c16059..4e8fd16 100644
--- a/services/people/java/com/android/server/people/data/UserData.java
+++ b/services/people/java/com/android/server/people/data/UserData.java
@@ -34,6 +34,12 @@
 
     private Map<String, PackageData> mPackageDataMap = new ArrayMap<>();
 
+    @Nullable
+    private String mDefaultDialer;
+
+    @Nullable
+    private String mDefaultSmsApp;
+
     UserData(@UserIdInt int userId) {
         mUserId = userId;
     }
@@ -66,8 +72,7 @@
      */
     @NonNull
     PackageData getOrCreatePackageData(String packageName) {
-        return mPackageDataMap.computeIfAbsent(
-                packageName, key -> new PackageData(packageName, mUserId));
+        return mPackageDataMap.computeIfAbsent(packageName, key -> createPackageData(packageName));
     }
 
     /**
@@ -80,24 +85,32 @@
     }
 
     void setDefaultDialer(@Nullable String packageName) {
-        for (PackageData packageData : mPackageDataMap.values()) {
-            if (packageData.isDefaultDialer()) {
-                packageData.setIsDefaultDialer(false);
-            }
-            if (TextUtils.equals(packageName, packageData.getPackageName())) {
-                packageData.setIsDefaultDialer(true);
-            }
-        }
+        mDefaultDialer = packageName;
+    }
+
+    @Nullable
+    PackageData getDefaultDialer() {
+        return mDefaultDialer != null ? getPackageData(mDefaultDialer) : null;
     }
 
     void setDefaultSmsApp(@Nullable String packageName) {
-        for (PackageData packageData : mPackageDataMap.values()) {
-            if (packageData.isDefaultSmsApp()) {
-                packageData.setIsDefaultSmsApp(false);
-            }
-            if (TextUtils.equals(packageName, packageData.getPackageName())) {
-                packageData.setIsDefaultSmsApp(true);
-            }
-        }
+        mDefaultSmsApp = packageName;
+    }
+
+    @Nullable
+    PackageData getDefaultSmsApp() {
+        return mDefaultSmsApp != null ? getPackageData(mDefaultSmsApp) : null;
+    }
+
+    private PackageData createPackageData(String packageName) {
+        return new PackageData(packageName, mUserId, this::isDefaultDialer, this::isDefaultSmsApp);
+    }
+
+    private boolean isDefaultDialer(String packageName) {
+        return TextUtils.equals(mDefaultDialer, packageName);
+    }
+
+    private boolean isDefaultSmsApp(String packageName) {
+        return TextUtils.equals(mDefaultSmsApp, packageName);
     }
 }
diff --git a/services/people/java/com/android/server/people/data/Utils.java b/services/people/java/com/android/server/people/data/Utils.java
new file mode 100644
index 0000000..b752960
--- /dev/null
+++ b/services/people/java/com/android/server/people/data/Utils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import android.content.Context;
+import android.location.Country;
+import android.location.CountryDetector;
+
+import java.util.Locale;
+
+/** The utilities static methods for people service data package. */
+class Utils {
+
+    /**
+     * @return The ISO 3166-1 two letters country code of the country the user is in.
+     */
+    static String getCurrentCountryIso(Context context) {
+        String countryIso = null;
+        CountryDetector detector = (CountryDetector) context.getSystemService(
+                Context.COUNTRY_DETECTOR);
+        if (detector != null) {
+            Country country = detector.detectCountry();
+            if (country != null) {
+                countryIso = country.getCountryIso();
+            }
+        }
+        if (countryIso == null) {
+            countryIso = Locale.getDefault().getCountry();
+        }
+        return countryIso;
+    }
+
+    private Utils() {
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java
new file mode 100644
index 0000000..7a16d17
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.util.ArrayMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@RunWith(JUnit4.class)
+public final class CallLogQueryHelperTest {
+
+    private static final String CALL_LOG_AUTHORITY = "call_log";
+    private static final String NORMALIZED_PHONE_NUMBER = "+16505551111";
+
+    private static final String[] CALL_LOG_COLUMNS = new String[] {
+            Calls.CACHED_NORMALIZED_NUMBER, Calls.DATE, Calls.DURATION, Calls.TYPE };
+
+    @Mock
+    private MockContext mContext;
+
+    private MatrixCursor mCursor;
+    private EventConsumer mEventConsumer;
+    private CallLogQueryHelper mHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mCursor = new MatrixCursor(CALL_LOG_COLUMNS);
+
+        MockContentResolver contentResolver = new MockContentResolver();
+        contentResolver.addProvider(CALL_LOG_AUTHORITY, new CallLogContentProvider());
+        when(mContext.getContentResolver()).thenReturn(contentResolver);
+
+        mEventConsumer = new EventConsumer();
+        mHelper = new CallLogQueryHelper(mContext, mEventConsumer);
+    }
+
+    @Test
+    public void testQueryNoCalls() {
+        assertFalse(mHelper.querySince(50L));
+        assertFalse(mEventConsumer.mEventMap.containsKey(NORMALIZED_PHONE_NUMBER));
+    }
+
+    @Test
+    public void testQueryIncomingCall() {
+        mCursor.addRow(new Object[] {
+                NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 30L,
+                /* type= */ Calls.INCOMING_TYPE });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100L, mHelper.getLastCallTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_CALL_INCOMING, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+        assertEquals(30L, events.get(0).getCallDetails().getDurationSeconds());
+    }
+
+    @Test
+    public void testQueryOutgoingCall() {
+        mCursor.addRow(new Object[] {
+                NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 40L,
+                /* type= */ Calls.OUTGOING_TYPE });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100L, mHelper.getLastCallTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_CALL_OUTGOING, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+        assertEquals(40L, events.get(0).getCallDetails().getDurationSeconds());
+    }
+
+    @Test
+    public void testQueryMissedCall() {
+        mCursor.addRow(new Object[] {
+                NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 0L,
+                /* type= */ Calls.MISSED_TYPE });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100L, mHelper.getLastCallTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_CALL_MISSED, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+        assertEquals(0L, events.get(0).getCallDetails().getDurationSeconds());
+    }
+
+    @Test
+    public void testQueryMultipleCalls() {
+        mCursor.addRow(new Object[] {
+                NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 0L,
+                /* type= */ Calls.MISSED_TYPE });
+        mCursor.addRow(new Object[] {
+                NORMALIZED_PHONE_NUMBER, /* date= */ 110L, /* duration= */ 40L,
+                /* type= */ Calls.OUTGOING_TYPE });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(110L, mHelper.getLastCallTimestamp());
+        assertEquals(2, events.size());
+        assertEquals(Event.TYPE_CALL_MISSED, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+        assertEquals(Event.TYPE_CALL_OUTGOING, events.get(1).getType());
+        assertEquals(110L, events.get(1).getTimestamp());
+        assertEquals(40L, events.get(1).getCallDetails().getDurationSeconds());
+    }
+
+    private class EventConsumer implements BiConsumer<String, Event> {
+
+        private final Map<String, List<Event>> mEventMap = new ArrayMap<>();
+
+        @Override
+        public void accept(String phoneNumber, Event event) {
+            mEventMap.computeIfAbsent(phoneNumber, key -> new ArrayList<>()).add(event);
+        }
+    }
+
+    private class CallLogContentProvider extends MockContentProvider {
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return mCursor;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 3113a61..62ea425 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -56,6 +58,7 @@
 import android.provider.ContactsContract;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
+import android.telecom.TelecomManager;
 import android.telephony.TelephonyManager;
 import android.util.Range;
 
@@ -77,6 +80,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
 
 @RunWith(JUnit4.class)
 public final class DataManagerTest {
@@ -88,6 +92,7 @@
     private static final String TEST_SHORTCUT_ID = "sc";
     private static final String CONTACT_URI = "content://com.android.contacts/contacts/lookup/123";
     private static final String PHONE_NUMBER = "+1234567890";
+    private static final long MILLIS_PER_MINUTE = 1000L * 60L;
 
     @Mock private Context mContext;
     @Mock private ShortcutServiceInternal mShortcutServiceInternal;
@@ -95,6 +100,7 @@
     @Mock private ShortcutManager mShortcutManager;
     @Mock private UserManager mUserManager;
     @Mock private TelephonyManager mTelephonyManager;
+    @Mock private TelecomManager mTelecomManager;
     @Mock private ContentResolver mContentResolver;
     @Mock private ScheduledExecutorService mExecutorService;
     @Mock private ScheduledFuture mScheduledFuture;
@@ -115,6 +121,9 @@
 
         when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
 
+        Context originalContext = getInstrumentation().getTargetContext();
+        when(mContext.getApplicationInfo()).thenReturn(originalContext.getApplicationInfo());
+
         when(mContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mShortcutManager);
         when(mContext.getSystemServiceName(ShortcutManager.class)).thenReturn(
                 Context.SHORTCUT_SERVICE);
@@ -125,6 +134,11 @@
 
         when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
 
+        when(mContext.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager);
+        when(mContext.getSystemServiceName(TelecomManager.class)).thenReturn(
+                Context.TELECOM_SERVICE);
+        when(mTelecomManager.getDefaultDialerPackage(anyInt())).thenReturn(TEST_PKG_NAME);
+
         when(mExecutorService.scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any(
                 TimeUnit.class))).thenReturn(mScheduledFuture);
 
@@ -324,6 +338,61 @@
         assertEquals(1, activeShortcutInvocationTimeSlots.size());
     }
 
+    @Test
+    public void testCallLogContentObserver() {
+        mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+                buildPerson());
+        mDataManager.onShortcutAddedOrUpdated(shortcut);
+
+        ContentObserver contentObserver = mDataManager.getCallLogContentObserverForTesting();
+        contentObserver.onChange(false);
+        long currentTimestamp = System.currentTimeMillis();
+        mInjector.mCallLogQueryHelper.mEventConsumer.accept(PHONE_NUMBER,
+                new Event(currentTimestamp - MILLIS_PER_MINUTE * 15L, Event.TYPE_CALL_OUTGOING));
+        mInjector.mCallLogQueryHelper.mEventConsumer.accept(PHONE_NUMBER,
+                new Event(currentTimestamp - MILLIS_PER_MINUTE * 10L, Event.TYPE_CALL_INCOMING));
+        mInjector.mCallLogQueryHelper.mEventConsumer.accept(PHONE_NUMBER,
+                new Event(currentTimestamp - MILLIS_PER_MINUTE * 5L, Event.TYPE_CALL_MISSED));
+
+        List<Range<Long>> activeTimeSlots = new ArrayList<>();
+        mDataManager.forAllPackages(packageData ->
+                activeTimeSlots.addAll(
+                        packageData.getEventHistory(TEST_SHORTCUT_ID)
+                                .getEventIndex(Event.CALL_EVENT_TYPES)
+                                .getActiveTimeSlots()));
+        assertEquals(3, activeTimeSlots.size());
+    }
+
+    @Test
+    public void testMmsSmsContentObserver() {
+        mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+        ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+                buildPerson());
+        mDataManager.onShortcutAddedOrUpdated(shortcut);
+        mDataManager.getUserDataForTesting(USER_ID_PRIMARY).setDefaultSmsApp(TEST_PKG_NAME);
+
+        ContentObserver contentObserver = mDataManager.getMmsSmsContentObserverForTesting();
+        contentObserver.onChange(false);
+        long currentTimestamp = System.currentTimeMillis();
+        Event outgoingSmsEvent =
+                new Event(currentTimestamp - MILLIS_PER_MINUTE * 10L, Event.TYPE_SMS_OUTGOING);
+        Event incomingSmsEvent =
+                new Event(currentTimestamp - MILLIS_PER_MINUTE * 5L, Event.TYPE_SMS_INCOMING);
+        mInjector.mMmsQueryHelper.mEventConsumer.accept(PHONE_NUMBER, outgoingSmsEvent);
+        mInjector.mSmsQueryHelper.mEventConsumer.accept(PHONE_NUMBER, incomingSmsEvent);
+
+        List<Range<Long>> activeTimeSlots = new ArrayList<>();
+        mDataManager.forAllPackages(packageData ->
+                activeTimeSlots.addAll(
+                        packageData.getEventHistory(TEST_SHORTCUT_ID)
+                                .getEventIndex(Event.SMS_EVENT_TYPES)
+                                .getActiveTimeSlots()));
+        assertEquals(2, activeTimeSlots.size());
+    }
+
     private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
         LocalServices.removeServiceForTest(clazz);
         LocalServices.addService(clazz, mock);
@@ -401,10 +470,73 @@
         }
     }
 
+    private class TestCallLogQueryHelper extends CallLogQueryHelper {
+
+        private final BiConsumer<String, Event> mEventConsumer;
+
+        TestCallLogQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+            super(context, eventConsumer);
+            mEventConsumer = eventConsumer;
+        }
+
+        @Override
+        boolean querySince(long sinceTime) {
+            return true;
+        }
+
+        @Override
+        long getLastCallTimestamp() {
+            return 100L;
+        }
+    }
+
+    private class TestSmsQueryHelper extends SmsQueryHelper {
+
+        private final BiConsumer<String, Event> mEventConsumer;
+
+        TestSmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+            super(context, eventConsumer);
+            mEventConsumer = eventConsumer;
+        }
+
+        @Override
+        boolean querySince(long sinceTime) {
+            return true;
+        }
+
+        @Override
+        long getLastMessageTimestamp() {
+            return 100L;
+        }
+    }
+
+    private class TestMmsQueryHelper extends MmsQueryHelper {
+
+        private final BiConsumer<String, Event> mEventConsumer;
+
+        TestMmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
+            super(context, eventConsumer);
+            mEventConsumer = eventConsumer;
+        }
+
+        @Override
+        boolean querySince(long sinceTime) {
+            return true;
+        }
+
+        @Override
+        long getLastMessageTimestamp() {
+            return 100L;
+        }
+    }
+
     private class TestInjector extends DataManager.Injector {
 
         private final TestContactsQueryHelper mContactsQueryHelper =
                 new TestContactsQueryHelper(mContext);
+        private TestCallLogQueryHelper mCallLogQueryHelper;
+        private TestMmsQueryHelper mMmsQueryHelper;
+        private TestSmsQueryHelper mSmsQueryHelper;
 
         @Override
         ScheduledExecutorService createScheduledExecutor() {
@@ -417,6 +549,27 @@
         }
 
         @Override
+        CallLogQueryHelper createCallLogQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            mCallLogQueryHelper = new TestCallLogQueryHelper(context, eventConsumer);
+            return mCallLogQueryHelper;
+        }
+
+        @Override
+        MmsQueryHelper createMmsQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            mMmsQueryHelper = new TestMmsQueryHelper(context, eventConsumer);
+            return mMmsQueryHelper;
+        }
+
+        @Override
+        SmsQueryHelper createSmsQueryHelper(Context context,
+                BiConsumer<String, Event> eventConsumer) {
+            mSmsQueryHelper = new TestSmsQueryHelper(context, eventConsumer);
+            return mSmsQueryHelper;
+        }
+
+        @Override
         int getCallingUserId() {
             return mCallingUserId;
         }
diff --git a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java
new file mode 100644
index 0000000..7730890
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.Telephony.BaseMmsColumns;
+import android.provider.Telephony.Mms;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.util.ArrayMap;
+
+import com.google.android.mms.pdu.PduHeaders;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@RunWith(JUnit4.class)
+public final class MmsQueryHelperTest {
+
+    private static final String MMS_AUTHORITY = "mms";
+    private static final String PHONE_NUMBER = "650-555-1111";
+    private static final String NORMALIZED_PHONE_NUMBER = "+16505551111";
+    private static final String OWN_PHONE_NUMBER = "650-555-9999";
+
+    private static final String[] MMS_COLUMNS = new String[] { Mms._ID, Mms.DATE, Mms.MESSAGE_BOX };
+    private static final String[] ADDR_COLUMNS = new String[] { Mms.Addr.ADDRESS, Mms.Addr.TYPE };
+
+    @Mock
+    private MockContext mContext;
+
+    private MatrixCursor mMmsCursor;
+    private final List<MatrixCursor> mAddrCursors = new ArrayList<>();
+    private EventConsumer mEventConsumer;
+    private MmsQueryHelper mHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mMmsCursor = new MatrixCursor(MMS_COLUMNS);
+        mAddrCursors.add(new MatrixCursor(ADDR_COLUMNS));
+        mAddrCursors.add(new MatrixCursor(ADDR_COLUMNS));
+
+        MockContentResolver contentResolver = new MockContentResolver();
+        contentResolver.addProvider(MMS_AUTHORITY, new MmsContentProvider());
+        when(mContext.getContentResolver()).thenReturn(contentResolver);
+
+        mEventConsumer = new EventConsumer();
+        mHelper = new MmsQueryHelper(mContext, mEventConsumer);
+    }
+
+    @Test
+    public void testQueryNoMessages() {
+        assertFalse(mHelper.querySince(50_000L));
+        assertFalse(mEventConsumer.mEventMap.containsKey(NORMALIZED_PHONE_NUMBER));
+    }
+
+    @Test
+    public void testQueryIncomingMessage() {
+        mMmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_INBOX });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ PHONE_NUMBER, /* type= */ PduHeaders.FROM });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ OWN_PHONE_NUMBER, /* type= */ PduHeaders.TO });
+
+        assertTrue(mHelper.querySince(50_000L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100_000L, mHelper.getLastMessageTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_SMS_INCOMING, events.get(0).getType());
+        assertEquals(100_000L, events.get(0).getTimestamp());
+    }
+
+    @Test
+    public void testQueryOutgoingMessage() {
+        mMmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_SENT });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ OWN_PHONE_NUMBER, /* type= */ PduHeaders.FROM });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ PHONE_NUMBER, /* type= */ PduHeaders.TO });
+
+        assertTrue(mHelper.querySince(50_000L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100_000L, mHelper.getLastMessageTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_SMS_OUTGOING, events.get(0).getType());
+        assertEquals(100_000L, events.get(0).getTimestamp());
+    }
+
+    @Test
+    public void testQueryMultipleMessages() {
+        mMmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_SENT });
+        mMmsCursor.addRow(new Object[] {
+                /* id= */ 1, /* date= */ 110L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_INBOX });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ OWN_PHONE_NUMBER, /* type= */ PduHeaders.FROM });
+        mAddrCursors.get(0).addRow(new Object[] {
+                /* address= */ PHONE_NUMBER, /* type= */ PduHeaders.TO });
+        mAddrCursors.get(1).addRow(new Object[] {
+                /* address= */ PHONE_NUMBER, /* type= */ PduHeaders.FROM });
+        mAddrCursors.get(1).addRow(new Object[] {
+                /* address= */ OWN_PHONE_NUMBER, /* type= */ PduHeaders.TO });
+
+        assertTrue(mHelper.querySince(50_000L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(110_000L, mHelper.getLastMessageTimestamp());
+        assertEquals(2, events.size());
+        assertEquals(Event.TYPE_SMS_OUTGOING, events.get(0).getType());
+        assertEquals(100_000L, events.get(0).getTimestamp());
+        assertEquals(Event.TYPE_SMS_INCOMING, events.get(1).getType());
+        assertEquals(110_000L, events.get(1).getTimestamp());
+    }
+
+    private class EventConsumer implements BiConsumer<String, Event> {
+
+        private final Map<String, List<Event>> mEventMap = new ArrayMap<>();
+
+        @Override
+        public void accept(String phoneNumber, Event event) {
+            mEventMap.computeIfAbsent(phoneNumber, key -> new ArrayList<>()).add(event);
+        }
+    }
+
+    private class MmsContentProvider extends MockContentProvider {
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            List<String> segments = uri.getPathSegments();
+            if (segments.size() == 2 && "addr".equals(segments.get(1))) {
+                int messageId = Integer.valueOf(segments.get(0));
+                return mAddrCursors.get(messageId);
+            }
+            return mMmsCursor;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/PackageDataTest.java b/services/tests/servicestests/src/com/android/server/people/data/PackageDataTest.java
index 1b80d6f..ec4789a 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/PackageDataTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/PackageDataTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.people.data;
 
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -46,12 +45,15 @@
     private Event mE2;
     private Event mE3;
     private Event mE4;
+    private boolean mIsDefaultDialer;
+    private boolean mIsDefaultSmsApp;
 
     private PackageData mPackageData;
 
     @Before
     public void setUp() {
-        mPackageData = new PackageData(PACKAGE_NAME, USER_ID);
+        mPackageData = new PackageData(
+                PACKAGE_NAME, USER_ID, pkg -> mIsDefaultDialer, pkg -> mIsDefaultSmsApp);
         ConversationInfo conversationInfo = new ConversationInfo.Builder()
                 .setShortcutId(SHORTCUT_ID)
                 .setLocusId(LOCUS_ID)
@@ -83,8 +85,8 @@
 
     @Test
     public void testGetEventHistoryDefaultDialerAndSmsApp() {
-        mPackageData.setIsDefaultDialer(true);
-        mPackageData.setIsDefaultSmsApp(true);
+        mIsDefaultDialer = true;
+        mIsDefaultSmsApp = true;
         EventStore eventStore = mPackageData.getEventStore();
         eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
         eventStore.getOrCreateCallEventHistory(PHONE_NUMBER).addEvent(mE3);
diff --git a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java
new file mode 100644
index 0000000..5cb8cb4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 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.people.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.TextBasedSmsColumns;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.util.ArrayMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@RunWith(JUnit4.class)
+public final class SmsQueryHelperTest {
+
+    private static final String SMS_AUTHORITY = "sms";
+    private static final String PHONE_NUMBER = "650-555-1111";
+    private static final String NORMALIZED_PHONE_NUMBER = "+16505551111";
+
+    private static final String[] SMS_COLUMNS = new String[] {
+            Sms._ID, Sms.DATE, Sms.TYPE, Sms.ADDRESS };
+
+    @Mock
+    private MockContext mContext;
+
+    private MatrixCursor mSmsCursor;
+    private EventConsumer mEventConsumer;
+    private SmsQueryHelper mHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mSmsCursor = new MatrixCursor(SMS_COLUMNS);
+
+        MockContentResolver contentResolver = new MockContentResolver();
+        contentResolver.addProvider(SMS_AUTHORITY, new SmsContentProvider());
+        when(mContext.getContentResolver()).thenReturn(contentResolver);
+
+        mEventConsumer = new EventConsumer();
+        mHelper = new SmsQueryHelper(mContext, mEventConsumer);
+    }
+
+    @Test
+    public void testQueryNoMessages() {
+        assertFalse(mHelper.querySince(50_000L));
+        assertFalse(mEventConsumer.mEventMap.containsKey(NORMALIZED_PHONE_NUMBER));
+    }
+
+    @Test
+    public void testQueryIncomingMessage() {
+        mSmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* type= */ TextBasedSmsColumns.MESSAGE_TYPE_INBOX,
+                /* address= */ PHONE_NUMBER });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100L, mHelper.getLastMessageTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_SMS_INCOMING, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+    }
+
+    @Test
+    public void testQueryOutgoingMessage() {
+        mSmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* type= */ TextBasedSmsColumns.MESSAGE_TYPE_SENT,
+                /* address= */ PHONE_NUMBER });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(100L, mHelper.getLastMessageTimestamp());
+        assertEquals(1, events.size());
+        assertEquals(Event.TYPE_SMS_OUTGOING, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+    }
+
+    @Test
+    public void testQueryMultipleMessages() {
+        mSmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 100L, /* type= */ TextBasedSmsColumns.MESSAGE_TYPE_SENT,
+                /* address= */ PHONE_NUMBER });
+        mSmsCursor.addRow(new Object[] {
+                /* id= */ 0, /* date= */ 110L, /* type= */ TextBasedSmsColumns.MESSAGE_TYPE_INBOX,
+                /* address= */ PHONE_NUMBER });
+
+        assertTrue(mHelper.querySince(50L));
+        List<Event> events = mEventConsumer.mEventMap.get(NORMALIZED_PHONE_NUMBER);
+
+        assertEquals(110L, mHelper.getLastMessageTimestamp());
+        assertEquals(2, events.size());
+        assertEquals(Event.TYPE_SMS_OUTGOING, events.get(0).getType());
+        assertEquals(100L, events.get(0).getTimestamp());
+        assertEquals(Event.TYPE_SMS_INCOMING, events.get(1).getType());
+        assertEquals(110L, events.get(1).getTimestamp());
+    }
+
+    private class EventConsumer implements BiConsumer<String, Event> {
+
+        private final Map<String, List<Event>> mEventMap = new ArrayMap<>();
+
+        @Override
+        public void accept(String phoneNumber, Event event) {
+            mEventMap.computeIfAbsent(phoneNumber, key -> new ArrayList<>()).add(event);
+        }
+    }
+
+    private class SmsContentProvider extends MockContentProvider {
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return mSmsCursor;
+        }
+    }
+}