diff options
9 files changed, 564 insertions, 127 deletions
diff --git a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java index d825b6b2bd8f..45e0aac24ca7 100644 --- a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java +++ b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java @@ -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; diff --git a/services/people/java/com/android/server/people/data/ConversationStore.java b/services/people/java/com/android/server/people/data/ConversationStore.java index f17e1b91cb5d..364992181f75 100644 --- a/services/people/java/com/android/server/people/data/ConversationStore.java +++ b/services/people/java/com/android/server/people/data/ConversationStore.java @@ -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)); + } } diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 43e773870124..7fdcf42c6364 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -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); @@ -386,36 +383,6 @@ public class DataManager { } @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); } @@ -612,19 +579,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(); + } } } @@ -680,6 +648,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(); } diff --git a/services/people/java/com/android/server/people/data/Event.java b/services/people/java/com/android/server/people/data/Event.java index c2364a295e30..81411c00db51 100644 --- a/services/people/java/com/android/server/people/data/Event.java +++ b/services/people/java/com/android/server/people/data/Event.java @@ -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; } diff --git a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java new file mode 100644 index 000000000000..4e37f47149b4 --- /dev/null +++ b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java @@ -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); + } +} diff --git a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java index 7a16d171b475..a54501029712 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java @@ -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> { diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationStoreTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationStoreTest.java index a40c6ab90197..bbcb54ef8d3e 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ConversationStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationStoreTest.java @@ -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) diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 9d2091a8578e..ad5c57dd11bc 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -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; @@ -312,35 +308,6 @@ public final class DataManagerTest { } @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); mDataManager.onUserUnlocked(USER_ID_SECONDARY); diff --git a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java new file mode 100644 index 000000000000..e4248a04878d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java @@ -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); + } + } +} |