Merge "People Service data layer main structure"
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
195
services/people/java/com/android/server/people/data/Event.java
Normal file
195
services/people/java/com/android/server/people/data/Event.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user