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:
Danning Chen
2020-02-03 16:04:56 -08:00
parent 098584d234
commit b601629811
9 changed files with 564 additions and 127 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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