People Service data layer main structure

Change-Id: I53bf5f1bce3234da7e1dcf8cfdd293120781aef4
Test: Build and run on a test device
Bug: 146522621
This commit is contained in:
Danning Chen
2020-01-14 16:09:24 -08:00
parent 6923cdd83d
commit a1bf86d516
27 changed files with 4278 additions and 9 deletions

View File

@@ -16,6 +16,7 @@
package com.android.server.people;
import android.annotation.NonNull;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.AppTarget;
@@ -29,6 +30,7 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import com.android.server.people.data.DataManager;
import java.util.List;
import java.util.Map;
@@ -41,6 +43,8 @@ public class PeopleService extends SystemService {
private static final String TAG = "PeopleService";
private final DataManager mDataManager;
/**
* Initializes the system service.
*
@@ -48,6 +52,15 @@ public class PeopleService extends SystemService {
*/
public PeopleService(Context context) {
super(context);
mDataManager = new DataManager(context);
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
mDataManager.initialize();
}
}
@Override
@@ -55,6 +68,16 @@ public class PeopleService extends SystemService {
publishLocalService(PeopleServiceInternal.class, new LocalService());
}
@Override
public void onUnlockUser(@NonNull TargetUser targetUser) {
mDataManager.onUserUnlocked(targetUser.getUserIdentifier());
}
@Override
public void onStopUser(@NonNull TargetUser targetUser) {
mDataManager.onUserStopped(targetUser.getUserIdentifier());
}
@VisibleForTesting
final class LocalService extends PeopleServiceInternal {
@@ -63,7 +86,7 @@ public class PeopleService extends SystemService {
@Override
public void onCreatePredictionSession(AppPredictionContext context,
AppPredictionSessionId sessionId) {
mSessions.put(sessionId, new SessionInfo(context));
mSessions.put(sessionId, new SessionInfo(context, mDataManager));
}
@Override

View File

@@ -24,6 +24,7 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Slog;
import com.android.server.people.data.DataManager;
import com.android.server.people.prediction.ConversationPredictor;
import java.util.List;
@@ -37,9 +38,9 @@ class SessionInfo {
private final RemoteCallbackList<IPredictionCallback> mCallbacks =
new RemoteCallbackList<>();
SessionInfo(AppPredictionContext predictionContext) {
SessionInfo(AppPredictionContext predictionContext, DataManager dataManager) {
mConversationPredictor = new ConversationPredictor(predictionContext,
this::updatePredictions);
this::updatePredictions, dataManager);
}
void addCallback(IPredictionCallback callback) {

View File

@@ -0,0 +1,95 @@
/*
* 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.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** An {@link EventHistory} that aggregates multiple {@link EventHistory}. */
class AggregateEventHistoryImpl implements EventHistory {
private final List<EventHistory> mEventHistoryList = new ArrayList<>();
@NonNull
@Override
public EventIndex getEventIndex(int eventType) {
for (EventHistory eventHistory : mEventHistoryList) {
EventIndex eventIndex = eventHistory.getEventIndex(eventType);
if (!eventIndex.isEmpty()) {
return eventIndex;
}
}
return EventIndex.EMPTY;
}
@NonNull
@Override
public EventIndex getEventIndex(Set<Integer> eventTypes) {
EventIndex merged = new EventIndex();
for (EventHistory eventHistory : mEventHistoryList) {
EventIndex eventIndex = eventHistory.getEventIndex(eventTypes);
if (!eventIndex.isEmpty()) {
merged = EventIndex.combine(merged, eventIndex);
}
}
return merged;
}
@NonNull
@Override
public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
List<Event> results = new ArrayList<>();
for (EventHistory eventHistory : mEventHistoryList) {
EventIndex eventIndex = eventHistory.getEventIndex(eventTypes);
if (eventIndex.isEmpty()) {
continue;
}
List<Event> queryResults = eventHistory.queryEvents(eventTypes, startTime, endTime);
results = combineEventLists(results, queryResults);
}
return results;
}
void addEventHistory(EventHistory eventHistory) {
mEventHistoryList.add(eventHistory);
}
/**
* Combines the sorted events (in chronological order) from the given 2 lists {@code lhs}
* and {@code rhs} and preserves the order.
*/
private List<Event> combineEventLists(List<Event> lhs, List<Event> rhs) {
List<Event> results = new ArrayList<>();
int i = 0, j = 0;
while (i < lhs.size() && j < rhs.size()) {
if (lhs.get(i).getTimestamp() < rhs.get(j).getTimestamp()) {
results.add(lhs.get(i++));
} else {
results.add(rhs.get(j++));
}
}
if (i < lhs.size()) {
results.addAll(lhs.subList(i, lhs.size()));
} else if (j < rhs.size()) {
results.addAll(rhs.subList(j, rhs.size()));
}
return results;
}
}

View File

@@ -0,0 +1,182 @@
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Slog;
/** A helper class that queries the Contacts database. */
class ContactsQueryHelper {
private static final String TAG = "ContactsQueryHelper";
private final Context mContext;
private Uri mContactUri;
private boolean mIsStarred;
private String mPhoneNumber;
private long mLastUpdatedTimestamp;
ContactsQueryHelper(Context context) {
mContext = context;
}
/**
* Queries the Contacts database with the given contact URI and returns whether the query runs
* successfully.
*/
@WorkerThread
boolean query(@NonNull String contactUri) {
if (TextUtils.isEmpty(contactUri)) {
return false;
}
Uri uri = Uri.parse(contactUri);
if ("tel".equals(uri.getScheme())) {
return queryWithPhoneNumber(uri.getSchemeSpecificPart());
} else if ("mailto".equals(uri.getScheme())) {
return queryWithEmail(uri.getSchemeSpecificPart());
} else if (contactUri.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
return queryWithUri(uri);
}
return false;
}
/** Queries the Contacts database and read the most recently updated contact. */
@WorkerThread
boolean querySince(long sinceTime) {
final String[] projection = new String[] {
Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER,
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
String selection = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
String[] selectionArgs = new String[] {Long.toString(sinceTime)};
return queryContact(Contacts.CONTENT_URI, projection, selection, selectionArgs);
}
@Nullable
Uri getContactUri() {
return mContactUri;
}
boolean isStarred() {
return mIsStarred;
}
@Nullable
String getPhoneNumber() {
return mPhoneNumber;
}
long getLastUpdatedTimestamp() {
return mLastUpdatedTimestamp;
}
private boolean queryWithPhoneNumber(String phoneNumber) {
Uri phoneUri = Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
return queryWithUri(phoneUri);
}
private boolean queryWithEmail(String email) {
Uri emailUri = Uri.withAppendedPath(
ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(email));
return queryWithUri(emailUri);
}
private boolean queryWithUri(@NonNull Uri uri) {
final String[] projection = new String[] {
Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER };
return queryContact(uri, projection, /* selection= */ null, /* selectionArgs= */ null);
}
private boolean queryContact(@NonNull Uri uri, @NonNull String[] projection,
@Nullable String selection, @Nullable String[] selectionArgs) {
long contactId;
String lookupKey = null;
boolean hasPhoneNumber = false;
boolean found = false;
try (Cursor cursor = mContext.getContentResolver().query(
uri, projection, selection, selectionArgs, /* sortOrder= */ null)) {
if (cursor == null) {
Slog.w(TAG, "Cursor is null when querying contact.");
return false;
}
while (cursor.moveToNext()) {
// Contact ID
int idIndex = cursor.getColumnIndex(Contacts._ID);
contactId = cursor.getLong(idIndex);
// Lookup key
int lookupKeyIndex = cursor.getColumnIndex(Contacts.LOOKUP_KEY);
lookupKey = cursor.getString(lookupKeyIndex);
mContactUri = Contacts.getLookupUri(contactId, lookupKey);
// Starred
int starredIndex = cursor.getColumnIndex(Contacts.STARRED);
mIsStarred = cursor.getInt(starredIndex) != 0;
// Has phone number
int hasPhoneNumIndex = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER);
hasPhoneNumber = cursor.getInt(hasPhoneNumIndex) != 0;
// Last updated timestamp
int lastUpdatedTimestampIndex = cursor.getColumnIndex(
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
if (lastUpdatedTimestampIndex >= 0) {
mLastUpdatedTimestamp = cursor.getLong(lastUpdatedTimestampIndex);
}
found = true;
}
}
if (found && lookupKey != null && hasPhoneNumber) {
return queryPhoneNumber(lookupKey);
}
return found;
}
private boolean queryPhoneNumber(String lookupKey) {
String[] projection = new String[] {
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER };
String selection = Contacts.LOOKUP_KEY + " = ?";
String[] selectionArgs = new String[] { lookupKey };
try (Cursor cursor = mContext.getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection,
selectionArgs, /* sortOrder= */ null)) {
if (cursor == null) {
Slog.w(TAG, "Cursor is null when querying contact phone number.");
return false;
}
while (cursor.moveToNext()) {
// Phone number
int phoneNumIdx = cursor.getColumnIndex(
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER);
if (phoneNumIdx >= 0) {
mPhoneNumber = cursor.getString(phoneNumIdx);
}
}
}
return true;
}
}

View File

@@ -0,0 +1,372 @@
/*
* 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Represents a conversation that is provided by the app based on {@link ShortcutInfo}.
*/
public class ConversationInfo {
private static final int FLAG_VIP = 1;
private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;
private static final int FLAG_BUBBLED = 1 << 2;
private static final int FLAG_PERSON_IMPORTANT = 1 << 3;
private static final int FLAG_PERSON_BOT = 1 << 4;
private static final int FLAG_CONTACT_STARRED = 1 << 5;
private static final int FLAG_DEMOTED = 1 << 6;
@IntDef(flag = true, prefix = {"FLAG_"}, value = {
FLAG_VIP,
FLAG_NOTIFICATION_SILENCED,
FLAG_BUBBLED,
FLAG_PERSON_IMPORTANT,
FLAG_PERSON_BOT,
FLAG_CONTACT_STARRED,
FLAG_DEMOTED,
})
@Retention(RetentionPolicy.SOURCE)
private @interface ConversationFlags {
}
@NonNull
private String mShortcutId;
@Nullable
private LocusId mLocusId;
@Nullable
private Uri mContactUri;
@Nullable
private String mContactPhoneNumber;
@Nullable
private String mNotificationChannelId;
@ShortcutFlags
private int mShortcutFlags;
@ConversationFlags
private int mConversationFlags;
private ConversationInfo(Builder builder) {
mShortcutId = builder.mShortcutId;
mLocusId = builder.mLocusId;
mContactUri = builder.mContactUri;
mContactPhoneNumber = builder.mContactPhoneNumber;
mNotificationChannelId = builder.mNotificationChannelId;
mShortcutFlags = builder.mShortcutFlags;
mConversationFlags = builder.mConversationFlags;
}
@NonNull
public String getShortcutId() {
return mShortcutId;
}
@Nullable
LocusId getLocusId() {
return mLocusId;
}
/** The URI to look up the entry in the contacts data provider. */
@Nullable
Uri getContactUri() {
return mContactUri;
}
/** The phone number of the associated contact. */
@Nullable
String getContactPhoneNumber() {
return mContactPhoneNumber;
}
/**
* ID of the {@link android.app.NotificationChannel} where the notifications for this
* conversation are posted.
*/
@Nullable
String getNotificationChannelId() {
return mNotificationChannelId;
}
/** Whether the shortcut for this conversation is set long-lived by the app. */
public boolean isShortcutLongLived() {
return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
}
/** Whether this conversation is marked as VIP by the user. */
public boolean isVip() {
return hasConversationFlags(FLAG_VIP);
}
/** Whether the notifications for this conversation should be silenced. */
public boolean isNotificationSilenced() {
return hasConversationFlags(FLAG_NOTIFICATION_SILENCED);
}
/** Whether the notifications for this conversation should show in bubbles. */
public boolean isBubbled() {
return hasConversationFlags(FLAG_BUBBLED);
}
/**
* Whether this conversation is demoted by the user. New notifications for the demoted
* conversation will not show in the conversation space.
*/
public boolean isDemoted() {
return hasConversationFlags(FLAG_DEMOTED);
}
/** Whether the associated person is marked as important by the app. */
public boolean isPersonImportant() {
return hasConversationFlags(FLAG_PERSON_IMPORTANT);
}
/** Whether the associated person is marked as a bot by the app. */
public boolean isPersonBot() {
return hasConversationFlags(FLAG_PERSON_BOT);
}
/** Whether the associated contact is marked as starred by the user. */
public boolean isContactStarred() {
return hasConversationFlags(FLAG_CONTACT_STARRED);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ConversationInfo)) {
return false;
}
ConversationInfo other = (ConversationInfo) obj;
return Objects.equals(mShortcutId, other.mShortcutId)
&& Objects.equals(mLocusId, other.mLocusId)
&& Objects.equals(mContactUri, other.mContactUri)
&& Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
&& Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
&& mShortcutFlags == other.mShortcutFlags
&& mConversationFlags == other.mConversationFlags;
}
@Override
public int hashCode() {
return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
mNotificationChannelId, mShortcutFlags, mConversationFlags);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ConversationInfo {");
sb.append("shortcutId=").append(mShortcutId);
sb.append(", locusId=").append(mLocusId);
sb.append(", contactUri=").append(mContactUri);
sb.append(", phoneNumber=").append(mContactPhoneNumber);
sb.append(", notificationChannelId=").append(mNotificationChannelId);
sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
sb.append(" [");
if (isShortcutLongLived()) {
sb.append("Liv");
}
sb.append("]");
sb.append(", conversationFlags=0x").append(Integer.toHexString(mConversationFlags));
sb.append(" [");
if (isVip()) {
sb.append("Vip");
}
if (isNotificationSilenced()) {
sb.append("Sil");
}
if (isBubbled()) {
sb.append("Bub");
}
if (isDemoted()) {
sb.append("Dem");
}
if (isPersonImportant()) {
sb.append("Imp");
}
if (isPersonBot()) {
sb.append("Bot");
}
if (isContactStarred()) {
sb.append("Sta");
}
sb.append("]}");
return sb.toString();
}
private boolean hasShortcutFlags(@ShortcutFlags int flags) {
return (mShortcutFlags & flags) == flags;
}
private boolean hasConversationFlags(@ConversationFlags int flags) {
return (mConversationFlags & flags) == flags;
}
/**
* Builder class for {@link ConversationInfo} objects.
*/
static class Builder {
private String mShortcutId;
@Nullable
private LocusId mLocusId;
@Nullable
private Uri mContactUri;
@Nullable
private String mContactPhoneNumber;
@Nullable
private String mNotificationChannelId;
@ShortcutFlags
private int mShortcutFlags;
@ConversationFlags
private int mConversationFlags;
Builder() {
}
Builder(@NonNull ConversationInfo conversationInfo) {
if (mShortcutId == null) {
mShortcutId = conversationInfo.mShortcutId;
} else {
Preconditions.checkArgument(mShortcutId.equals(conversationInfo.mShortcutId));
}
mLocusId = conversationInfo.mLocusId;
mContactUri = conversationInfo.mContactUri;
mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
mNotificationChannelId = conversationInfo.mNotificationChannelId;
mShortcutFlags = conversationInfo.mShortcutFlags;
mConversationFlags = conversationInfo.mConversationFlags;
}
Builder setShortcutId(@NonNull String shortcutId) {
mShortcutId = shortcutId;
return this;
}
Builder setLocusId(LocusId locusId) {
mLocusId = locusId;
return this;
}
Builder setContactUri(Uri contactUri) {
mContactUri = contactUri;
return this;
}
Builder setContactPhoneNumber(String phoneNumber) {
mContactPhoneNumber = phoneNumber;
return this;
}
Builder setNotificationChannelId(String notificationChannelId) {
mNotificationChannelId = notificationChannelId;
return this;
}
Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
mShortcutFlags = shortcutFlags;
return this;
}
Builder setConversationFlags(@ConversationFlags int conversationFlags) {
mConversationFlags = conversationFlags;
return this;
}
Builder setVip(boolean value) {
return setConversationFlag(FLAG_VIP, value);
}
Builder setNotificationSilenced(boolean value) {
return setConversationFlag(FLAG_NOTIFICATION_SILENCED, value);
}
Builder setBubbled(boolean value) {
return setConversationFlag(FLAG_BUBBLED, value);
}
Builder setDemoted(boolean value) {
return setConversationFlag(FLAG_DEMOTED, value);
}
Builder setPersonImportant(boolean value) {
return setConversationFlag(FLAG_PERSON_IMPORTANT, value);
}
Builder setPersonBot(boolean value) {
return setConversationFlag(FLAG_PERSON_BOT, value);
}
Builder setContactStarred(boolean value) {
return setConversationFlag(FLAG_CONTACT_STARRED, value);
}
private Builder setConversationFlag(@ConversationFlags int flags, boolean value) {
if (value) {
return addConversationFlags(flags);
} else {
return removeConversationFlags(flags);
}
}
private Builder addConversationFlags(@ConversationFlags int flags) {
mConversationFlags |= flags;
return this;
}
private Builder removeConversationFlags(@ConversationFlags int flags) {
mConversationFlags &= ~flags;
return this;
}
ConversationInfo build() {
Objects.requireNonNull(mShortcutId);
return new ConversationInfo(this);
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.net.Uri;
import android.util.ArrayMap;
import java.util.Map;
import java.util.function.Consumer;
/** The store that stores and accesses the conversations data for a package. */
class ConversationStore {
// Shortcut ID -> Conversation Info
private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>();
// Locus ID -> Shortcut ID
private final Map<LocusId, String> mLocusIdToShortcutIdMap = new ArrayMap<>();
// Contact URI -> Shortcut ID
private final Map<Uri, String> mContactUriToShortcutIdMap = new ArrayMap<>();
// Phone Number -> Shortcut ID
private final Map<String, String> mPhoneNumberToShortcutIdMap = new ArrayMap<>();
void addOrUpdate(@NonNull ConversationInfo conversationInfo) {
mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo);
LocusId locusId = conversationInfo.getLocusId();
if (locusId != null) {
mLocusIdToShortcutIdMap.put(locusId, conversationInfo.getShortcutId());
}
Uri contactUri = conversationInfo.getContactUri();
if (contactUri != null) {
mContactUriToShortcutIdMap.put(contactUri, conversationInfo.getShortcutId());
}
String phoneNumber = conversationInfo.getContactPhoneNumber();
if (phoneNumber != null) {
mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId());
}
}
void deleteConversation(@NonNull String shortcutId) {
ConversationInfo conversationInfo = mConversationInfoMap.remove(shortcutId);
if (conversationInfo == null) {
return;
}
LocusId locusId = conversationInfo.getLocusId();
if (locusId != null) {
mLocusIdToShortcutIdMap.remove(locusId);
}
Uri contactUri = conversationInfo.getContactUri();
if (contactUri != null) {
mContactUriToShortcutIdMap.remove(contactUri);
}
String phoneNumber = conversationInfo.getContactPhoneNumber();
if (phoneNumber != null) {
mPhoneNumberToShortcutIdMap.remove(phoneNumber);
}
}
void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
for (ConversationInfo ci : mConversationInfoMap.values()) {
consumer.accept(ci);
}
}
@Nullable
ConversationInfo getConversation(@Nullable String shortcutId) {
return shortcutId != null ? mConversationInfoMap.get(shortcutId) : null;
}
@Nullable
ConversationInfo getConversationByLocusId(@NonNull LocusId locusId) {
return getConversation(mLocusIdToShortcutIdMap.get(locusId));
}
@Nullable
ConversationInfo getConversationByContactUri(@NonNull Uri contactUri) {
return getConversation(mContactUriToShortcutIdMap.get(contactUri));
}
@Nullable
ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) {
return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber));
}
}

