Merge "Listen to the new calls and SMS/MMS messages and store the derived events in the People Service event store"
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ import android.os.Process;
|
||||
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.Executors;
|
||||
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 @@ public class DataManager {
|
||||
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 @@ public class DataManager {
|
||||
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 @@ public class DataManager {
|
||||
private UserManager mUserManager;
|
||||
|
||||
public DataManager(Context context) {
|
||||
mContext = context;
|
||||
mInjector = new Injector();
|
||||
mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
|
||||
this(context, new Injector());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -106,6 +109,10 @@ public class DataManager {
|
||||
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 @@ public class DataManager {
|
||||
} 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 @@ public class DataManager {
|
||||
// Should never occur for local calls.
|
||||
}
|
||||
}
|
||||
if (userId == UserHandle.USER_SYSTEM) {
|
||||
mContext.getContentResolver().unregisterContentObserver(mCallLogContentObserver);
|
||||
mContext.getContentResolver().unregisterContentObserver(mMmsSmsContentObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +297,15 @@ public class DataManager {
|
||||
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);
|
||||
@@ -387,11 +419,26 @@ public class DataManager {
|
||||
return mContactsContentObservers.get(userId);
|
||||
}
|
||||
|
||||
@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 @@ public class DataManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 @@ public class DataManager {
|
||||
|
||||
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 @@ public class DataManager {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.content.LocusId;
|
||||
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 @@ public class PackageData {
|
||||
@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 class PackageData {
|
||||
}
|
||||
|
||||
public boolean isDefaultDialer() {
|
||||
return mIsDefaultDialer;
|
||||
return mIsDefaultDialerPredicate.test(mPackageName);
|
||||
}
|
||||
|
||||
public boolean isDefaultSmsApp() {
|
||||
return mIsDefaultSmsApp;
|
||||
return mIsDefaultSmsAppPredicate.test(mPackageName);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -141,14 +146,6 @@ public class PackageData {
|
||||
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.
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,12 @@ class UserData {
|
||||
|
||||
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 @@ class UserData {
|
||||
*/
|
||||
@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 @@ class UserData {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package com.android.server.people.data;
|
||||
|
||||
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.os.UserManager;
|
||||
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.List;
|
||||
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 @@ public final class DataManagerTest {
|
||||
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 @@ public final class DataManagerTest {
|
||||
@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 @@ public final class DataManagerTest {
|
||||
|
||||
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 @@ public final class DataManagerTest {
|
||||
|
||||
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 @@ public final class DataManagerTest {
|
||||
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 @@ public final class DataManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -416,6 +548,27 @@ public final class DataManagerTest {
|
||||
return mContactsQueryHelper;
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@ public final class PackageDataTest {
|
||||
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 @@ public final class PackageDataTest {
|
||||
|
||||
@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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user