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:
Danning Chen
2020-02-03 23:58:53 +00:00
committed by Android (Google) Code Review
12 changed files with 1283 additions and 40 deletions

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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.
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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() {
}
}

View File

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

View File

@@ -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;

View File

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

View File

@@ -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);

View File

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