View File

@@ -0,0 +1,559 @@
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.Notification;
import android.app.Person;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutManager.ShareShortcutInfo;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract.Contacts;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ChooserActivity;
import com.android.internal.os.BackgroundThread;
import com.android.internal.telephony.SmsApplication;
import com.android.server.LocalServices;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* A class manages the lifecycle of the conversations and associated data, and exposes the methods
* to access the data in People Service and other system services.
*/
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 USAGE_STATS_QUERY_INTERVAL_SEC = 120L;
private final Context mContext;
private final Injector mInjector;
private final ScheduledExecutorService mUsageStatsQueryExecutor;
private final SparseArray<UserData> mUserDataArray = new SparseArray<>();
private final SparseArray<BroadcastReceiver> mBroadcastReceivers = new SparseArray<>();
private final SparseArray<ContentObserver> mContactsContentObservers = new SparseArray<>();
private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
private final SparseArray<NotificationListenerService> mNotificationListeners =
new SparseArray<>();
private ShortcutServiceInternal mShortcutServiceInternal;
private UsageStatsManagerInternal mUsageStatsManagerInternal;
private ShortcutManager mShortcutManager;
private UserManager mUserManager;
public DataManager(Context context) {
mContext = context;
mInjector = new Injector();
mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
}
@VisibleForTesting
DataManager(Context context, Injector injector) {
mContext = context;
mInjector = injector;
mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
}
/** Initialization. Called when the system services are up running. */
public void initialize() {
mShortcutServiceInternal = LocalServices.getService(ShortcutServiceInternal.class);
mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
mShortcutManager = mContext.getSystemService(ShortcutManager.class);
mUserManager = mContext.getSystemService(UserManager.class);
mShortcutServiceInternal.addListener(new ShortcutServiceListener());
}
/** This method is called when a user is unlocked. */
public void onUserUnlocked(int userId) {
UserData userData = mUserDataArray.get(userId);
if (userData == null) {
userData = new UserData(userId);
mUserDataArray.put(userId, userData);
}
userData.setUserUnlocked();
updateDefaultDialer(userData);
updateDefaultSmsApp(userData);
ScheduledFuture<?> scheduledFuture = mUsageStatsQueryExecutor.scheduleAtFixedRate(
new UsageStatsQueryRunnable(userId), 1L, USAGE_STATS_QUERY_INTERVAL_SEC,
TimeUnit.SECONDS);
mUsageStatsQueryFutures.put(userId, scheduledFuture);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED);
intentFilter.addAction(SmsApplication.ACTION_DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL);
BroadcastReceiver broadcastReceiver = new PerUserBroadcastReceiver(userId);
mBroadcastReceivers.put(userId, broadcastReceiver);
mContext.registerReceiverAsUser(
broadcastReceiver, UserHandle.of(userId), intentFilter, null, null);
ContentObserver contactsContentObserver = new ContactsContentObserver(
BackgroundThread.getHandler());
mContactsContentObservers.put(userId, contactsContentObserver);
mContext.getContentResolver().registerContentObserver(
Contacts.CONTENT_URI, /* notifyForDescendants= */ true,
contactsContentObserver, userId);
NotificationListener notificationListener = new NotificationListener();
mNotificationListeners.put(userId, notificationListener);
try {
notificationListener.registerAsSystemService(mContext,
new ComponentName(PLATFORM_PACKAGE_NAME, getClass().getSimpleName()),
UserHandle.myUserId());
} catch (RemoteException e) {
// Should never occur for local calls.
}
}
/** This method is called when a user is stopped. */
public void onUserStopped(int userId) {
if (mUserDataArray.indexOfKey(userId) >= 0) {
mUserDataArray.get(userId).setUserStopped();
}
if (mUsageStatsQueryFutures.indexOfKey(userId) >= 0) {
mUsageStatsQueryFutures.valueAt(userId).cancel(true);
}
if (mBroadcastReceivers.indexOfKey(userId) >= 0) {
mContext.unregisterReceiver(mBroadcastReceivers.get(userId));
}
if (mContactsContentObservers.indexOfKey(userId) >= 0) {
mContext.getContentResolver().unregisterContentObserver(
mContactsContentObservers.get(userId));
}
if (mNotificationListeners.indexOfKey(userId) >= 0) {
try {
mNotificationListeners.get(userId).unregisterAsSystemService();
} catch (RemoteException e) {
// Should never occur for local calls.
}
}
}
/**
* Iterates through all the {@link PackageData}s owned by the unlocked users who are in the
* same profile group as the calling user.
*/
public void forAllPackages(Consumer<PackageData> consumer) {
List<UserInfo> users = mUserManager.getEnabledProfiles(mInjector.getCallingUserId());
for (UserInfo userInfo : users) {
UserData userData = getUnlockedUserData(userInfo.id);
if (userData != null) {
userData.forAllPackages(consumer);
}
}
}
/** Gets the {@link ShortcutInfo} for the given shortcut ID. */
@Nullable
public ShortcutInfo getShortcut(@NonNull String packageName, @UserIdInt int userId,
@NonNull String shortcutId) {
List<ShortcutInfo> shortcuts = getShortcuts(packageName, userId,
Collections.singletonList(shortcutId));
if (shortcuts != null && !shortcuts.isEmpty()) {
return shortcuts.get(0);
}
return null;
}
/**
* Gets the conversation {@link ShareShortcutInfo}s from all packages owned by the calling user
* that match the specified {@link IntentFilter}.
*/
public List<ShareShortcutInfo> getConversationShareTargets(
@NonNull IntentFilter intentFilter) {
List<ShareShortcutInfo> shareShortcuts = mShortcutManager.getShareTargets(intentFilter);
List<ShareShortcutInfo> result = new ArrayList<>();
for (ShareShortcutInfo shareShortcut : shareShortcuts) {
ShortcutInfo si = shareShortcut.getShortcutInfo();
if (getConversationInfo(si.getPackage(), si.getUserId(), si.getId()) != null) {
result.add(shareShortcut);
}
}
return result;
}
/** Reports the {@link AppTargetEvent} from App Prediction Manager. */
public void reportAppTargetEvent(@NonNull AppTargetEvent event,
@Nullable IntentFilter intentFilter) {
AppTarget appTarget = event.getTarget();
ShortcutInfo shortcutInfo = appTarget != null ? appTarget.getShortcutInfo() : null;
if (shortcutInfo == null || event.getAction() != AppTargetEvent.ACTION_LAUNCH) {
return;
}
PackageData packageData = getPackageData(appTarget.getPackageName(),
appTarget.getUser().getIdentifier());
if (packageData == null) {
return;
}
if (ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE.equals(event.getLaunchLocation())) {
String mimeType = intentFilter != null ? intentFilter.getDataType(0) : null;
String shortcutId = shortcutInfo.getId();
if (packageData.getConversationStore().getConversation(shortcutId) == null
|| TextUtils.isEmpty(mimeType)) {
return;
}
EventHistoryImpl eventHistory =
packageData.getEventStore().getOrCreateShortcutEventHistory(
shortcutInfo.getId());
@Event.EventType int eventType;
if (mimeType.startsWith("text/")) {
eventType = Event.TYPE_SHARE_TEXT;
} else if (mimeType.startsWith("image/")) {
eventType = Event.TYPE_SHARE_IMAGE;
} else if (mimeType.startsWith("video/")) {
eventType = Event.TYPE_SHARE_VIDEO;
} else {
eventType = Event.TYPE_SHARE_OTHER;
}
eventHistory.addEvent(new Event(System.currentTimeMillis(), eventType));
}
}
/** Gets a list of {@link ShortcutInfo}s with the given shortcut IDs. */
private List<ShortcutInfo> getShortcuts(
@NonNull String packageName, @UserIdInt int userId,
@Nullable List<String> shortcutIds) {
@ShortcutQuery.QueryFlags int queryFlags = ShortcutQuery.FLAG_MATCH_DYNAMIC
| ShortcutQuery.FLAG_MATCH_PINNED | ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
return mShortcutServiceInternal.getShortcuts(
mInjector.getCallingUserId(), /*callingPackage=*/ PLATFORM_PACKAGE_NAME,
/*changedSince=*/ 0, packageName, shortcutIds, /*componentName=*/ null, queryFlags,
userId, MY_PID, MY_UID);
}
@Nullable
private UserData getUnlockedUserData(int userId) {
UserData userData = mUserDataArray.get(userId);
return userData != null && userData.isUnlocked() ? userData : null;
}
@Nullable
private PackageData getPackageData(@NonNull String packageName, int userId) {
UserData userData = getUnlockedUserData(userId);
return userData != null ? userData.getPackageData(packageName) : null;
}
@Nullable
private ConversationInfo getConversationInfo(@NonNull String packageName, @UserIdInt int userId,
@NonNull String shortcutId) {
PackageData packageData = getPackageData(packageName, userId);
return packageData != null ? packageData.getConversationStore().getConversation(shortcutId)
: null;
}
private void updateDefaultDialer(@NonNull UserData userData) {
TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
String defaultDialer = telecomManager != null
? telecomManager.getDefaultDialerPackage(userData.getUserId()) : null;
userData.setDefaultDialer(defaultDialer);
}
private void updateDefaultSmsApp(@NonNull UserData userData) {
ComponentName component = SmsApplication.getDefaultSmsApplicationAsUser(
mContext, /* updateIfNeeded= */ false, userData.getUserId());
String defaultSmsApp = component != null ? component.getPackageName() : null;
userData.setDefaultSmsApp(defaultSmsApp);
}
@Nullable
private EventHistoryImpl getEventHistoryIfEligible(StatusBarNotification sbn) {
Notification notification = sbn.getNotification();
String shortcutId = notification.getShortcutId();
if (shortcutId == null) {
return null;
}
PackageData packageData = getPackageData(sbn.getPackageName(),
sbn.getUser().getIdentifier());
if (packageData == null
|| packageData.getConversationStore().getConversation(shortcutId) == null) {
return null;
}
return packageData.getEventStore().getOrCreateShortcutEventHistory(shortcutId);
}
@VisibleForTesting
@WorkerThread
void onShortcutAddedOrUpdated(@NonNull ShortcutInfo shortcutInfo) {
if (shortcutInfo.getPersons() == null || shortcutInfo.getPersons().length == 0) {
return;
}
UserData userData = getUnlockedUserData(shortcutInfo.getUserId());
if (userData == null) {
return;
}
PackageData packageData = userData.getOrCreatePackageData(shortcutInfo.getPackage());
ConversationStore conversationStore = packageData.getConversationStore();
ConversationInfo oldConversationInfo =
conversationStore.getConversation(shortcutInfo.getId());
ConversationInfo.Builder builder = oldConversationInfo != null
? new ConversationInfo.Builder(oldConversationInfo)
: new ConversationInfo.Builder();
builder.setShortcutId(shortcutInfo.getId());
builder.setLocusId(shortcutInfo.getLocusId());
builder.setShortcutFlags(shortcutInfo.getFlags());
Person person = shortcutInfo.getPersons()[0];
builder.setPersonImportant(person.isImportant());
builder.setPersonBot(person.isBot());
String contactUri = person.getUri();
if (contactUri != null) {
ContactsQueryHelper helper = mInjector.createContactsQueryHelper(mContext);
if (helper.query(contactUri)) {
builder.setContactUri(helper.getContactUri());
builder.setContactStarred(helper.isStarred());
builder.setContactPhoneNumber(helper.getPhoneNumber());
}
} else {
builder.setContactUri(null);
builder.setContactPhoneNumber(null);
builder.setContactStarred(false);
}
conversationStore.addOrUpdate(builder.build());
}
@VisibleForTesting
@WorkerThread
void queryUsageStatsService(@UserIdInt int userId, long currentTime, long lastQueryTime) {
UsageEvents usageEvents = mUsageStatsManagerInternal.queryEventsForUser(
userId, lastQueryTime, currentTime, false);
if (usageEvents == null) {
return;
}
while (usageEvents.hasNextEvent()) {
UsageEvents.Event e = new UsageEvents.Event();
usageEvents.getNextEvent(e);
String packageName = e.getPackageName();
PackageData packageData = getPackageData(packageName, userId);
if (packageData == null) {
continue;
}
if (e.getEventType() == UsageEvents.Event.SHORTCUT_INVOCATION) {
String shortcutId = e.getShortcutId();
if (packageData.getConversationStore().getConversation(shortcutId) != null) {
EventHistoryImpl eventHistory =
packageData.getEventStore().getOrCreateShortcutEventHistory(
shortcutId);
eventHistory.addEvent(
new Event(e.getTimeStamp(), Event.TYPE_SHORTCUT_INVOCATION));
}
}
}
}
@VisibleForTesting
ContentObserver getContactsContentObserverForTesting(@UserIdInt int userId) {
return mContactsContentObservers.get(userId);
}
@VisibleForTesting
NotificationListenerService getNotificationListenerServiceForTesting(@UserIdInt int userId) {
return mNotificationListeners.get(userId);
}
/** Observer that observes the changes in the Contacts database. */
private class ContactsContentObserver extends ContentObserver {
private long mLastUpdatedTimestamp;
private ContactsContentObserver(Handler handler) {
super(handler);
mLastUpdatedTimestamp = System.currentTimeMillis();
}
@Override
public void onChange(boolean selfChange, Uri uri, @UserIdInt int userId) {
ContactsQueryHelper helper = mInjector.createContactsQueryHelper(mContext);
if (!helper.querySince(mLastUpdatedTimestamp)) {
return;
}
Uri contactUri = helper.getContactUri();
final ConversationSelector conversationSelector = new ConversationSelector();
UserData userData = getUnlockedUserData(userId);
if (userData == null) {
return;
}
userData.forAllPackages(packageData -> {
ConversationInfo ci =
packageData.getConversationStore().getConversationByContactUri(contactUri);
if (ci != null) {
conversationSelector.mConversationStore =
packageData.getConversationStore();
conversationSelector.mConversationInfo = ci;
}
});
if (conversationSelector.mConversationInfo == null) {
return;
}
ConversationInfo.Builder builder =
new ConversationInfo.Builder(conversationSelector.mConversationInfo);
builder.setContactStarred(helper.isStarred());
builder.setContactPhoneNumber(helper.getPhoneNumber());
conversationSelector.mConversationStore.addOrUpdate(builder.build());
mLastUpdatedTimestamp = helper.getLastUpdatedTimestamp();
}
private class ConversationSelector {
private ConversationStore mConversationStore = null;
private ConversationInfo mConversationInfo = null;
}
}
/** Listener for the shortcut data changes. */
private class ShortcutServiceListener implements
ShortcutServiceInternal.ShortcutChangeListener {
@Override
public void onShortcutChanged(@NonNull String packageName, int userId) {
BackgroundThread.getExecutor().execute(() -> {
List<ShortcutInfo> shortcuts = getShortcuts(packageName, userId,
/*shortcutIds=*/ null);
for (ShortcutInfo shortcut : shortcuts) {
onShortcutAddedOrUpdated(shortcut);
}
});
}
}
/** Listener for the notifications and their settings changes. */
private class NotificationListener extends NotificationListenerService {
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
int reason) {
if (reason != REASON_CLICK) {
return;
}
EventHistoryImpl eventHistory = getEventHistoryIfEligible(sbn);
if (eventHistory == null) {
return;
}
long currentTime = System.currentTimeMillis();
eventHistory.addEvent(new Event(currentTime, Event.TYPE_NOTIFICATION_OPENED));
}
}
/**
* A {@link Runnable} that queries the Usage Stats Service for recent events for a specified
* user.
*/
private class UsageStatsQueryRunnable implements Runnable {
private final int mUserId;
private long mLastQueryTime;
private UsageStatsQueryRunnable(int userId) {
mUserId = userId;
mLastQueryTime = System.currentTimeMillis() - USAGE_STATS_QUERY_MAX_EVENT_AGE_MS;
}
@Override
public void run() {
long currentTime = System.currentTimeMillis();
queryUsageStatsService(mUserId, currentTime, mLastQueryTime);
mLastQueryTime = currentTime;
}
}
/** A {@link BroadcastReceiver} that receives the intents for a specified user. */
private class PerUserBroadcastReceiver extends BroadcastReceiver {
private final int mUserId;
private PerUserBroadcastReceiver(int userId) {
mUserId = userId;
}
@Override
public void onReceive(Context context, Intent intent) {
UserData userData = getUnlockedUserData(mUserId);
if (userData == null) {
return;
}
if (TelecomManager.ACTION_DEFAULT_DIALER_CHANGED.equals(intent.getAction())) {
String defaultDialer = intent.getStringExtra(
TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME);
userData.setDefaultDialer(defaultDialer);
} else if (SmsApplication.ACTION_DEFAULT_SMS_PACKAGE_CHANGED_INTERNAL.equals(
intent.getAction())) {
updateDefaultSmsApp(userData);
}
}
}
@VisibleForTesting
static class Injector {
ScheduledExecutorService createScheduledExecutor() {
return Executors.newSingleThreadScheduledExecutor();
}
ContactsQueryHelper createContactsQueryHelper(Context context) {
return new ContactsQueryHelper(context);
}
int getCallingUserId() {
return Binder.getCallingUserHandle().getIdentifier();
}
}
}

