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;
+ }
+ }
+}