Add UsageStatsQueryHelper to People Service
It processes these UsageStats events:
- SHORTCUT_INVOCATION
- NOTIFICATION_INTERRUPTION
- LOCUS_ID_SET
- ACTIVITY_PAUSED/STOPPED/DESTROYED
The in-app conversation is counted from the LOCUS_ID_SET event to the
next LOCUS_ID_SET event, or the associated Activity is
paused/stopped/destroyed.
Change-Id: I5ecb78e61f9f69129e7fa9cc67e9e94b2148fcda
Test: Manual tests \
atest com.android.server.people.data.UsageStatsQueryHelperTest \
atest com.android.server.people.data.DataManagerTest \
atest com.android.server.people.data.CallLogQueryHelperTest \
atest com.android.server.people.data.ConversationInfoTest \
atest com.android.server.people.data.ConversationStoreTest
Bug: 146522621
This commit is contained in:
@@ -107,7 +107,7 @@ class CallLogQueryHelper {
|
||||
}
|
||||
@Event.EventType int eventType = CALL_TYPE_TO_EVENT_TYPE.get(callType);
|
||||
Event event = new Event.Builder(date, eventType)
|
||||
.setCallDetails(new Event.CallDetails(durationSeconds))
|
||||
.setDurationSeconds((int) durationSeconds)
|
||||
.build();
|
||||
mEventConsumer.accept(phoneNumber, event);
|
||||
return true;
|
||||
|
||||
@@ -40,6 +40,9 @@ class ConversationStore {
|
||||
// Phone Number -> Shortcut ID
|
||||
private final Map<String, String> mPhoneNumberToShortcutIdMap = new ArrayMap<>();
|
||||
|
||||
// Notification Channel ID -> Shortcut ID
|
||||
private final Map<String, String> mNotifChannelIdToShortcutIdMap = new ArrayMap<>();
|
||||
|
||||
void addOrUpdate(@NonNull ConversationInfo conversationInfo) {
|
||||
mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo);
|
||||
|
||||
@@ -57,6 +60,11 @@ class ConversationStore {
|
||||
if (phoneNumber != null) {
|
||||
mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId());
|
||||
}
|
||||
|
||||
String notifChannelId = conversationInfo.getNotificationChannelId();
|
||||
if (notifChannelId != null) {
|
||||
mNotifChannelIdToShortcutIdMap.put(notifChannelId, conversationInfo.getShortcutId());
|
||||
}
|
||||
}
|
||||
|
||||
void deleteConversation(@NonNull String shortcutId) {
|
||||
@@ -79,6 +87,11 @@ class ConversationStore {
|
||||
if (phoneNumber != null) {
|
||||
mPhoneNumberToShortcutIdMap.remove(phoneNumber);
|
||||
}
|
||||
|
||||
String notifChannelId = conversationInfo.getNotificationChannelId();
|
||||
if (notifChannelId != null) {
|
||||
mNotifChannelIdToShortcutIdMap.remove(notifChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
|
||||
@@ -106,4 +119,9 @@ class ConversationStore {
|
||||
ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) {
|
||||
return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ConversationInfo getConversationByNotificationChannelId(@NonNull String notifChannelId) {
|
||||
return getConversation(mNotifChannelIdToShortcutIdMap.get(notifChannelId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ 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;
|
||||
@@ -69,6 +67,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A class manages the lifecycle of the conversations and associated data, and exposes the methods
|
||||
@@ -96,7 +95,6 @@ public class DataManager {
|
||||
private final ContentObserver mMmsSmsContentObserver;
|
||||
|
||||
private ShortcutServiceInternal mShortcutServiceInternal;
|
||||
private UsageStatsManagerInternal mUsageStatsManagerInternal;
|
||||
private ShortcutManager mShortcutManager;
|
||||
private UserManager mUserManager;
|
||||
|
||||
@@ -118,7 +116,6 @@ public class DataManager {
|
||||
/** 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);
|
||||
|
||||
@@ -384,36 +381,6 @@ public class DataManager {
|
||||
conversationStore.addOrUpdate(builder.build());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
void queryUsageStatsService(@UserIdInt int userId, long currentTime, long lastQueryTime) {
|
||||
UsageEvents usageEvents = mUsageStatsManagerInternal.queryEventsForUser(
|
||||
userId, lastQueryTime, currentTime, false, false);
|
||||
if (usageEvents == null) {
|
||||
return;
|
||||
}
|
||||
while (usageEvents.hasNextEvent()) {
|
||||
UsageEvents.Event e = new UsageEvents.Event();
|
||||
usageEvents.getNextEvent(e);
|
||||
|
||||
String packageName = e.getPackageName();
|
||||
PackageData packageData = getPackage(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);
|
||||
@@ -611,19 +578,20 @@ public class DataManager {
|
||||
*/
|
||||
private class UsageStatsQueryRunnable implements Runnable {
|
||||
|
||||
private final int mUserId;
|
||||
private long mLastQueryTime;
|
||||
private final UsageStatsQueryHelper mUsageStatsQueryHelper;
|
||||
private long mLastEventTimestamp;
|
||||
|
||||
private UsageStatsQueryRunnable(int userId) {
|
||||
mUserId = userId;
|
||||
mLastQueryTime = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
|
||||
mUsageStatsQueryHelper = mInjector.createUsageStatsQueryHelper(userId,
|
||||
(packageName) -> getPackage(packageName, userId));
|
||||
mLastEventTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
queryUsageStatsService(mUserId, currentTime, mLastQueryTime);
|
||||
mLastQueryTime = currentTime;
|
||||
if (mUsageStatsQueryHelper.querySince(mLastEventTimestamp)) {
|
||||
mLastEventTimestamp = mUsageStatsQueryHelper.getLastEventTimestamp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,6 +647,11 @@ public class DataManager {
|
||||
return new SmsQueryHelper(context, eventConsumer);
|
||||
}
|
||||
|
||||
UsageStatsQueryHelper createUsageStatsQueryHelper(@UserIdInt int userId,
|
||||
Function<String, PackageData> packageDataGetter) {
|
||||
return new UsageStatsQueryHelper(userId, packageDataGetter);
|
||||
}
|
||||
|
||||
int getCallingUserId() {
|
||||
return Binder.getCallingUserHandle().getIdentifier();
|
||||
}
|
||||
|
||||
@@ -18,14 +18,12 @@ 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.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** An event representing the interaction with a specific conversation or app. */
|
||||
@@ -55,6 +53,8 @@ public class Event {
|
||||
|
||||
public static final int TYPE_CALL_MISSED = 12;
|
||||
|
||||
public static final int TYPE_IN_APP_CONVERSATION = 13;
|
||||
|
||||
@IntDef(prefix = { "TYPE_" }, value = {
|
||||
TYPE_SHORTCUT_INVOCATION,
|
||||
TYPE_NOTIFICATION_POSTED,
|
||||
@@ -68,6 +68,7 @@ public class Event {
|
||||
TYPE_CALL_OUTGOING,
|
||||
TYPE_CALL_INCOMING,
|
||||
TYPE_CALL_MISSED,
|
||||
TYPE_IN_APP_CONVERSATION,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface EventType {}
|
||||
@@ -95,6 +96,7 @@ public class Event {
|
||||
CALL_EVENT_TYPES.add(TYPE_CALL_MISSED);
|
||||
|
||||
ALL_EVENT_TYPES.add(TYPE_SHORTCUT_INVOCATION);
|
||||
ALL_EVENT_TYPES.add(TYPE_IN_APP_CONVERSATION);
|
||||
ALL_EVENT_TYPES.addAll(NOTIFICATION_EVENT_TYPES);
|
||||
ALL_EVENT_TYPES.addAll(SHARE_EVENT_TYPES);
|
||||
ALL_EVENT_TYPES.addAll(SMS_EVENT_TYPES);
|
||||
@@ -105,18 +107,18 @@ public class Event {
|
||||
|
||||
private final int mType;
|
||||
|
||||
private final CallDetails mCallDetails;
|
||||
private final int mDurationSeconds;
|
||||
|
||||
Event(long timestamp, @EventType int type) {
|
||||
mTimestamp = timestamp;
|
||||
mType = type;
|
||||
mCallDetails = null;
|
||||
mDurationSeconds = 0;
|
||||
}
|
||||
|
||||
private Event(@NonNull Builder builder) {
|
||||
mTimestamp = builder.mTimestamp;
|
||||
mType = builder.mType;
|
||||
mCallDetails = builder.mCallDetails;
|
||||
mDurationSeconds = builder.mDurationSeconds;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
@@ -128,12 +130,35 @@ public class Event {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* Gets the duration of the event in seconds. It is only available for these events:
|
||||
* <ul>
|
||||
* <li>{@link #TYPE_CALL_INCOMING}
|
||||
* <li>{@link #TYPE_CALL_OUTGOING}
|
||||
* <li>{@link #TYPE_IN_APP_CONVERSATION}
|
||||
* </ul>
|
||||
* <p>For the other event types, it always returns {@code 0}.
|
||||
*/
|
||||
@Nullable
|
||||
public CallDetails getCallDetails() {
|
||||
return mCallDetails;
|
||||
public int getDurationSeconds() {
|
||||
return mDurationSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Event)) {
|
||||
return false;
|
||||
}
|
||||
Event other = (Event) obj;
|
||||
return mTimestamp == other.mTimestamp
|
||||
&& mType == other.mType
|
||||
&& mDurationSeconds == other.mDurationSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mTimestamp, mType, mDurationSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,32 +167,13 @@ public class Event {
|
||||
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);
|
||||
if (mDurationSeconds > 0) {
|
||||
sb.append(", durationSeconds=").append(mDurationSeconds);
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -175,16 +181,15 @@ public class Event {
|
||||
|
||||
private final int mType;
|
||||
|
||||
private CallDetails mCallDetails;
|
||||
private int mDurationSeconds;
|
||||
|
||||
Builder(long timestamp, @EventType int type) {
|
||||
mTimestamp = timestamp;
|
||||
mType = type;
|
||||
}
|
||||
|
||||
Builder setCallDetails(CallDetails callDetails) {
|
||||
Preconditions.checkArgument(CALL_EVENT_TYPES.contains(mType));
|
||||
mCallDetails = callDetails;
|
||||
Builder setDurationSeconds(int durationSeconds) {
|
||||
mDurationSeconds = durationSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.app.usage.UsageEvents;
|
||||
import android.app.usage.UsageStatsManagerInternal;
|
||||
import android.content.ComponentName;
|
||||
import android.content.LocusId;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.server.LocalServices;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/** A helper class that queries {@link UsageStatsManagerInternal}. */
|
||||
class UsageStatsQueryHelper {
|
||||
|
||||
private final UsageStatsManagerInternal mUsageStatsManagerInternal;
|
||||
private final int mUserId;
|
||||
private final Function<String, PackageData> mPackageDataGetter;
|
||||
// Activity name -> Conversation start event (LOCUS_ID_SET)
|
||||
private final Map<ComponentName, UsageEvents.Event> mConvoStartEvents = new ArrayMap<>();
|
||||
private long mLastEventTimestamp;
|
||||
|
||||
/**
|
||||
* @param userId The user whose events are to be queried.
|
||||
* @param packageDataGetter The function to get {@link PackageData} with a package name.
|
||||
*/
|
||||
UsageStatsQueryHelper(@UserIdInt int userId,
|
||||
Function<String, PackageData> packageDataGetter) {
|
||||
mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
|
||||
mUserId = userId;
|
||||
mPackageDataGetter = packageDataGetter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries {@link UsageStatsManagerInternal} for the recent events occurred since {@code
|
||||
* sinceTime} and adds the derived {@link Event}s into the corresponding package's event store,
|
||||
*
|
||||
* @return true if the query runs successfully and at least one event is found.
|
||||
*/
|
||||
boolean querySince(long sinceTime) {
|
||||
UsageEvents usageEvents = mUsageStatsManagerInternal.queryEventsForUser(
|
||||
mUserId, sinceTime, System.currentTimeMillis(), false, false);
|
||||
if (usageEvents == null) {
|
||||
return false;
|
||||
}
|
||||
boolean hasEvents = false;
|
||||
while (usageEvents.hasNextEvent()) {
|
||||
UsageEvents.Event e = new UsageEvents.Event();
|
||||
usageEvents.getNextEvent(e);
|
||||
|
||||
hasEvents = true;
|
||||
mLastEventTimestamp = Math.max(mLastEventTimestamp, e.getTimeStamp());
|
||||
String packageName = e.getPackageName();
|
||||
PackageData packageData = mPackageDataGetter.apply(packageName);
|
||||
if (packageData == null) {
|
||||
continue;
|
||||
}
|
||||
switch (e.getEventType()) {
|
||||
case UsageEvents.Event.SHORTCUT_INVOCATION:
|
||||
addEventByShortcutId(packageData, e.getShortcutId(),
|
||||
new Event(e.getTimeStamp(), Event.TYPE_SHORTCUT_INVOCATION));
|
||||
break;
|
||||
case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
|
||||
addEventByNotificationChannelId(packageData, e.getNotificationChannelId(),
|
||||
new Event(e.getTimeStamp(), Event.TYPE_NOTIFICATION_POSTED));
|
||||
break;
|
||||
case UsageEvents.Event.LOCUS_ID_SET:
|
||||
onInAppConversationEnded(packageData, e);
|
||||
LocusId locusId = e.getLocusId() != null ? new LocusId(e.getLocusId()) : null;
|
||||
if (locusId != null) {
|
||||
if (packageData.getConversationStore().getConversationByLocusId(locusId)
|
||||
!= null) {
|
||||
ComponentName activityName =
|
||||
new ComponentName(packageName, e.getClassName());
|
||||
mConvoStartEvents.put(activityName, e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UsageEvents.Event.ACTIVITY_PAUSED:
|
||||
case UsageEvents.Event.ACTIVITY_STOPPED:
|
||||
case UsageEvents.Event.ACTIVITY_DESTROYED:
|
||||
onInAppConversationEnded(packageData, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasEvents;
|
||||
}
|
||||
|
||||
long getLastEventTimestamp() {
|
||||
return mLastEventTimestamp;
|
||||
}
|
||||
|
||||
private void onInAppConversationEnded(@NonNull PackageData packageData,
|
||||
@NonNull UsageEvents.Event endEvent) {
|
||||
ComponentName activityName =
|
||||
new ComponentName(endEvent.getPackageName(), endEvent.getClassName());
|
||||
UsageEvents.Event startEvent = mConvoStartEvents.remove(activityName);
|
||||
if (startEvent == null || startEvent.getTimeStamp() >= endEvent.getTimeStamp()) {
|
||||
return;
|
||||
}
|
||||
long durationMillis = endEvent.getTimeStamp() - startEvent.getTimeStamp();
|
||||
Event event = new Event.Builder(startEvent.getTimeStamp(), Event.TYPE_IN_APP_CONVERSATION)
|
||||
.setDurationSeconds((int) (durationMillis / DateUtils.SECOND_IN_MILLIS))
|
||||
.build();
|
||||
addEventByLocusId(packageData, new LocusId(startEvent.getLocusId()), event);
|
||||
}
|
||||
|
||||
private void addEventByShortcutId(PackageData packageData, String shortcutId, Event event) {
|
||||
if (packageData.getConversationStore().getConversation(shortcutId) == null) {
|
||||
return;
|
||||
}
|
||||
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateShortcutEventHistory(
|
||||
shortcutId);
|
||||
eventHistory.addEvent(event);
|
||||
}
|
||||
|
||||
private void addEventByLocusId(PackageData packageData, LocusId locusId, Event event) {
|
||||
if (packageData.getConversationStore().getConversationByLocusId(locusId) == null) {
|
||||
return;
|
||||
}
|
||||
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateLocusEventHistory(
|
||||
locusId);
|
||||
eventHistory.addEvent(event);
|
||||
}
|
||||
|
||||
private void addEventByNotificationChannelId(PackageData packageData,
|
||||
String notificationChannelId, Event event) {
|
||||
ConversationInfo conversationInfo =
|
||||
packageData.getConversationStore().getConversationByNotificationChannelId(
|
||||
notificationChannelId);
|
||||
if (conversationInfo == null) {
|
||||
return;
|
||||
}
|
||||
EventHistoryImpl eventHistory = packageData.getEventStore().getOrCreateShortcutEventHistory(
|
||||
conversationInfo.getShortcutId());
|
||||
eventHistory.addEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public final class CallLogQueryHelperTest {
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(Event.TYPE_CALL_INCOMING, events.get(0).getType());
|
||||
assertEquals(100L, events.get(0).getTimestamp());
|
||||
assertEquals(30L, events.get(0).getCallDetails().getDurationSeconds());
|
||||
assertEquals(30L, events.get(0).getDurationSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,7 +108,7 @@ public final class CallLogQueryHelperTest {
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(Event.TYPE_CALL_OUTGOING, events.get(0).getType());
|
||||
assertEquals(100L, events.get(0).getTimestamp());
|
||||
assertEquals(40L, events.get(0).getCallDetails().getDurationSeconds());
|
||||
assertEquals(40L, events.get(0).getDurationSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -124,7 +124,7 @@ public final class CallLogQueryHelperTest {
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(Event.TYPE_CALL_MISSED, events.get(0).getType());
|
||||
assertEquals(100L, events.get(0).getTimestamp());
|
||||
assertEquals(0L, events.get(0).getCallDetails().getDurationSeconds());
|
||||
assertEquals(0L, events.get(0).getDurationSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -145,7 +145,7 @@ public final class CallLogQueryHelperTest {
|
||||
assertEquals(100L, events.get(0).getTimestamp());
|
||||
assertEquals(Event.TYPE_CALL_OUTGOING, events.get(1).getType());
|
||||
assertEquals(110L, events.get(1).getTimestamp());
|
||||
assertEquals(40L, events.get(1).getCallDetails().getDurationSeconds());
|
||||
assertEquals(40L, events.get(1).getDurationSeconds());
|
||||
}
|
||||
|
||||
private class EventConsumer implements BiConsumer<String, Event> {
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.Set;
|
||||
public final class ConversationStoreTest {
|
||||
|
||||
private static final String SHORTCUT_ID = "abc";
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "test : 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";
|
||||
@@ -59,16 +60,19 @@ public final class ConversationStoreTest {
|
||||
|
||||
@Test
|
||||
public void testUpdateConversation() {
|
||||
ConversationInfo original =
|
||||
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
|
||||
ConversationInfo original = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
|
||||
PHONE_NUMBER, null);
|
||||
mConversationStore.addOrUpdate(original);
|
||||
assertEquals(LOCUS_ID, mConversationStore.getConversation(SHORTCUT_ID).getLocusId());
|
||||
assertNull(mConversationStore.getConversation(SHORTCUT_ID).getNotificationChannelId());
|
||||
|
||||
LocusId newLocusId = new LocusId("ghi");
|
||||
ConversationInfo update = buildConversationInfo(
|
||||
SHORTCUT_ID, newLocusId, CONTACT_URI, PHONE_NUMBER);
|
||||
SHORTCUT_ID, newLocusId, CONTACT_URI, PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
|
||||
mConversationStore.addOrUpdate(update);
|
||||
assertEquals(newLocusId, mConversationStore.getConversation(SHORTCUT_ID).getLocusId());
|
||||
ConversationInfo updated = mConversationStore.getConversation(SHORTCUT_ID);
|
||||
assertEquals(newLocusId, updated.getLocusId());
|
||||
assertEquals(NOTIFICATION_CHANNEL_ID, updated.getNotificationChannelId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,8 +101,8 @@ public final class ConversationStoreTest {
|
||||
|
||||
@Test
|
||||
public void testGetConversationByLocusId() {
|
||||
ConversationInfo in =
|
||||
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
|
||||
ConversationInfo in = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
|
||||
PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
|
||||
mConversationStore.addOrUpdate(in);
|
||||
ConversationInfo out = mConversationStore.getConversationByLocusId(LOCUS_ID);
|
||||
assertNotNull(out);
|
||||
@@ -110,8 +114,8 @@ public final class ConversationStoreTest {
|
||||
|
||||
@Test
|
||||
public void testGetConversationByContactUri() {
|
||||
ConversationInfo in =
|
||||
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
|
||||
ConversationInfo in = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
|
||||
PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
|
||||
mConversationStore.addOrUpdate(in);
|
||||
ConversationInfo out = mConversationStore.getConversationByContactUri(CONTACT_URI);
|
||||
assertNotNull(out);
|
||||
@@ -123,8 +127,8 @@ public final class ConversationStoreTest {
|
||||
|
||||
@Test
|
||||
public void testGetConversationByPhoneNumber() {
|
||||
ConversationInfo in =
|
||||
buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI, PHONE_NUMBER);
|
||||
ConversationInfo in = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
|
||||
PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
|
||||
mConversationStore.addOrUpdate(in);
|
||||
ConversationInfo out = mConversationStore.getConversationByPhoneNumber(PHONE_NUMBER);
|
||||
assertNotNull(out);
|
||||
@@ -134,17 +138,34 @@ public final class ConversationStoreTest {
|
||||
assertNull(mConversationStore.getConversationByPhoneNumber(PHONE_NUMBER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetConversationByNotificationChannelId() {
|
||||
ConversationInfo in = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
|
||||
PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
|
||||
mConversationStore.addOrUpdate(in);
|
||||
ConversationInfo out = mConversationStore.getConversationByNotificationChannelId(
|
||||
NOTIFICATION_CHANNEL_ID);
|
||||
assertNotNull(out);
|
||||
assertEquals(SHORTCUT_ID, out.getShortcutId());
|
||||
|
||||
mConversationStore.deleteConversation(SHORTCUT_ID);
|
||||
assertNull(
|
||||
mConversationStore.getConversationByNotificationChannelId(NOTIFICATION_CHANNEL_ID));
|
||||
}
|
||||
|
||||
private static ConversationInfo buildConversationInfo(String shortcutId) {
|
||||
return buildConversationInfo(shortcutId, null, null, null);
|
||||
return buildConversationInfo(shortcutId, null, null, null, null);
|
||||
}
|
||||
|
||||
private static ConversationInfo buildConversationInfo(
|
||||
String shortcutId, LocusId locusId, Uri contactUri, String phoneNumber) {
|
||||
String shortcutId, LocusId locusId, Uri contactUri, String phoneNumber,
|
||||
String notificationChannelId) {
|
||||
return new ConversationInfo.Builder()
|
||||
.setShortcutId(shortcutId)
|
||||
.setLocusId(locusId)
|
||||
.setContactUri(contactUri)
|
||||
.setContactPhoneNumber(phoneNumber)
|
||||
.setNotificationChannelId(notificationChannelId)
|
||||
.setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED)
|
||||
.setVip(true)
|
||||
.setBubbled(true)
|
||||
|
||||
@@ -16,15 +16,12 @@
|
||||
|
||||
package com.android.server.people.data;
|
||||
|
||||
import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
|
||||
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
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;
|
||||
@@ -40,7 +37,6 @@ 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.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -309,35 +305,6 @@ public final class DataManagerTest {
|
||||
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(), 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCallLogContentObserver() {
|
||||
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.people.data;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.usage.UsageEvents;
|
||||
import android.app.usage.UsageStatsManagerInternal;
|
||||
import android.content.LocusId;
|
||||
|
||||
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.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public final class UsageStatsQueryHelperTest {
|
||||
|
||||
private static final int USER_ID_PRIMARY = 0;
|
||||
private static final String PKG_NAME = "pkg";
|
||||
private static final String ACTIVITY_NAME = "TestActivity";
|
||||
private static final String SHORTCUT_ID = "abc";
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "test : abc";
|
||||
private static final LocusId LOCUS_ID_1 = new LocusId("locus_1");
|
||||
private static final LocusId LOCUS_ID_2 = new LocusId("locus_2");
|
||||
|
||||
@Mock private UsageStatsManagerInternal mUsageStatsManagerInternal;
|
||||
|
||||
private TestPackageData mPackageData;
|
||||
private UsageStatsQueryHelper mHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
addLocalServiceMock(UsageStatsManagerInternal.class, mUsageStatsManagerInternal);
|
||||
|
||||
mPackageData = new TestPackageData(PKG_NAME, USER_ID_PRIMARY, pkg -> false, pkg -> false);
|
||||
mPackageData.mConversationStore.mConversationInfo = new ConversationInfo.Builder()
|
||||
.setShortcutId(SHORTCUT_ID)
|
||||
.setNotificationChannelId(NOTIFICATION_CHANNEL_ID)
|
||||
.setLocusId(LOCUS_ID_1)
|
||||
.build();
|
||||
|
||||
mHelper = new UsageStatsQueryHelper(USER_ID_PRIMARY, pkg -> mPackageData);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryNoEvents() {
|
||||
assertFalse(mHelper.querySince(50L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryShortcutInvocationEvent() {
|
||||
addUsageEvents(createShortcutInvocationEvent(100L));
|
||||
|
||||
assertTrue(mHelper.querySince(50L));
|
||||
assertEquals(100L, mHelper.getLastEventTimestamp());
|
||||
Event expectedEvent = new Event(100L, Event.TYPE_SHORTCUT_INVOCATION);
|
||||
List<Event> events = mPackageData.mEventStore.mShortcutEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(expectedEvent, events.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryNotificationInterruptionEvent() {
|
||||
addUsageEvents(createNotificationInterruptionEvent(100L));
|
||||
|
||||
assertTrue(mHelper.querySince(50L));
|
||||
assertEquals(100L, mHelper.getLastEventTimestamp());
|
||||
Event expectedEvent = new Event(100L, Event.TYPE_NOTIFICATION_POSTED);
|
||||
List<Event> events = mPackageData.mEventStore.mShortcutEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(expectedEvent, events.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInAppConversationSwitch() {
|
||||
addUsageEvents(
|
||||
createLocusIdSetEvent(100_000L, LOCUS_ID_1.getId()),
|
||||
createLocusIdSetEvent(110_000L, LOCUS_ID_2.getId()));
|
||||
|
||||
assertTrue(mHelper.querySince(50_000L));
|
||||
assertEquals(110_000L, mHelper.getLastEventTimestamp());
|
||||
List<Event> events = mPackageData.mEventStore.mLocusEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(createInAppConversationEvent(100_000L, 10), events.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInAppConversationExplicitlyEnd() {
|
||||
addUsageEvents(
|
||||
createLocusIdSetEvent(100_000L, LOCUS_ID_1.getId()),
|
||||
createLocusIdSetEvent(110_000L, null));
|
||||
|
||||
assertTrue(mHelper.querySince(50_000L));
|
||||
assertEquals(110_000L, mHelper.getLastEventTimestamp());
|
||||
List<Event> events = mPackageData.mEventStore.mLocusEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(createInAppConversationEvent(100_000L, 10), events.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInAppConversationImplicitlyEnd() {
|
||||
addUsageEvents(
|
||||
createLocusIdSetEvent(100_000L, LOCUS_ID_1.getId()),
|
||||
createActivityStoppedEvent(110_000L));
|
||||
|
||||
assertTrue(mHelper.querySince(50_000L));
|
||||
assertEquals(110_000L, mHelper.getLastEventTimestamp());
|
||||
List<Event> events = mPackageData.mEventStore.mLocusEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(createInAppConversationEvent(100_000L, 10), events.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleInAppConversations() {
|
||||
addUsageEvents(
|
||||
createLocusIdSetEvent(100_000L, LOCUS_ID_1.getId()),
|
||||
createLocusIdSetEvent(110_000L, LOCUS_ID_2.getId()),
|
||||
createLocusIdSetEvent(130_000L, LOCUS_ID_1.getId()),
|
||||
createActivityStoppedEvent(160_000L));
|
||||
|
||||
assertTrue(mHelper.querySince(50_000L));
|
||||
assertEquals(160_000L, mHelper.getLastEventTimestamp());
|
||||
List<Event> events = mPackageData.mEventStore.mLocusEventHistory.queryEvents(
|
||||
Event.ALL_EVENT_TYPES, 0L, Long.MAX_VALUE);
|
||||
assertEquals(3, events.size());
|
||||
assertEquals(createInAppConversationEvent(100_000L, 10), events.get(0));
|
||||
assertEquals(createInAppConversationEvent(110_000L, 20), events.get(1));
|
||||
assertEquals(createInAppConversationEvent(130_000L, 30), events.get(2));
|
||||
}
|
||||
|
||||
private void addUsageEvents(UsageEvents.Event ... events) {
|
||||
UsageEvents usageEvents = new UsageEvents(Arrays.asList(events), new String[]{});
|
||||
when(mUsageStatsManagerInternal.queryEventsForUser(anyInt(), anyLong(), anyLong(),
|
||||
anyBoolean(), anyBoolean())).thenReturn(usageEvents);
|
||||
}
|
||||
|
||||
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
|
||||
LocalServices.removeServiceForTest(clazz);
|
||||
LocalServices.addService(clazz, mock);
|
||||
}
|
||||
|
||||
private static UsageEvents.Event createShortcutInvocationEvent(long timestamp) {
|
||||
UsageEvents.Event e = createUsageEvent(UsageEvents.Event.SHORTCUT_INVOCATION, timestamp);
|
||||
e.mShortcutId = SHORTCUT_ID;
|
||||
return e;
|
||||
}
|
||||
|
||||
private static UsageEvents.Event createNotificationInterruptionEvent(long timestamp) {
|
||||
UsageEvents.Event e = createUsageEvent(UsageEvents.Event.NOTIFICATION_INTERRUPTION,
|
||||
timestamp);
|
||||
e.mNotificationChannelId = NOTIFICATION_CHANNEL_ID;
|
||||
return e;
|
||||
}
|
||||
|
||||
private static UsageEvents.Event createLocusIdSetEvent(long timestamp, String locusId) {
|
||||
UsageEvents.Event e = createUsageEvent(UsageEvents.Event.LOCUS_ID_SET, timestamp);
|
||||
e.mClass = ACTIVITY_NAME;
|
||||
e.mLocusId = locusId;
|
||||
return e;
|
||||
}
|
||||
|
||||
private static UsageEvents.Event createActivityStoppedEvent(long timestamp) {
|
||||
UsageEvents.Event e = createUsageEvent(UsageEvents.Event.ACTIVITY_STOPPED, timestamp);
|
||||
e.mClass = ACTIVITY_NAME;
|
||||
return e;
|
||||
}
|
||||
|
||||
private static UsageEvents.Event createUsageEvent(int eventType, long timestamp) {
|
||||
UsageEvents.Event e = new UsageEvents.Event(eventType, timestamp);
|
||||
e.mPackage = PKG_NAME;
|
||||
return e;
|
||||
}
|
||||
|
||||
private static Event createInAppConversationEvent(long timestamp, int durationSeconds) {
|
||||
return new Event.Builder(timestamp, Event.TYPE_IN_APP_CONVERSATION)
|
||||
.setDurationSeconds(durationSeconds)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class TestConversationStore extends ConversationStore {
|
||||
|
||||
private ConversationInfo mConversationInfo;
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
ConversationInfo getConversation(@Nullable String shortcutId) {
|
||||
return mConversationInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestPackageData extends PackageData {
|
||||
|
||||
private final TestConversationStore mConversationStore = new TestConversationStore();
|
||||
private final TestEventStore mEventStore = new TestEventStore();
|
||||
|
||||
TestPackageData(@NonNull String packageName, @UserIdInt int userId,
|
||||
@NonNull Predicate<String> isDefaultDialerPredicate,
|
||||
@NonNull Predicate<String> isDefaultSmsAppPredicate) {
|
||||
super(packageName, userId, isDefaultDialerPredicate, isDefaultSmsAppPredicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
ConversationStore getConversationStore() {
|
||||
return mConversationStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
EventStore getEventStore() {
|
||||
return mEventStore;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestEventStore extends EventStore {
|
||||
|
||||
private final EventHistoryImpl mShortcutEventHistory = new TestEventHistoryImpl();
|
||||
private final EventHistoryImpl mLocusEventHistory = new TestEventHistoryImpl();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
EventHistoryImpl getOrCreateShortcutEventHistory(String shortcutId) {
|
||||
return mShortcutEventHistory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
EventHistoryImpl getOrCreateLocusEventHistory(LocusId locusId) {
|
||||
return mLocusEventHistory;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestEventHistoryImpl extends EventHistoryImpl {
|
||||
|
||||
private final List<Event> mEvents = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
|
||||
return mEvents;
|
||||
}
|
||||
|
||||
@Override
|
||||
void addEvent(Event event) {
|
||||
mEvents.add(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user