View File

@@ -0,0 +1,195 @@
/*
* 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.format.DateFormat;
import android.util.ArraySet;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Set;
/** An event representing the interaction with a specific conversation or app. */
public class Event {
public static final int TYPE_SHORTCUT_INVOCATION = 1;
public static final int TYPE_NOTIFICATION_POSTED = 2;
public static final int TYPE_NOTIFICATION_OPENED = 3;
public static final int TYPE_SHARE_TEXT = 4;
public static final int TYPE_SHARE_IMAGE = 5;
public static final int TYPE_SHARE_VIDEO = 6;
public static final int TYPE_SHARE_OTHER = 7;
public static final int TYPE_SMS_OUTGOING = 8;
public static final int TYPE_SMS_INCOMING = 9;
public static final int TYPE_CALL_OUTGOING = 10;
public static final int TYPE_CALL_INCOMING = 11;
public static final int TYPE_CALL_MISSED = 12;
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_SHORTCUT_INVOCATION,
TYPE_NOTIFICATION_POSTED,
TYPE_NOTIFICATION_OPENED,
TYPE_SHARE_TEXT,
TYPE_SHARE_IMAGE,
TYPE_SHARE_VIDEO,
TYPE_SHARE_OTHER,
TYPE_SMS_OUTGOING,
TYPE_SMS_INCOMING,
TYPE_CALL_OUTGOING,
TYPE_CALL_INCOMING,
TYPE_CALL_MISSED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventType {}
public static final Set<Integer> NOTIFICATION_EVENT_TYPES = new ArraySet<>();
public static final Set<Integer> SHARE_EVENT_TYPES = new ArraySet<>();
public static final Set<Integer> SMS_EVENT_TYPES = new ArraySet<>();
public static final Set<Integer> CALL_EVENT_TYPES = new ArraySet<>();
public static final Set<Integer> ALL_EVENT_TYPES = new ArraySet<>();
static {
NOTIFICATION_EVENT_TYPES.add(TYPE_NOTIFICATION_POSTED);
NOTIFICATION_EVENT_TYPES.add(TYPE_NOTIFICATION_OPENED);
SHARE_EVENT_TYPES.add(TYPE_SHARE_TEXT);
SHARE_EVENT_TYPES.add(TYPE_SHARE_IMAGE);
SHARE_EVENT_TYPES.add(TYPE_SHARE_VIDEO);
SHARE_EVENT_TYPES.add(TYPE_SHARE_OTHER);
SMS_EVENT_TYPES.add(TYPE_SMS_INCOMING);
SMS_EVENT_TYPES.add(TYPE_SMS_OUTGOING);
CALL_EVENT_TYPES.add(TYPE_CALL_INCOMING);
CALL_EVENT_TYPES.add(TYPE_CALL_OUTGOING);
CALL_EVENT_TYPES.add(TYPE_CALL_MISSED);
ALL_EVENT_TYPES.add(TYPE_SHORTCUT_INVOCATION);
ALL_EVENT_TYPES.addAll(NOTIFICATION_EVENT_TYPES);
ALL_EVENT_TYPES.addAll(SHARE_EVENT_TYPES);
ALL_EVENT_TYPES.addAll(SMS_EVENT_TYPES);
ALL_EVENT_TYPES.addAll(CALL_EVENT_TYPES);
}
private final long mTimestamp;
private final int mType;
private final CallDetails mCallDetails;
Event(long timestamp, @EventType int type) {
mTimestamp = timestamp;
mType = type;
mCallDetails = null;
}
private Event(@NonNull Builder builder) {
mTimestamp = builder.mTimestamp;
mType = builder.mType;
mCallDetails = builder.mCallDetails;
}
public long getTimestamp() {
return mTimestamp;
}
public @EventType int getType() {
return mType;
}
/**
* Gets the {@link CallDetails} of the event. It is only available if the event type is one of
* {@code CALL_EVENT_TYPES}, otherwise, it's always {@code null}.
*/
@Nullable
public CallDetails getCallDetails() {
return mCallDetails;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Event {");
sb.append("timestamp=").append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimestamp));
sb.append(", type=").append(mType);
if (mCallDetails != null) {
sb.append(", callDetails=").append(mCallDetails);
}
sb.append("}");
return sb.toString();
}
/** Type-specific details of a call event. */
public static class CallDetails {
private final long mDurationSeconds;
CallDetails(long durationSeconds) {
mDurationSeconds = durationSeconds;
}
public long getDurationSeconds() {
return mDurationSeconds;
}
@Override
public String toString() {
return "CallDetails {durationSeconds=" + mDurationSeconds + "}";
}
}
/** Builder class for {@link Event} objects. */
static class Builder {
private final long mTimestamp;
private final int mType;
private CallDetails mCallDetails;
Builder(long timestamp, @EventType int type) {
mTimestamp = timestamp;
mType = type;
}
Builder setCallDetails(CallDetails callDetails) {
Preconditions.checkArgument(CALL_EVENT_TYPES.contains(mType));
mCallDetails = callDetails;
return this;
}
Event build() {
return new Event(this);
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.NonNull;
import java.util.List;
import java.util.Set;
/** The interface for querying the event time distribution and details. */
public interface EventHistory {
/** Gets the {@link EventIndex} for the specified event type. */
@NonNull
EventIndex getEventIndex(@Event.EventType int eventType);
/** Gets the combined {@link EventIndex} for a set of event types. */
@NonNull
EventIndex getEventIndex(Set<Integer> eventTypes);
/**
* Returns a {@link List} of {@link Event}s those timestamps are between the specified {@code
* fromTimestamp}, inclusive, and {@code toTimestamp} exclusive, and match the specified event
* types.
*
* @return a list of matched events in chronological order.
*/
@NonNull
List<Event> queryEvents(Set<Integer> eventTypes, long fromTimestamp, long toTimestamp);
}

View File

@@ -0,0 +1,88 @@
/*
* 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.NonNull;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
import java.util.Set;
class EventHistoryImpl implements EventHistory {
private final Injector mInjector;
// Event Type -> Event Index
private final SparseArray<EventIndex> mEventIndexArray = new SparseArray<>();
private final EventList mRecentEvents = new EventList();
EventHistoryImpl() {
mInjector = new Injector();
}
@VisibleForTesting
EventHistoryImpl(Injector injector) {
mInjector = injector;
}
@Override
@NonNull
public EventIndex getEventIndex(@Event.EventType int eventType) {
EventIndex eventIndex = mEventIndexArray.get(eventType);
return eventIndex != null ? new EventIndex(eventIndex) : mInjector.createEventIndex();
}
@Override
@NonNull
public EventIndex getEventIndex(Set<Integer> eventTypes) {
EventIndex combined = mInjector.createEventIndex();
for (@Event.EventType int eventType : eventTypes) {
EventIndex eventIndex = mEventIndexArray.get(eventType);
if (eventIndex != null) {
combined = EventIndex.combine(combined, eventIndex);
}
}
return combined;
}
@Override
@NonNull
public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
return mRecentEvents.queryEvents(eventTypes, startTime, endTime);
}
void addEvent(Event event) {
EventIndex eventIndex = mEventIndexArray.get(event.getType());
if (eventIndex == null) {
eventIndex = mInjector.createEventIndex();
mEventIndexArray.put(event.getType(), eventIndex);
}
eventIndex.addEvent(event.getTimestamp());
mRecentEvents.add(event);
}
@VisibleForTesting
static class Injector {
EventIndex createEventIndex() {
return new EventIndex();
}
}
}

View File

@@ -0,0 +1,377 @@
/*
* 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.format.DateFormat;
import android.util.Range;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
import java.util.function.Function;
/**
* The index of {@link Event}s. It is used for quickly looking up the time distribution of
* {@link Event}s based on {@code Event#getTimestamp()}.
*
* <p>The 64-bits {code long} is used as the bitmap index. Each bit is to denote whether there are
* any events in a specified time slot. The least significant bit is for the most recent time slot.
* And the most significant bit is for the oldest time slot.
*
* <p>Multiple {code long}s are used to index the events in different time grains. For the recent
* events, the fine-grained bitmap index can provide the narrower time range. For the older events,
* the coarse-grained bitmap index can cover longer period but can only provide wider time range.
*
* <p>E.g. the below chart shows how the bitmap indexes index the events in the past 24 hours:
* <pre>
* 2020/1/3 2020/1/4
* 0:00 4:00 8:00 12:00 16:00 20:00 0:00
* --+-----------------------------------------------------------------------+- 1 day per bit
* --+-----------+-----------+-----------+-----------+-----------+-----------+- 4 hours per bit
* --+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+- 1 hour per bit
* +++++++++ 2 minutes per bit
* </pre>
*/
public class EventIndex {
private static final int LONG_SIZE_BITS = 64;
private static final int TIME_SLOT_ONE_DAY = 0;
private static final int TIME_SLOT_FOUR_HOURS = 1;
private static final int TIME_SLOT_ONE_HOUR = 2;
private static final int TIME_SLOT_TWO_MINUTES = 3;
@IntDef(prefix = {"TIME_SLOT_"}, value = {
TIME_SLOT_ONE_DAY,
TIME_SLOT_FOUR_HOURS,
TIME_SLOT_ONE_HOUR,
TIME_SLOT_TWO_MINUTES,
})
@Retention(RetentionPolicy.SOURCE)
private @interface TimeSlotType {
}
private static final int TIME_SLOT_TYPES_COUNT = 4;
static final EventIndex EMPTY = new EventIndex();
private static final List<Function<Long, Range<Long>>> TIME_SLOT_FACTORIES =
Collections.unmodifiableList(
Arrays.asList(
EventIndex::createOneDayLongTimeSlot,
EventIndex::createFourHoursLongTimeSlot,
EventIndex::createOneHourLongTimeSlot,
EventIndex::createTwoMinutesLongTimeSlot
)
);
/** Combines the two {@link EventIndex} objects and returns the combined result. */
static EventIndex combine(EventIndex lhs, EventIndex rhs) {
EventIndex older = lhs.mLastUpdatedTime < rhs.mLastUpdatedTime ? lhs : rhs;
EventIndex younger = lhs.mLastUpdatedTime >= rhs.mLastUpdatedTime ? lhs : rhs;
EventIndex combined = new EventIndex(older);
combined.updateEventBitmaps(younger.mLastUpdatedTime);
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
combined.mEventBitmaps[slotType] |= younger.mEventBitmaps[slotType];
}
return combined;
}
private final long[] mEventBitmaps;
private long mLastUpdatedTime;
private final Object mLock = new Object();
private final Injector mInjector;
EventIndex() {
mInjector = new Injector();
mEventBitmaps = new long[]{0L, 0L, 0L, 0L};
mLastUpdatedTime = mInjector.currentTimeMillis();
}
EventIndex(EventIndex from) {
mInjector = new Injector();
mEventBitmaps = Arrays.copyOf(from.mEventBitmaps, TIME_SLOT_TYPES_COUNT);
mLastUpdatedTime = from.mLastUpdatedTime;
}
@VisibleForTesting
EventIndex(Injector injector) {
mInjector = injector;
mEventBitmaps = new long[]{0L, 0L, 0L, 0L};
mLastUpdatedTime = mInjector.currentTimeMillis();
}
/**
* Gets the most recent active time slot. A time slot is active if there is at least one event
* occurred in that time slot.
*/
@Nullable
public Range<Long> getMostRecentActiveTimeSlot() {
synchronized (mLock) {
for (int slotType = TIME_SLOT_TYPES_COUNT - 1; slotType >= 0; slotType--) {
if (mEventBitmaps[slotType] == 0L) {
continue;
}
Range<Long> lastTimeSlot =
TIME_SLOT_FACTORIES.get(slotType).apply(mLastUpdatedTime);
int numberOfTrailingZeros = Long.numberOfTrailingZeros(mEventBitmaps[slotType]);
long offset = getDuration(lastTimeSlot) * numberOfTrailingZeros;
return Range.create(lastTimeSlot.getLower() - offset,
lastTimeSlot.getUpper() - offset);
}
}
return null;
}
/**
* Gets the active time slots. A time slot is active if there is at least one event occurred
* in that time slot.
*
* @return active time slots in chronological order.
*/
@NonNull
public List<Range<Long>> getActiveTimeSlots() {
List<Range<Long>> activeTimeSlots = new ArrayList<>();
synchronized (mLock) {
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
activeTimeSlots = combineTimeSlotLists(activeTimeSlots,
getActiveTimeSlotsForType(slotType));
}
}
Collections.reverse(activeTimeSlots);
return activeTimeSlots;
}
/** Returns whether this {@link EventIndex} instance is empty. */
public boolean isEmpty() {
synchronized (mLock) {
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
if (mEventBitmaps[slotType] != 0L) {
return false;
}
}
}
return true;
}
/**
* Adds an event to this index with the given event time. Before the new event is recorded, the
* index is updated first with the current timestamp.
*/
void addEvent(long eventTime) {
if (EMPTY == this) {
throw new IllegalStateException("EMPTY instance is immutable");
}
synchronized (mLock) {
long currentTime = mInjector.currentTimeMillis();
updateEventBitmaps(currentTime);
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
int offset = diffTimeSlots(slotType, eventTime, currentTime);
if (offset < LONG_SIZE_BITS) {
mEventBitmaps[slotType] |= (1L << offset);
}
}
}
}
/** Updates to make all bitmaps up to date. */
void update() {
updateEventBitmaps(mInjector.currentTimeMillis());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("EventIndex {");
sb.append("perDayEventBitmap=0b");
sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_DAY]));
sb.append(", perFourHoursEventBitmap=0b");
sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_FOUR_HOURS]));
sb.append(", perHourEventBitmap=0b");
sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_ONE_HOUR]));
sb.append(", perTwoMinutesEventBitmap=0b");
sb.append(Long.toBinaryString(mEventBitmaps[TIME_SLOT_TWO_MINUTES]));
sb.append(", lastUpdatedTime=");
sb.append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mLastUpdatedTime));
sb.append("}");
return sb.toString();
}
/** Shifts the event bitmaps to make them up-to-date. */
private void updateEventBitmaps(long currentTimeMillis) {
for (int slotType = 0; slotType < TIME_SLOT_TYPES_COUNT; slotType++) {
int offset = diffTimeSlots(slotType, mLastUpdatedTime, currentTimeMillis);
if (offset < LONG_SIZE_BITS) {
mEventBitmaps[slotType] <<= offset;
} else {
mEventBitmaps[slotType] = 0L;
}
}
mLastUpdatedTime = currentTimeMillis;
}
private static LocalDateTime toLocalDateTime(long epochMilli) {
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(epochMilli), TimeZone.getDefault().toZoneId());
}
private static long toEpochMilli(LocalDateTime localDateTime) {
return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
private static long getDuration(Range<Long> timeSlot) {
return timeSlot.getUpper() - timeSlot.getLower();
}
/**
* Finds the time slots for the given two timestamps and returns the distance (in the number
* of time slots) between these two time slots.
*/
private static int diffTimeSlots(@TimeSlotType int timeSlotType, long fromTime, long toTime) {
Function<Long, Range<Long>> timeSlotFactory = TIME_SLOT_FACTORIES.get(timeSlotType);
Range<Long> fromSlot = timeSlotFactory.apply(fromTime);
Range<Long> toSlot = timeSlotFactory.apply(toTime);
return (int) ((toSlot.getLower() - fromSlot.getLower()) / getDuration(fromSlot));
}
/**
* Returns the active time slots for a specified type. The returned time slots are in
* reverse-chronological order.
*/
private List<Range<Long>> getActiveTimeSlotsForType(@TimeSlotType int timeSlotType) {
long eventBitmap = mEventBitmaps[timeSlotType];
Range<Long> latestTimeSlot = TIME_SLOT_FACTORIES.get(timeSlotType).apply(mLastUpdatedTime);
long startTime = latestTimeSlot.getLower();
final long duration = getDuration(latestTimeSlot);
List<Range<Long>> timeSlots = new ArrayList<>();
while (eventBitmap != 0) {
int trailingZeros = Long.numberOfTrailingZeros(eventBitmap);
if (trailingZeros > 0) {
startTime -= duration * trailingZeros;
eventBitmap >>>= trailingZeros;
}
if (eventBitmap != 0) {
timeSlots.add(Range.create(startTime, startTime + duration));
startTime -= duration;
eventBitmap >>>= 1;
}
}
return timeSlots;
}
/**
* Combines two lists of time slots into one. If one longer time slot covers one or multiple
* shorter time slots, the smaller time slot(s) will be added to the result and the longer one
* will be dropped. This ensures the returned list does not contain any overlapping time slots.
*/
private static List<Range<Long>> combineTimeSlotLists(List<Range<Long>> longerSlots,
List<Range<Long>> shorterSlots) {
List<Range<Long>> result = new ArrayList<>();
int i = 0;
int j = 0;
while (i < longerSlots.size() && j < shorterSlots.size()) {
Range<Long> longerSlot = longerSlots.get(i);
Range<Long> shorterSlot = shorterSlots.get(j);
if (longerSlot.contains(shorterSlot)) {
result.add(shorterSlot);
i++;
j++;
} else if (longerSlot.getLower() < shorterSlot.getLower()) {
result.add(shorterSlot);
j++;
} else {
result.add(longerSlot);
i++;
}
}
if (i < longerSlots.size()) {
result.addAll(longerSlots.subList(i, longerSlots.size()));
} else if (j < shorterSlots.size()) {
result.addAll(shorterSlots.subList(j, shorterSlots.size()));
}
return result;
}
/**
* Finds and creates the time slot (duration = 1 day) that the given time falls into.
*/
@NonNull
private static Range<Long> createOneDayLongTimeSlot(long time) {
LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.DAYS);
return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusDays(1)));
}
/**
* Finds and creates the time slot (duration = 4 hours) that the given time falls into.
*/
@NonNull
private static Range<Long> createFourHoursLongTimeSlot(long time) {
int hourOfDay = toLocalDateTime(time).getHour();
LocalDateTime beginTime =
toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS).minusHours(hourOfDay % 4);
return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(4)));
}
/**
* Finds and creates the time slot (duration = 1 hour) that the given time falls into.
*/
@NonNull
private static Range<Long> createOneHourLongTimeSlot(long time) {
LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(ChronoUnit.HOURS);
return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusHours(1)));
}
/**
* Finds and creates the time slot (duration = 2 minutes) that the given time falls into.
*/
@NonNull
private static Range<Long> createTwoMinutesLongTimeSlot(long time) {
int minuteOfHour = toLocalDateTime(time).getMinute();
LocalDateTime beginTime = toLocalDateTime(time).truncatedTo(
ChronoUnit.MINUTES).minusMinutes(minuteOfHour % 2);
return Range.create(toEpochMilli(beginTime), toEpochMilli(beginTime.plusMinutes(2)));
}
@VisibleForTesting
static class Injector {
/** This should be the only way to get the current timestamp in {@code EventIndex}. */
long currentTimeMillis() {
return System.currentTimeMillis();
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** A container that holds a list of {@link Event}s in chronological order. */
class EventList {
private final List<Event> mEvents = new ArrayList<>();
/**
* Adds an event to the list unless there is an existing event with the same timestamp and
* type.
*/
void add(@NonNull Event event) {
int index = firstIndexOnOrAfter(event.getTimestamp());
if (index < mEvents.size()
&& mEvents.get(index).getTimestamp() == event.getTimestamp()
&& isDuplicate(event, index)) {
return;
}
mEvents.add(index, event);
}
/**
* Returns a {@link List} of {@link Event}s whose timestamps are between the specified {@code
* fromTimestamp}, inclusive, and {@code toTimestamp} exclusive, and match the specified event
* types.
*
* @return a {@link List} of matched {@link Event}s in chronological order.
*/
@NonNull
List<Event> queryEvents(@NonNull Set<Integer> eventTypes, long fromTimestamp,
long toTimestamp) {
int fromIndex = firstIndexOnOrAfter(fromTimestamp);
if (fromIndex == mEvents.size()) {
return new ArrayList<>();
}
int toIndex = firstIndexOnOrAfter(toTimestamp);
if (toIndex < fromIndex) {
return new ArrayList<>();
}
List<Event> result = new ArrayList<>();
for (int i = fromIndex; i < toIndex; i++) {
Event e = mEvents.get(i);
if (eventTypes.contains(e.getType())) {
result.add(e);
}
}
return result;
}
/** Returns the first index whose timestamp is greater or equal to the provided timestamp. */
private int firstIndexOnOrAfter(long timestamp) {
int result = mEvents.size();
int low = 0;
int high = mEvents.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
if (mEvents.get(mid).getTimestamp() >= timestamp) {
high = mid - 1;
result = mid;
} else {
low = mid + 1;
}
}
return result;
}
/**
* Checks whether the {@link Event} is duplicate with one of the existing events. The checking
* starts from the {@code startIndex}.
*/
private boolean isDuplicate(Event event, int startIndex) {
int size = mEvents.size();
int index = startIndex;
while (index < size && mEvents.get(index).getTimestamp() <= event.getTimestamp()) {
if (mEvents.get(index++).getType() == event.getType()) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.util.ArrayMap;
import java.util.Map;
/** The store that stores and accesses the events data for a package. */
class EventStore {
private final EventHistoryImpl mPackageEventHistory = new EventHistoryImpl();
// Shortcut ID -> Event History
private final Map<String, EventHistoryImpl> mShortcutEventHistoryMap = new ArrayMap<>();
// Locus ID -> Event History
private final Map<LocusId, EventHistoryImpl> mLocusEventHistoryMap = new ArrayMap<>();
// Phone Number -> Event History
private final Map<String, EventHistoryImpl> mCallEventHistoryMap = new ArrayMap<>();
// Phone Number -> Event History
private final Map<String, EventHistoryImpl> mSmsEventHistoryMap = new ArrayMap<>();
/** Gets the package level {@link EventHistory}. */
@NonNull
EventHistory getPackageEventHistory() {
return mPackageEventHistory;
}
/** Gets the {@link EventHistory} for the specified {@code shortcutId} if exists. */
@Nullable
EventHistory getShortcutEventHistory(String shortcutId) {
return mShortcutEventHistoryMap.get(shortcutId);
}
/** Gets the {@link EventHistory} for the specified {@code locusId} if exists. */
@Nullable
EventHistory getLocusEventHistory(LocusId locusId) {
return mLocusEventHistoryMap.get(locusId);
}
/** Gets the phone call {@link EventHistory} for the specified {@code phoneNumber} if exists. */
@Nullable
EventHistory getCallEventHistory(String phoneNumber) {
return mCallEventHistoryMap.get(phoneNumber);
}
/** Gets the SMS {@link EventHistory} for the specified {@code phoneNumber} if exists. */
@Nullable
EventHistory getSmsEventHistory(String phoneNumber) {
return mSmsEventHistoryMap.get(phoneNumber);
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code shortcutId} or creates a new
* instance and put it into the store if not exists. The caller needs to verify if a
* conversation with this shortcut ID exists before calling this method.
*/
@NonNull
EventHistoryImpl getOrCreateShortcutEventHistory(String shortcutId) {
return mShortcutEventHistoryMap.computeIfAbsent(shortcutId, key -> new EventHistoryImpl());
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code locusId} or creates a new
* instance and put it into the store if not exists. The caller needs to ensure a conversation
* with this locus ID exists before calling this method.
*/
@NonNull
EventHistoryImpl getOrCreateLocusEventHistory(LocusId locusId) {
return mLocusEventHistoryMap.computeIfAbsent(locusId, key -> new EventHistoryImpl());
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code phoneNumber} for call events
* or creates a new instance and put it into the store if not exists. The caller needs to ensure
* a conversation with this phone number exists and this package is the default dialer
* before calling this method.
*/
@NonNull
EventHistoryImpl getOrCreateCallEventHistory(String phoneNumber) {
return mCallEventHistoryMap.computeIfAbsent(phoneNumber, key -> new EventHistoryImpl());
}
/**
* Gets the {@link EventHistoryImpl} for the specified {@code phoneNumber} for SMS events
* or creates a new instance and put it into the store if not exists. The caller needs to ensure
* a conversation with this phone number exists and this package is the default SMS app
* before calling this method.
*/
@NonNull
EventHistoryImpl getOrCreateSmsEventHistory(String phoneNumber) {
return mSmsEventHistoryMap.computeIfAbsent(phoneNumber, key -> new EventHistoryImpl());
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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.NonNull;
import android.annotation.UserIdInt;
import android.content.LocusId;
import android.text.TextUtils;
import java.util.function.Consumer;
/** The data associated with a package. */
public class PackageData {
@NonNull
private final String mPackageName;
private final @UserIdInt int mUserId;
@NonNull
private final ConversationStore mConversationStore;
@NonNull
private final EventStore mEventStore;
private boolean mIsDefaultDialer;
private boolean mIsDefaultSmsApp;
PackageData(@NonNull String packageName, @UserIdInt int userId) {
mPackageName = packageName;
mUserId = userId;
mConversationStore = new ConversationStore();
mEventStore = new EventStore();
}
@NonNull
public String getPackageName() {
return mPackageName;
}
public @UserIdInt int getUserId() {
return mUserId;
}
/** Iterates over all the conversations in this package. */
public void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
mConversationStore.forAllConversations(consumer);
}
@NonNull
public EventHistory getPackageLevelEventHistory() {
return getEventStore().getPackageEventHistory();
}
/**
* Gets the combined {@link EventHistory} for a given shortcut ID. This returned {@link
* EventHistory} has events of all types, no matter whether they're annotated with shortcut ID,
* Locus ID, or phone number etc.
*/
@NonNull
public EventHistory getEventHistory(@NonNull String shortcutId) {
AggregateEventHistoryImpl result = new AggregateEventHistoryImpl();
ConversationInfo conversationInfo = mConversationStore.getConversation(shortcutId);
if (conversationInfo == null) {
return result;
}
EventHistory shortcutEventHistory = getEventStore().getShortcutEventHistory(shortcutId);
if (shortcutEventHistory != null) {
result.addEventHistory(shortcutEventHistory);
}
LocusId locusId = conversationInfo.getLocusId();
if (locusId != null) {
EventHistory locusEventHistory = getEventStore().getLocusEventHistory(locusId);
if (locusEventHistory != null) {
result.addEventHistory(locusEventHistory);
}
}
String phoneNumber = conversationInfo.getContactPhoneNumber();
if (TextUtils.isEmpty(phoneNumber)) {
return result;
}
if (isDefaultDialer()) {
EventHistory callEventHistory = getEventStore().getCallEventHistory(phoneNumber);
if (callEventHistory != null) {
result.addEventHistory(callEventHistory);
}
}
if (isDefaultSmsApp()) {
EventHistory smsEventHistory = getEventStore().getSmsEventHistory(phoneNumber);
if (smsEventHistory != null) {
result.addEventHistory(smsEventHistory);
}
}
return result;
}
public boolean isDefaultDialer() {
return mIsDefaultDialer;
}
public boolean isDefaultSmsApp() {
return mIsDefaultSmsApp;
}
@NonNull
ConversationStore getConversationStore() {
return mConversationStore;
}
@NonNull
EventStore getEventStore() {
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,103 @@
/*
* 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.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.text.TextUtils;
import android.util.ArrayMap;
import java.util.Map;
import java.util.function.Consumer;
/** The data associated with a user profile. */
class UserData {
private final @UserIdInt int mUserId;
private boolean mIsUnlocked;
private Map<String, PackageData> mPackageDataMap = new ArrayMap<>();
UserData(@UserIdInt int userId) {
mUserId = userId;
}
@UserIdInt int getUserId() {
return mUserId;
}
void forAllPackages(@NonNull Consumer<PackageData> consumer) {
for (PackageData packageData : mPackageDataMap.values()) {
consumer.accept(packageData);
}
}
void setUserUnlocked() {
mIsUnlocked = true;
}
void setUserStopped() {
mIsUnlocked = false;
}
boolean isUnlocked() {
return mIsUnlocked;
}
/**
* Gets the {@link PackageData} for the specified {@code packageName} if exists; otherwise
* creates a new instance and returns it.
*/
@NonNull
PackageData getOrCreatePackageData(String packageName) {
return mPackageDataMap.computeIfAbsent(
packageName, key -> new PackageData(packageName, mUserId));
}
/**
* Gets the {@link PackageData} for the specified {@code packageName} if exists; otherwise
* returns {@code null}.
*/
@Nullable
PackageData getPackageData(@NonNull String packageName) {
return mPackageDataMap.get(packageName);
}
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);
}
}
}
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);
}
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.prediction;
import android.annotation.NonNull;
import com.android.server.people.data.ConversationInfo;
import com.android.server.people.data.EventHistory;
/** The conversation data which is used for scoring and then ranking the conversations. */
class ConversationData {
private final String mPackageName;
private final int mUserId;
private final ConversationInfo mConversationInfo;
private final EventHistory mEventHistory;
ConversationData(@NonNull String packageName, int userId,
@NonNull ConversationInfo conversationInfo, @NonNull EventHistory eventHistory) {
mPackageName = packageName;
mUserId = userId;
mConversationInfo = conversationInfo;
mEventHistory = eventHistory;
}
String getPackageName() {
return mPackageName;
}
int getUserId() {
return mUserId;
}
ConversationInfo getConversationInfo() {
return mConversationInfo;
}
EventHistory getEventHistory() {
return mEventHistory;
}
}

View File

@@ -17,12 +17,20 @@
package com.android.server.people.prediction;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager.ShareShortcutInfo;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ChooserActivity;
import com.android.server.people.data.DataManager;
import com.android.server.people.data.EventHistory;
import java.util.ArrayList;
import java.util.List;
@@ -35,15 +43,28 @@ import java.util.function.Consumer;
*/
public class ConversationPredictor {
private static final String UI_SURFACE_SHARE = "share";
private final AppPredictionContext mPredictionContext;
private final Consumer<List<AppTarget>> mUpdatePredictionsMethod;
private final DataManager mDataManager;
private final ExecutorService mCallbackExecutor;
@Nullable
private final IntentFilter mIntentFilter;
public ConversationPredictor(AppPredictionContext predictionContext,
Consumer<List<AppTarget>> updatePredictionsMethod) {
public ConversationPredictor(@NonNull AppPredictionContext predictionContext,
@NonNull Consumer<List<AppTarget>> updatePredictionsMethod,
@NonNull DataManager dataManager) {
mPredictionContext = predictionContext;
mUpdatePredictionsMethod = updatePredictionsMethod;
mDataManager = dataManager;
mCallbackExecutor = Executors.newSingleThreadExecutor();
if (UI_SURFACE_SHARE.equals(mPredictionContext.getUiSurface())) {
mIntentFilter = mPredictionContext.getExtras().getParcelable(
ChooserActivity.APP_PREDICTION_INTENT_FILTER_KEY);
} else {
mIntentFilter = null;
}
}
/**
@@ -51,14 +72,14 @@ public class ConversationPredictor {
*/
@MainThread
public void onAppTargetEvent(AppTargetEvent event) {
mDataManager.reportAppTargetEvent(event, mIntentFilter);
}
/**
* Called by the client app to indicate a particular location has been shown to the user.
*/
@MainThread
public void onLaunchLocationShown(String launchLocation, List<AppTargetId> targetIds) {
}
public void onLaunchLocationShown(String launchLocation, List<AppTargetId> targetIds) {}
/**
* Called by the client app to request sorting of the provided targets based on the prediction
@@ -74,8 +95,44 @@ public class ConversationPredictor {
*/
@MainThread
public void onRequestPredictionUpdate() {
List<AppTarget> targets = new ArrayList<>();
mCallbackExecutor.execute(() -> mUpdatePredictionsMethod.accept(targets));
// TODO: Re-route the call to different ranking classes for different surfaces.
mCallbackExecutor.execute(() -> {
List<AppTarget> targets = new ArrayList<>();
if (mIntentFilter != null) {
List<ShareShortcutInfo> shareShortcuts =
mDataManager.getConversationShareTargets(mIntentFilter);
for (ShareShortcutInfo shareShortcut : shareShortcuts) {
ShortcutInfo shortcutInfo = shareShortcut.getShortcutInfo();
AppTargetId appTargetId = new AppTargetId(shortcutInfo.getId());
String shareTargetClass = shareShortcut.getTargetComponent().getClassName();
targets.add(new AppTarget.Builder(appTargetId, shortcutInfo)
.setClassName(shareTargetClass)
.build());
}
} else {
List<ConversationData> conversationDataList = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
packageData.forAllConversations(conversationInfo -> {
EventHistory eventHistory = packageData.getEventHistory(
conversationInfo.getShortcutId());
ConversationData conversationData = new ConversationData(
packageData.getPackageName(), packageData.getUserId(),
conversationInfo, eventHistory);
conversationDataList.add(conversationData);
}));
for (ConversationData conversationData : conversationDataList) {
String shortcutId = conversationData.getConversationInfo().getShortcutId();
ShortcutInfo shortcut = mDataManager.getShortcut(
conversationData.getPackageName(), conversationData.getUserId(),
shortcutId);
if (shortcut != null) {
AppTargetId appTargetId = new AppTargetId(shortcut.getId());
targets.add(new AppTarget.Builder(appTargetId, shortcut).build());
}
}
}
mUpdatePredictionsMethod.accept(targets);
});
}
@VisibleForTesting

View File

@@ -0,0 +1,131 @@
/*
* 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 com.android.server.people.data.TestUtils.timestamp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@RunWith(JUnit4.class)
public final class AggregateEventHistoryImplTest {
private static final long CURRENT_TIMESTAMP = timestamp("01-30 18:50");
private static final Event E1 = new Event(timestamp("01-06 05:26"),
Event.TYPE_NOTIFICATION_OPENED);
private static final Event E2 = new Event(timestamp("01-27 18:41"),
Event.TYPE_NOTIFICATION_OPENED);
private static final Event E3 = new Event(timestamp("01-30 03:06"),
Event.TYPE_SMS_OUTGOING);
private static final Event E4 = new Event(timestamp("01-30 18:14"),
Event.TYPE_SMS_INCOMING);
private EventHistoryImpl mEventHistory1;
private EventHistoryImpl mEventHistory2;
private AggregateEventHistoryImpl mAggEventHistory;
private EventIndex.Injector mInjector = new EventIndex.Injector() {
@Override
long currentTimeMillis() {
return CURRENT_TIMESTAMP;
}
};
@Before
public void setUp() {
mAggEventHistory = new AggregateEventHistoryImpl();
EventHistoryImpl.Injector injector = new EventHistoryImplInjector();
mEventHistory1 = new EventHistoryImpl(injector);
mEventHistory1.addEvent(E1);
mEventHistory1.addEvent(E2);
mEventHistory2 = new EventHistoryImpl(injector);
mEventHistory2.addEvent(E3);
mEventHistory2.addEvent(E4);
}
@Test
public void testEmptyAggregateEventHistory() {
assertTrue(mAggEventHistory.getEventIndex(Event.TYPE_SHORTCUT_INVOCATION).isEmpty());
assertTrue(mAggEventHistory.getEventIndex(Event.ALL_EVENT_TYPES).isEmpty());
assertTrue(mAggEventHistory.queryEvents(
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE).isEmpty());
}
@Test
public void testQueryEventIndexForSingleEventType() {
mAggEventHistory.addEventHistory(mEventHistory1);
mAggEventHistory.addEventHistory(mEventHistory2);
EventIndex eventIndex;
eventIndex = mAggEventHistory.getEventIndex(Event.TYPE_NOTIFICATION_OPENED);
assertEquals(2, eventIndex.getActiveTimeSlots().size());
eventIndex = mAggEventHistory.getEventIndex(Event.TYPE_SMS_OUTGOING);
assertEquals(1, eventIndex.getActiveTimeSlots().size());
eventIndex = mAggEventHistory.getEventIndex(Event.TYPE_SHORTCUT_INVOCATION);
assertTrue(eventIndex.isEmpty());
}
@Test
public void testQueryEventIndexForMultipleEventTypes() {
mAggEventHistory.addEventHistory(mEventHistory1);
mAggEventHistory.addEventHistory(mEventHistory2);
EventIndex eventIndex;
eventIndex = mAggEventHistory.getEventIndex(Event.SMS_EVENT_TYPES);
assertEquals(2, eventIndex.getActiveTimeSlots().size());
eventIndex = mAggEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
assertEquals(4, eventIndex.getActiveTimeSlots().size());
}
@Test
public void testQueryEvents() {
mAggEventHistory.addEventHistory(mEventHistory1);
mAggEventHistory.addEventHistory(mEventHistory2);
List<Event> events;
events = mAggEventHistory.queryEvents(Event.NOTIFICATION_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(2, events.size());
events = mAggEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(4, events.size());
}
private class EventHistoryImplInjector extends EventHistoryImpl.Injector {
EventIndex createEventIndex() {
return new EventIndex(mInjector);
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* 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.assertNotNull;
import static org.junit.Assert.assertNull;
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.ContactsContract;
import android.provider.ContactsContract.Contacts;
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.Map;
@RunWith(JUnit4.class)
public final class ContactsQueryHelperTest {
private static final String CONTACT_LOOKUP_KEY = "123";
private static final String PHONE_NUMBER = "+1234567890";
private static final String[] CONTACTS_COLUMNS = new String[] {
Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER,
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
private static final String[] CONTACTS_LOOKUP_COLUMNS = new String[] {
Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER };
private static final String[] PHONE_COLUMNS = new String[] {
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER };
@Mock
private MockContext mContext;
private MatrixCursor mContactsCursor;
private MatrixCursor mContactsLookupCursor;
private MatrixCursor mPhoneCursor;
private ContactsQueryHelper mHelper;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContactsCursor = new MatrixCursor(CONTACTS_COLUMNS);
mContactsLookupCursor = new MatrixCursor(CONTACTS_LOOKUP_COLUMNS);
mPhoneCursor = new MatrixCursor(PHONE_COLUMNS);
MockContentResolver contentResolver = new MockContentResolver();
ContactsContentProvider contentProvider = new ContactsContentProvider();
contentProvider.registerCursor(Contacts.CONTENT_URI, mContactsCursor);
contentProvider.registerCursor(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI, mContactsLookupCursor);
contentProvider.registerCursor(
ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, mContactsLookupCursor);
contentProvider.registerCursor(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI, mPhoneCursor);
contentResolver.addProvider(ContactsContract.AUTHORITY, contentProvider);
when(mContext.getContentResolver()).thenReturn(contentResolver);
mHelper = new ContactsQueryHelper(mContext);
}
@Test
public void testQueryWithUri() {
mContactsCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1,
/* lastUpdatedTimestamp= */ 100L });
mPhoneCursor.addRow(new String[] { PHONE_NUMBER });
Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
assertTrue(mHelper.query(contactUri.toString()));
assertNotNull(mHelper.getContactUri());
assertEquals(PHONE_NUMBER, mHelper.getPhoneNumber());
assertEquals(100L, mHelper.getLastUpdatedTimestamp());
assertTrue(mHelper.isStarred());
}
@Test
public void testQueryWithUriNotStarredNoPhoneNumber() {
mContactsCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 0, /* hasPhoneNumber= */ 0,
/* lastUpdatedTimestamp= */ 100L });
Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
assertTrue(mHelper.query(contactUri.toString()));
assertNotNull(mHelper.getContactUri());
assertNull(mHelper.getPhoneNumber());
assertFalse(mHelper.isStarred());
assertEquals(100L, mHelper.getLastUpdatedTimestamp());
}
@Test
public void testQueryWithUriNotFound() {
Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
assertFalse(mHelper.query(contactUri.toString()));
}
@Test
public void testQueryWithPhoneNumber() {
mContactsLookupCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1 });
mPhoneCursor.addRow(new String[] { PHONE_NUMBER });
String contactUri = "tel:" + PHONE_NUMBER;
assertTrue(mHelper.query(contactUri));
assertNotNull(mHelper.getContactUri());
assertEquals(PHONE_NUMBER, mHelper.getPhoneNumber());
assertTrue(mHelper.isStarred());
}
@Test
public void testQueryWithEmail() {
mContactsLookupCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 0 });
String contactUri = "mailto:test@gmail.com";
assertTrue(mHelper.query(contactUri));
assertNotNull(mHelper.getContactUri());
assertNull(mHelper.getPhoneNumber());
assertTrue(mHelper.isStarred());
}
@Test
public void testQueryUpdatedContactSinceTime() {
mContactsCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 0,
/* lastUpdatedTimestamp= */ 100L });
assertTrue(mHelper.querySince(50L));
assertNotNull(mHelper.getContactUri());
assertNull(mHelper.getPhoneNumber());
assertTrue(mHelper.isStarred());
assertEquals(100L, mHelper.getLastUpdatedTimestamp());
}
@Test
public void testQueryWithUnsupportedScheme() {
mContactsLookupCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 1 });
mPhoneCursor.addRow(new String[] { PHONE_NUMBER });
String contactUri = "unknown:test";
assertFalse(mHelper.query(contactUri));
}
private class ContactsContentProvider extends MockContentProvider {
private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>();
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) {
if (uri.isPathPrefixMatch(prefixUri)) {
return mUriPrefixToCursorMap.get(prefixUri);
}
}
return mUriPrefixToCursorMap.get(uri);
}
private void registerCursor(Uri uriPrefix, Cursor cursor) {
mUriPrefixToCursorMap.put(uriPrefix, cursor);
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* 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.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class ConversationInfoTest {
private static final String SHORTCUT_ID = "abc";
private static final LocusId LOCUS_ID = new LocusId("def");
private static final Uri CONTACT_URI = Uri.parse("tel:+1234567890");
private static final String PHONE_NUMBER = "+1234567890";
private static final String NOTIFICATION_CHANNEL_ID = "test : abc";
@Test
public void testBuild() {
ConversationInfo conversationInfo = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(LOCUS_ID)
.setContactUri(CONTACT_URI)
.setContactPhoneNumber(PHONE_NUMBER)
.setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.setVip(true)
.setNotificationSilenced(true)
.setBubbled(true)
.setDemoted(true)
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
.build();
assertEquals(SHORTCUT_ID, conversationInfo.getShortcutId());
assertEquals(LOCUS_ID, conversationInfo.getLocusId());
assertEquals(CONTACT_URI, conversationInfo.getContactUri());
assertEquals(PHONE_NUMBER, conversationInfo.getContactPhoneNumber());
assertEquals(NOTIFICATION_CHANNEL_ID, conversationInfo.getNotificationChannelId());
assertTrue(conversationInfo.isShortcutLongLived());
assertTrue(conversationInfo.isVip());
assertTrue(conversationInfo.isNotificationSilenced());
assertTrue(conversationInfo.isBubbled());
assertTrue(conversationInfo.isDemoted());
assertTrue(conversationInfo.isPersonImportant());
assertTrue(conversationInfo.isPersonBot());
assertTrue(conversationInfo.isContactStarred());
}
@Test
public void testBuildEmpty() {
ConversationInfo conversationInfo = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.build();
assertEquals(SHORTCUT_ID, conversationInfo.getShortcutId());
assertNull(conversationInfo.getLocusId());
assertNull(conversationInfo.getContactUri());
assertNull(conversationInfo.getContactPhoneNumber());
assertNull(conversationInfo.getNotificationChannelId());
assertFalse(conversationInfo.isShortcutLongLived());
assertFalse(conversationInfo.isVip());
assertFalse(conversationInfo.isNotificationSilenced());
assertFalse(conversationInfo.isBubbled());
assertFalse(conversationInfo.isDemoted());
assertFalse(conversationInfo.isPersonImportant());
assertFalse(conversationInfo.isPersonBot());
assertFalse(conversationInfo.isContactStarred());
}
@Test
public void testBuildFromAnotherConversationInfo() {
ConversationInfo source = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(LOCUS_ID)
.setContactUri(CONTACT_URI)
.setContactPhoneNumber(PHONE_NUMBER)
.setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.setVip(true)
.setNotificationSilenced(true)
.setBubbled(true)
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
.build();
ConversationInfo destination = new ConversationInfo.Builder(source)
.setVip(false)
.setContactStarred(false)
.build();
assertEquals(SHORTCUT_ID, destination.getShortcutId());
assertEquals(LOCUS_ID, destination.getLocusId());
assertEquals(CONTACT_URI, destination.getContactUri());
assertEquals(PHONE_NUMBER, destination.getContactPhoneNumber());
assertEquals(NOTIFICATION_CHANNEL_ID, destination.getNotificationChannelId());
assertTrue(destination.isShortcutLongLived());
assertFalse(destination.isVip());
assertTrue(destination.isNotificationSilenced());
assertTrue(destination.isBubbled());
assertTrue(destination.isPersonImportant());
assertTrue(destination.isPersonBot());
assertFalse(destination.isContactStarred());
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
import android.util.ArraySet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Set;
@RunWith(JUnit4.class)
public final class ConversationStoreTest {
private static final String SHORTCUT_ID = "abc";
private static final LocusId LOCUS_ID = new LocusId("def");
private static final Uri CONTACT_URI = Uri.parse("tel:+1234567890");
private static final String PHONE_NUMBER = "+1234567890";
private ConversationStore mConversationStore;
@Before
public void setUp() {
mConversationStore = new ConversationStore();
}
@Test
public void testAddConversation() {
mConversationStore.addOrUpdate(buildConversationInfo(SHORTCUT_ID));
ConversationInfo out = mConversationStore.getConversation(SHORTCUT_ID);
assertNotNull(out);
assertEquals(SHORTCUT_ID, out.getShortcutId());
}
@Test
public void testUpdateConversation() {
ConversationInfo original =
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
mConversationStore.addOrUpdate(original);
assertEquals(LOCUS_ID, mConversationStore.getConversation(SHORTCUT_ID).getLocusId());
LocusId newLocusId = new LocusId("ghi");
ConversationInfo update = buildConversationInfo(
SHORTCUT_ID, newLocusId, CONTACT_URI, PHONE_NUMBER);
mConversationStore.addOrUpdate(update);
assertEquals(newLocusId, mConversationStore.getConversation(SHORTCUT_ID).getLocusId());
}
@Test
public void testDeleteConversation() {
mConversationStore.addOrUpdate(buildConversationInfo(SHORTCUT_ID));
assertNotNull(mConversationStore.getConversation(SHORTCUT_ID));
mConversationStore.deleteConversation(SHORTCUT_ID);
assertNull(mConversationStore.getConversation(SHORTCUT_ID));
}
@Test
public void testForAllConversations() {
mConversationStore.addOrUpdate(buildConversationInfo("a"));
mConversationStore.addOrUpdate(buildConversationInfo("b"));
mConversationStore.addOrUpdate(buildConversationInfo("c"));
Set<String> shortcutIds = new ArraySet<>();
mConversationStore.forAllConversations(
conversationInfo -> shortcutIds.add(conversationInfo.getShortcutId()));
assertTrue(shortcutIds.contains("a"));
assertTrue(shortcutIds.contains("b"));
assertTrue(shortcutIds.contains("c"));
}
@Test
public void testGetConversationByLocusId() {
ConversationInfo in =
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
mConversationStore.addOrUpdate(in);
ConversationInfo out = mConversationStore.getConversationByLocusId(LOCUS_ID);
assertNotNull(out);
assertEquals(SHORTCUT_ID, out.getShortcutId());
mConversationStore.deleteConversation(SHORTCUT_ID);
assertNull(mConversationStore.getConversationByLocusId(LOCUS_ID));
}
@Test
public void testGetConversationByContactUri() {
ConversationInfo in =
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
mConversationStore.addOrUpdate(in);
ConversationInfo out = mConversationStore.getConversationByContactUri(CONTACT_URI);
assertNotNull(out);
assertEquals(SHORTCUT_ID, out.getShortcutId());
mConversationStore.deleteConversation(SHORTCUT_ID);
assertNull(mConversationStore.getConversationByContactUri(CONTACT_URI));
}
@Test
public void testGetConversationByPhoneNumber() {
ConversationInfo in =
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
mConversationStore.addOrUpdate(in);
ConversationInfo out = mConversationStore.getConversationByPhoneNumber(PHONE_NUMBER);
assertNotNull(out);
assertEquals(SHORTCUT_ID, out.getShortcutId());
mConversationStore.deleteConversation(SHORTCUT_ID);
assertNull(mConversationStore.getConversationByPhoneNumber(PHONE_NUMBER));
}
private static ConversationInfo buildConversationInfo(String shortcutId) {
return buildConversationInfo(shortcutId, null, null, null);
}
private static ConversationInfo buildConversationInfo(
String shortcutId, LocusId locusId, Uri contactUri, String phoneNumber) {
return new ConversationInfo.Builder()
.setShortcutId(shortcutId)
.setLocusId(locusId)
.setContactUri(contactUri)
.setContactPhoneNumber(phoneNumber)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.setVip(true)
.setBubbled(true)
.build();
}
}

View File

@@ -0,0 +1,451 @@
/*
* 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 android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.Person;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutManager.ShareShortcutInfo;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.telephony.TelephonyManager;
import android.util.Range;
import com.android.internal.app.ChooserActivity;
import com.android.server.LocalServices;
import org.junit.After;
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.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@RunWith(JUnit4.class)
public final class DataManagerTest {
private static final int USER_ID_PRIMARY = 0;
private static final int USER_ID_PRIMARY_MANAGED = 10;
private static final int USER_ID_SECONDARY = 11;
private static final String TEST_PKG_NAME = "pkg";
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";
@Mock private Context mContext;
@Mock private ShortcutServiceInternal mShortcutServiceInternal;
@Mock private UsageStatsManagerInternal mUsageStatsManagerInternal;
@Mock private ShortcutManager mShortcutManager;
@Mock private UserManager mUserManager;
@Mock private TelephonyManager mTelephonyManager;
@Mock private ContentResolver mContentResolver;
@Mock private ScheduledExecutorService mExecutorService;
@Mock private ScheduledFuture mScheduledFuture;
@Mock private StatusBarNotification mStatusBarNotification;
@Mock private Notification mNotification;
private DataManager mDataManager;
private int mCallingUserId;
private TestInjector mInjector;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
addLocalServiceMock(ShortcutServiceInternal.class, mShortcutServiceInternal);
addLocalServiceMock(UsageStatsManagerInternal.class, mUsageStatsManagerInternal);
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
when(mContext.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(mShortcutManager);
when(mContext.getSystemServiceName(ShortcutManager.class)).thenReturn(
Context.SHORTCUT_SERVICE);
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
when(mContext.getSystemServiceName(UserManager.class)).thenReturn(
Context.USER_SERVICE);
when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
when(mExecutorService.scheduleAtFixedRate(any(Runnable.class), anyLong(), anyLong(), any(
TimeUnit.class))).thenReturn(mScheduledFuture);
when(mUserManager.getEnabledProfiles(USER_ID_PRIMARY))
.thenReturn(Arrays.asList(
buildUserInfo(USER_ID_PRIMARY),
buildUserInfo(USER_ID_PRIMARY_MANAGED)));
when(mUserManager.getEnabledProfiles(USER_ID_SECONDARY))
.thenReturn(Collections.singletonList(buildUserInfo(USER_ID_SECONDARY)));
when(mContext.getContentResolver()).thenReturn(mContentResolver);
when(mStatusBarNotification.getNotification()).thenReturn(mNotification);
when(mStatusBarNotification.getPackageName()).thenReturn(TEST_PKG_NAME);
when(mStatusBarNotification.getUser()).thenReturn(UserHandle.of(USER_ID_PRIMARY));
when(mNotification.getShortcutId()).thenReturn(TEST_SHORTCUT_ID);
mCallingUserId = USER_ID_PRIMARY;
mInjector = new TestInjector();
mDataManager = new DataManager(mContext, mInjector);
mDataManager.initialize();
}
@After
public void tearDown() {
LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
}
@Test
public void testAccessConversationFromTheSameProfileGroup() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
mDataManager.onUserUnlocked(USER_ID_PRIMARY_MANAGED);
mDataManager.onUserUnlocked(USER_ID_SECONDARY);
mDataManager.onShortcutAddedOrUpdated(
buildShortcutInfo("pkg_1", USER_ID_PRIMARY, "sc_1",
buildPerson(true, false)));
mDataManager.onShortcutAddedOrUpdated(
buildShortcutInfo("pkg_2", USER_ID_PRIMARY_MANAGED, "sc_2",
buildPerson(false, true)));
mDataManager.onShortcutAddedOrUpdated(
buildShortcutInfo("pkg_3", USER_ID_SECONDARY, "sc_3", buildPerson()));
List<ConversationInfo> conversations = new ArrayList<>();
mDataManager.forAllPackages(
packageData -> packageData.forAllConversations(conversations::add));
// USER_ID_SECONDARY is not in the same profile group as USER_ID_PRIMARY.
assertEquals(2, conversations.size());
assertEquals("sc_1", conversations.get(0).getShortcutId());
assertTrue(conversations.get(0).isPersonImportant());
assertFalse(conversations.get(0).isPersonBot());
assertFalse(conversations.get(0).isContactStarred());
assertEquals(PHONE_NUMBER, conversations.get(0).getContactPhoneNumber());
assertEquals("sc_2", conversations.get(1).getShortcutId());
assertFalse(conversations.get(1).isPersonImportant());
assertTrue(conversations.get(1).isPersonBot());
assertFalse(conversations.get(0).isContactStarred());
assertEquals(PHONE_NUMBER, conversations.get(0).getContactPhoneNumber());
}
@Test
public void testAccessConversationForUnlockedUsersOnly() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
mDataManager.onShortcutAddedOrUpdated(
buildShortcutInfo("pkg_1", USER_ID_PRIMARY, "sc_1", buildPerson()));
mDataManager.onShortcutAddedOrUpdated(
buildShortcutInfo("pkg_2", USER_ID_PRIMARY_MANAGED, "sc_2", buildPerson()));
List<ConversationInfo> conversations = new ArrayList<>();
mDataManager.forAllPackages(
packageData -> packageData.forAllConversations(conversations::add));
// USER_ID_PRIMARY_MANAGED is not locked, so only USER_ID_PRIMARY's conversation is stored.
assertEquals(1, conversations.size());
assertEquals("sc_1", conversations.get(0).getShortcutId());
mDataManager.onUserStopped(USER_ID_PRIMARY);
conversations.clear();
mDataManager.forAllPackages(
packageData -> packageData.forAllConversations(conversations::add));
assertTrue(conversations.isEmpty());
}
@Test
public void testGetShortcut() {
mDataManager.getShortcut(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID);
verify(mShortcutServiceInternal).getShortcuts(anyInt(), anyString(), anyLong(),
eq(TEST_PKG_NAME), eq(Collections.singletonList(TEST_SHORTCUT_ID)),
eq(null), anyInt(), eq(USER_ID_PRIMARY), anyInt(), anyInt());
}
@Test
public void testGetShareTargets() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut1 =
buildShortcutInfo("pkg_1", USER_ID_PRIMARY, "sc_1", buildPerson());
ShareShortcutInfo shareShortcut1 =
new ShareShortcutInfo(shortcut1, new ComponentName("pkg_1", "activity"));
ShortcutInfo shortcut2 =
buildShortcutInfo("pkg_2", USER_ID_PRIMARY, "sc_2", buildPerson());
ShareShortcutInfo shareShortcut2 =
new ShareShortcutInfo(shortcut2, new ComponentName("pkg_2", "activity"));
mDataManager.onShortcutAddedOrUpdated(shortcut2);
when(mShortcutManager.getShareTargets(any(IntentFilter.class)))
.thenReturn(Arrays.asList(shareShortcut1, shareShortcut2));
List<ShareShortcutInfo> shareShortcuts =
mDataManager.getConversationShareTargets(new IntentFilter());
// Only "sc_2" is stored as a conversation.
assertEquals(1, shareShortcuts.size());
assertEquals("sc_2", shareShortcuts.get(0).getShortcutInfo().getId());
}
@Test
public void testReportAppTargetEvent() throws IntentFilter.MalformedMimeTypeException {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
AppTarget appTarget = new AppTarget.Builder(new AppTargetId(TEST_SHORTCUT_ID), shortcut)
.build();
AppTargetEvent appTargetEvent =
new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
.setLaunchLocation(ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE)
.build();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND, "image/jpg");
mDataManager.reportAppTargetEvent(appTargetEvent, intentFilter);
List<Range<Long>> activeShareTimeSlots = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
activeShareTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.TYPE_SHARE_IMAGE)
.getActiveTimeSlots()));
assertEquals(1, activeShareTimeSlots.size());
}
@Test
public void testContactsChanged() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
final String newPhoneNumber = "+1000000000";
mInjector.mContactsQueryHelper.mIsStarred = true;
mInjector.mContactsQueryHelper.mPhoneNumber = newPhoneNumber;
ContentObserver contentObserver = mDataManager.getContactsContentObserverForTesting(
USER_ID_PRIMARY);
contentObserver.onChange(false, ContactsContract.Contacts.CONTENT_URI, USER_ID_PRIMARY);
List<ConversationInfo> conversations = new ArrayList<>();
mDataManager.forAllPackages(
packageData -> packageData.forAllConversations(conversations::add));
assertEquals(1, conversations.size());
assertEquals(TEST_SHORTCUT_ID, conversations.get(0).getShortcutId());
assertTrue(conversations.get(0).isContactStarred());
assertEquals(newPhoneNumber, conversations.get(0).getContactPhoneNumber());
}
@Test
public void testNotificationListener() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
NotificationListenerService listenerService =
mDataManager.getNotificationListenerServiceForTesting(USER_ID_PRIMARY);
listenerService.onNotificationRemoved(mStatusBarNotification, null,
NotificationListenerService.REASON_CLICK);
List<Range<Long>> activeNotificationOpenTimeSlots = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
activeNotificationOpenTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.TYPE_NOTIFICATION_OPENED)
.getActiveTimeSlots()));
assertEquals(1, activeNotificationOpenTimeSlots.size());
}
@Test
public void testQueryUsageStatsService() {
UsageEvents.Event e = new UsageEvents.Event(SHORTCUT_INVOCATION,
System.currentTimeMillis());
e.mPackage = TEST_PKG_NAME;
e.mShortcutId = TEST_SHORTCUT_ID;
List<UsageEvents.Event> events = new ArrayList<>();
events.add(e);
UsageEvents usageEvents = new UsageEvents(events, new String[]{});
when(mUsageStatsManagerInternal.queryEventsForUser(anyInt(), anyLong(), anyLong(),
anyBoolean())).thenReturn(usageEvents);
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
buildPerson());
mDataManager.onShortcutAddedOrUpdated(shortcut);
mDataManager.queryUsageStatsService(USER_ID_PRIMARY, 0L, Long.MAX_VALUE);
List<Range<Long>> activeShortcutInvocationTimeSlots = new ArrayList<>();
mDataManager.forAllPackages(packageData ->
activeShortcutInvocationTimeSlots.addAll(
packageData.getEventHistory(TEST_SHORTCUT_ID)
.getEventIndex(Event.TYPE_SHORTCUT_INVOCATION)
.getActiveTimeSlots()));
assertEquals(1, activeShortcutInvocationTimeSlots.size());
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock);
}
private ShortcutInfo buildShortcutInfo(String packageName, int userId, String id,
@Nullable Person person) {
Context mockContext = mock(Context.class);
when(mockContext.getPackageName()).thenReturn(packageName);
when(mockContext.getUserId()).thenReturn(userId);
when(mockContext.getUser()).thenReturn(UserHandle.of(userId));
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mockContext, id)
.setShortLabel(id)
.setIntent(new Intent("TestIntent"));
if (person != null) {
builder.setPersons(new Person[] {person});
}
return builder.build();
}
private Person buildPerson() {
return buildPerson(true, false);
}
private Person buildPerson(boolean isImportant, boolean isBot) {
return new Person.Builder()
.setImportant(isImportant)
.setBot(isBot)
.setUri(CONTACT_URI)
.build();
}
private UserInfo buildUserInfo(int userId) {
return new UserInfo(userId, "", 0);
}
private class TestContactsQueryHelper extends ContactsQueryHelper {
private Uri mContactUri;
private boolean mIsStarred;
private String mPhoneNumber;
TestContactsQueryHelper(Context context) {
super(context);
mContactUri = Uri.parse(CONTACT_URI);
mIsStarred = false;
mPhoneNumber = PHONE_NUMBER;
}
@Override
boolean query(@NonNull String contactUri) {
return true;
}
@Override
boolean querySince(long sinceTime) {
return true;
}
@Override
@Nullable
Uri getContactUri() {
return mContactUri;
}
@Override
boolean isStarred() {
return mIsStarred;
}
@Override
@Nullable
String getPhoneNumber() {
return mPhoneNumber;
}
}
private class TestInjector extends DataManager.Injector {
private final TestContactsQueryHelper mContactsQueryHelper =
new TestContactsQueryHelper(mContext);
@Override
ScheduledExecutorService createScheduledExecutor() {
return mExecutorService;
}
@Override
ContactsQueryHelper createContactsQueryHelper(Context context) {
return mContactsQueryHelper;
}
@Override
int getCallingUserId() {
return mCallingUserId;
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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 com.android.server.people.data.TestUtils.timestamp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.google.android.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@RunWith(JUnit4.class)
public final class EventHistoryImplTest {
private static final long CURRENT_TIMESTAMP = timestamp("01-30 18:50");
private static final Event E1 = new Event(timestamp("01-06 05:26"),
Event.TYPE_NOTIFICATION_OPENED);
private static final Event E2 = new Event(timestamp("01-27 18:41"),
Event.TYPE_NOTIFICATION_OPENED);
private static final Event E3 = new Event(timestamp("01-30 03:06"),
Event.TYPE_SHARE_IMAGE);
private static final Event E4 = new Event(timestamp("01-30 18:14"),
Event.TYPE_SMS_INCOMING);
private EventHistoryImpl mEventHistory;
@Before
public void setUp() {
EventIndex.Injector eventIndexInjector = new EventIndex.Injector() {
@Override
long currentTimeMillis() {
return CURRENT_TIMESTAMP;
}
};
EventHistoryImpl.Injector eventHistoryInjector = new EventHistoryImpl.Injector() {
@Override
EventIndex createEventIndex() {
return new EventIndex(eventIndexInjector);
}
};
mEventHistory = new EventHistoryImpl(eventHistoryInjector);
}
@Test
public void testNoEvents() {
EventIndex eventIndex = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
assertTrue(eventIndex.isEmpty());
List<Event> events = mEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, 999L);
assertTrue(events.isEmpty());
}
@Test
public void testMultipleEvents() {
mEventHistory.addEvent(E1);
mEventHistory.addEvent(E2);
mEventHistory.addEvent(E3);
mEventHistory.addEvent(E4);
EventIndex eventIndex = mEventHistory.getEventIndex(Event.ALL_EVENT_TYPES);
assertEquals(4, eventIndex.getActiveTimeSlots().size());
List<Event> events = mEventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(4, events.size());
}
@Test
public void testQuerySomeEventTypes() {
mEventHistory.addEvent(E1);
mEventHistory.addEvent(E2);
mEventHistory.addEvent(E3);
mEventHistory.addEvent(E4);
EventIndex eventIndex = mEventHistory.getEventIndex(Event.NOTIFICATION_EVENT_TYPES);
assertEquals(2, eventIndex.getActiveTimeSlots().size());
List<Event> events = mEventHistory.queryEvents(
Event.NOTIFICATION_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(2, events.size());
}
@Test
public void testQuerySingleEventType() {
mEventHistory.addEvent(E1);
mEventHistory.addEvent(E2);
mEventHistory.addEvent(E3);
mEventHistory.addEvent(E4);
EventIndex eventIndex = mEventHistory.getEventIndex(Event.TYPE_SHARE_IMAGE);
assertEquals(1, eventIndex.getActiveTimeSlots().size());
List<Event> events = mEventHistory.queryEvents(
Sets.newArraySet(Event.TYPE_SHARE_IMAGE), 0L, Long.MAX_VALUE);
assertEquals(1, events.size());
}
}

View File

@@ -0,0 +1,176 @@
/*
* 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 com.android.server.people.data.TestUtils.timestamp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.util.Range;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@RunWith(JUnit4.class)
public final class EventIndexTest {
private static final long CURRENT_TIMESTAMP = timestamp("01-30 18:50");
private static final long SECONDS_PER_HOUR = 60L * 60L;
private static final long SECONDS_PER_DAY = SECONDS_PER_HOUR * 24L;
private TestInjector mInjector;
private EventIndex mEventIndex;
@Before
public void setUp() {
mInjector = new TestInjector(CURRENT_TIMESTAMP);
mEventIndex = new EventIndex(mInjector);
}
@Test
public void testNoEvents() {
assertTrue(mEventIndex.isEmpty());
assertNull(mEventIndex.getMostRecentActiveTimeSlot());
assertTrue(mEventIndex.getActiveTimeSlots().isEmpty());
}
@Test
public void testMultipleEvents() {
mEventIndex.addEvent(timestamp("01-06 05:26"));
mEventIndex.addEvent(timestamp("01-27 18:41"));
mEventIndex.addEvent(timestamp("01-30 03:06"));
mEventIndex.addEvent(timestamp("01-30 18:14"));
assertFalse(mEventIndex.isEmpty());
Range<Long> mostRecentSlot = mEventIndex.getMostRecentActiveTimeSlot();
assertNotNull(mostRecentSlot);
assertTimeSlot(timestamp("01-30 18:14"), timestamp("01-30 18:16"), mostRecentSlot);
List<Range<Long>> slots = mEventIndex.getActiveTimeSlots();
assertEquals(4, slots.size());
assertTimeSlot(timestamp("01-06 00:00"), timestamp("01-07 00:00"), slots.get(0));
assertTimeSlot(timestamp("01-27 16:00"), timestamp("01-27 20:00"), slots.get(1));
assertTimeSlot(timestamp("01-30 03:00"), timestamp("01-30 04:00"), slots.get(2));
assertTimeSlot(timestamp("01-30 18:14"), timestamp("01-30 18:16"), slots.get(3));
}
@Test
public void testBitmapShift() {
mEventIndex.addEvent(CURRENT_TIMESTAMP);
List<Range<Long>> slots;
slots = mEventIndex.getActiveTimeSlots();
assertEquals(1, slots.size());
assertTimeSlot(timestamp("01-30 18:50"), timestamp("01-30 18:52"), slots.get(0));
mInjector.moveTimeForwardSeconds(SECONDS_PER_HOUR * 3L);
mEventIndex.update();
slots = mEventIndex.getActiveTimeSlots();
assertEquals(1, slots.size());
assertTimeSlot(timestamp("01-30 18:00"), timestamp("01-30 19:00"), slots.get(0));
mInjector.moveTimeForwardSeconds(SECONDS_PER_DAY * 6L);
mEventIndex.update();
slots = mEventIndex.getActiveTimeSlots();
assertEquals(1, slots.size());
assertTimeSlot(timestamp("01-30 16:00"), timestamp("01-30 20:00"), slots.get(0));
mInjector.moveTimeForwardSeconds(SECONDS_PER_DAY * 30L);
mEventIndex.update();
slots = mEventIndex.getActiveTimeSlots();
assertEquals(1, slots.size());
assertTimeSlot(timestamp("01-30 00:00"), timestamp("01-31 00:00"), slots.get(0));
mInjector.moveTimeForwardSeconds(SECONDS_PER_DAY * 80L);
mEventIndex.update();
slots = mEventIndex.getActiveTimeSlots();
// The event has been shifted off the left end.
assertTrue(slots.isEmpty());
}
@Test
public void testCopyConstructor() {
mEventIndex.addEvent(timestamp("01-06 05:26"));
mEventIndex.addEvent(timestamp("01-27 18:41"));
mEventIndex.addEvent(timestamp("01-30 03:06"));
mEventIndex.addEvent(timestamp("01-30 18:14"));
List<Range<Long>> slots = mEventIndex.getActiveTimeSlots();
EventIndex newIndex = new EventIndex(mEventIndex);
List<Range<Long>> newSlots = newIndex.getActiveTimeSlots();
assertEquals(slots.size(), newSlots.size());
for (int i = 0; i < slots.size(); i++) {
assertEquals(slots.get(i), newSlots.get(i));
}
}
@Test
public void combineEventIndexes() {
EventIndex a = new EventIndex(mInjector);
mInjector.mCurrentTimeMillis = timestamp("01-27 18:41");
a.addEvent(mInjector.mCurrentTimeMillis);
mInjector.mCurrentTimeMillis = timestamp("01-30 03:06");
a.addEvent(mInjector.mCurrentTimeMillis);
mInjector.mCurrentTimeMillis = CURRENT_TIMESTAMP;
EventIndex b = new EventIndex(mInjector);
b.addEvent(timestamp("01-06 05:26"));
b.addEvent(timestamp("01-30 18:14"));
EventIndex combined = EventIndex.combine(a, b);
List<Range<Long>> slots = combined.getActiveTimeSlots();
assertEquals(4, slots.size());
assertTimeSlot(timestamp("01-06 00:00"), timestamp("01-07 00:00"), slots.get(0));
assertTimeSlot(timestamp("01-27 16:00"), timestamp("01-27 20:00"), slots.get(1));
assertTimeSlot(timestamp("01-30 03:00"), timestamp("01-30 04:00"), slots.get(2));
assertTimeSlot(timestamp("01-30 18:14"), timestamp("01-30 18:16"), slots.get(3));
}
private static void assertTimeSlot(
long expectedLower, long expectedUpper, Range<Long> actualSlot) {
assertEquals(expectedLower, actualSlot.getLower().longValue());
assertEquals(expectedUpper, actualSlot.getUpper().longValue());
}
private class TestInjector extends EventIndex.Injector {
private long mCurrentTimeMillis;
TestInjector(long currentTimeMillis) {
mCurrentTimeMillis = currentTimeMillis;
}
private void moveTimeForwardSeconds(long seconds) {
mCurrentTimeMillis += (seconds * 1000L);
}
@Override
long currentTimeMillis() {
return mCurrentTimeMillis;
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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.assertTrue;
import com.google.android.collect.Lists;
import com.google.android.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@RunWith(JUnit4.class)
public final class EventListTest {
private static final Event E1 = new Event(101L, Event.TYPE_NOTIFICATION_OPENED);
private static final Event E2 = new Event(103L, Event.TYPE_NOTIFICATION_OPENED);
private static final Event E3 = new Event(107L, Event.TYPE_SHARE_IMAGE);
private static final Event E4 = new Event(109L, Event.TYPE_SMS_INCOMING);
private EventList mEventList;
@Before
public void setUp() {
mEventList = new EventList();
}
@Test
public void testQueryEmptyEventList() {
List<Event> events = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 0L, 999L);
assertTrue(events.isEmpty());
}
@Test
public void testAddAndQueryEvents() {
List<Event> in = Lists.newArrayList(E1, E2, E3, E4);
for (Event e : in) {
mEventList.add(e);
}
List<Event> out = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 0L, 999L);
assertEventListEquals(in, out);
}
@Test
public void testAddEventsNotInOrder() {
mEventList.add(E3);
mEventList.add(E1);
mEventList.add(E4);
mEventList.add(E2);
List<Event> out = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 0L, 999L);
List<Event> expected = Lists.newArrayList(E1, E2, E3, E4);
assertEventListEquals(expected, out);
}
@Test
public void testQueryEventsByType() {
mEventList.add(E1);
mEventList.add(E2);
mEventList.add(E3);
mEventList.add(E4);
List<Event> out = mEventList.queryEvents(
Sets.newArraySet(Event.TYPE_NOTIFICATION_OPENED), 0L, 999L);
assertEventListEquals(Lists.newArrayList(E1, E2), out);
}
@Test
public void testQueryEventsByTimeRange() {
mEventList.add(E1);
mEventList.add(E2);
mEventList.add(E3);
mEventList.add(E4);
List<Event> out = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 103L, 109L);
// Only E2 and E3 are in the time range [103L, 109L).
assertEventListEquals(Lists.newArrayList(E2, E3), out);
}
@Test
public void testQueryEventsOutOfRange() {
mEventList.add(E1);
mEventList.add(E2);
mEventList.add(E3);
mEventList.add(E4);
List<Event> out = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 900L, 900L);
assertTrue(out.isEmpty());
}
@Test
public void testAddDuplicateEvents() {
mEventList.add(E1);
mEventList.add(E2);
mEventList.add(E2);
mEventList.add(E3);
mEventList.add(E2);
mEventList.add(E3);
mEventList.add(E3);
mEventList.add(E4);
mEventList.add(E1);
mEventList.add(E3);
mEventList.add(E2);
List<Event> out = mEventList.queryEvents(Event.ALL_EVENT_TYPES, 0L, 999L);
List<Event> expected = Lists.newArrayList(E1, E2, E3, E4);
assertEventListEquals(expected, out);
}
private static void assertEventListEquals(List<Event> expected, List<Event> actual) {
assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
assertEquals(expected.get(i).getTimestamp(), actual.get(i).getTimestamp());
assertEquals(expected.get(i).getType(), actual.get(i).getType());
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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 android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
@RunWith(JUnit4.class)
public final class PackageDataTest {
private static final String PACKAGE_NAME = "com.google.test";
private static final int USER_ID = 0;
private static final String SHORTCUT_ID = "abc";
private static final LocusId LOCUS_ID = new LocusId("def");
private static final Uri CONTACT_URI = Uri.parse("tel:+1234567890");
private static final String PHONE_NUMBER = "+1234567890";
private Event mE1;
private Event mE2;
private Event mE3;
private Event mE4;
private PackageData mPackageData;
@Before
public void setUp() {
mPackageData = new PackageData(PACKAGE_NAME, USER_ID);
ConversationInfo conversationInfo = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(LOCUS_ID)
.setContactUri(CONTACT_URI)
.setContactPhoneNumber(PHONE_NUMBER)
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
.build();
mPackageData.getConversationStore().addOrUpdate(conversationInfo);
long currentTimestamp = System.currentTimeMillis();
mE1 = new Event(currentTimestamp - 800L, Event.TYPE_SHORTCUT_INVOCATION);
mE2 = new Event(currentTimestamp - 700L, Event.TYPE_NOTIFICATION_OPENED);
mE3 = new Event(currentTimestamp - 600L, Event.TYPE_CALL_INCOMING);
mE4 = new Event(currentTimestamp - 500L, Event.TYPE_SMS_OUTGOING);
}
@Test
public void testGetEventHistory() {
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateLocusEventHistory(LOCUS_ID).addEvent(mE2);
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(2, events.size());
assertEventEquals(mE1, events.get(0));
assertEventEquals(mE2, events.get(1));
}
@Test
public void testGetEventHistoryDefaultDialerAndSmsApp() {
mPackageData.setIsDefaultDialer(true);
mPackageData.setIsDefaultSmsApp(true);
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateCallEventHistory(PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateSmsEventHistory(PHONE_NUMBER).addEvent(mE4);
assertTrue(mPackageData.isDefaultDialer());
assertTrue(mPackageData.isDefaultSmsApp());
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(3, events.size());
assertEventEquals(mE1, events.get(0));
assertEventEquals(mE3, events.get(1));
assertEventEquals(mE4, events.get(2));
}
@Test
public void testGetEventHistoryNotDefaultDialerOrSmsApp() {
EventStore eventStore = mPackageData.getEventStore();
eventStore.getOrCreateShortcutEventHistory(SHORTCUT_ID).addEvent(mE1);
eventStore.getOrCreateCallEventHistory(PHONE_NUMBER).addEvent(mE3);
eventStore.getOrCreateSmsEventHistory(PHONE_NUMBER).addEvent(mE4);
assertFalse(mPackageData.isDefaultDialer());
assertFalse(mPackageData.isDefaultSmsApp());
EventHistory eventHistory = mPackageData.getEventHistory(SHORTCUT_ID);
List<Event> events = eventHistory.queryEvents(Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
assertEquals(1, events.size());
assertEventEquals(mE1, events.get(0));
}
private void assertEventEquals(Event expected, Event actual) {
assertEquals(expected.getTimestamp(), actual.getTimestamp());
assertEquals(expected.getType(), actual.getType());
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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 java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
final class TestUtils {
/**
* Gets the epoch time in millis for the specified time string.
* @param timeString e.g. "01-02 15:20"
* @return epoch time in millis
*/
static long timestamp(String timeString) {
String str = String.format("2020-%s", timeString);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return toEpochMilli(LocalDateTime.parse(str, formatter));
}
private static long toEpochMilli(LocalDateTime localDateTime) {
return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
private TestUtils() {
}
}