Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b6016298 authored by Danning Chen's avatar Danning Chen
Browse files

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
parent 098584d2
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -107,7 +107,7 @@ class CallLogQueryHelper {
        }
        }
        @Event.EventType int eventType  = CALL_TYPE_TO_EVENT_TYPE.get(callType);
        @Event.EventType int eventType  = CALL_TYPE_TO_EVENT_TYPE.get(callType);
        Event event = new Event.Builder(date, eventType)
        Event event = new Event.Builder(date, eventType)
                .setCallDetails(new Event.CallDetails(durationSeconds))
                .setDurationSeconds((int) durationSeconds)
                .build();
                .build();
        mEventConsumer.accept(phoneNumber, event);
        mEventConsumer.accept(phoneNumber, event);
        return true;
        return true;
+18 −0
Original line number Original line Diff line number Diff line
@@ -40,6 +40,9 @@ class ConversationStore {
    // Phone Number -> Shortcut ID
    // Phone Number -> Shortcut ID
    private final Map<String, String> mPhoneNumberToShortcutIdMap = new ArrayMap<>();
    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) {
    void addOrUpdate(@NonNull ConversationInfo conversationInfo) {
        mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo);
        mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo);


@@ -57,6 +60,11 @@ class ConversationStore {
        if (phoneNumber != null) {
        if (phoneNumber != null) {
            mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId());
            mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId());
        }
        }

        String notifChannelId = conversationInfo.getNotificationChannelId();
        if (notifChannelId != null) {
            mNotifChannelIdToShortcutIdMap.put(notifChannelId, conversationInfo.getShortcutId());
        }
    }
    }


    void deleteConversation(@NonNull String shortcutId) {
    void deleteConversation(@NonNull String shortcutId) {
@@ -79,6 +87,11 @@ class ConversationStore {
        if (phoneNumber != null) {
        if (phoneNumber != null) {
            mPhoneNumberToShortcutIdMap.remove(phoneNumber);
            mPhoneNumberToShortcutIdMap.remove(phoneNumber);
        }
        }

        String notifChannelId = conversationInfo.getNotificationChannelId();
        if (notifChannelId != null) {
            mNotifChannelIdToShortcutIdMap.remove(notifChannelId);
        }
    }
    }


    void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
    void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
@@ -106,4 +119,9 @@ class ConversationStore {
    ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) {
    ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) {
        return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber));
        return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber));
    }
    }

    @Nullable
    ConversationInfo getConversationByNotificationChannelId(@NonNull String notifChannelId) {
        return getConversation(mNotifChannelIdToShortcutIdMap.get(notifChannelId));
    }
}
}
+14 −41
Original line number Original line Diff line number Diff line
@@ -24,8 +24,6 @@ import android.app.Notification;
import android.app.Person;
import android.app.Person;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetEvent;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ComponentName;
import android.content.Context;
import android.content.Context;
@@ -69,6 +67,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
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
 * 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 final ContentObserver mMmsSmsContentObserver;


    private ShortcutServiceInternal mShortcutServiceInternal;
    private ShortcutServiceInternal mShortcutServiceInternal;
    private UsageStatsManagerInternal mUsageStatsManagerInternal;
    private ShortcutManager mShortcutManager;
    private ShortcutManager mShortcutManager;
    private UserManager mUserManager;
    private UserManager mUserManager;


@@ -118,7 +116,6 @@ public class DataManager {
    /** Initialization. Called when the system services are up running. */
    /** Initialization. Called when the system services are up running. */
    public void initialize() {
    public void initialize() {
        mShortcutServiceInternal = LocalServices.getService(ShortcutServiceInternal.class);
        mShortcutServiceInternal = LocalServices.getService(ShortcutServiceInternal.class);
        mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
        mShortcutManager = mContext.getSystemService(ShortcutManager.class);
        mShortcutManager = mContext.getSystemService(ShortcutManager.class);
        mUserManager = mContext.getSystemService(UserManager.class);
        mUserManager = mContext.getSystemService(UserManager.class);


@@ -384,36 +381,6 @@ public class DataManager {
        conversationStore.addOrUpdate(builder.build());
        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
    @VisibleForTesting
    ContentObserver getContactsContentObserverForTesting(@UserIdInt int userId) {
    ContentObserver getContactsContentObserverForTesting(@UserIdInt int userId) {
        return mContactsContentObservers.get(userId);
        return mContactsContentObservers.get(userId);
@@ -611,19 +578,20 @@ public class DataManager {
     */
     */
    private class UsageStatsQueryRunnable implements Runnable {
    private class UsageStatsQueryRunnable implements Runnable {


        private final int mUserId;
        private final UsageStatsQueryHelper mUsageStatsQueryHelper;
        private long mLastQueryTime;
        private long mLastEventTimestamp;


        private UsageStatsQueryRunnable(int userId) {
        private UsageStatsQueryRunnable(int userId) {
            mUserId = userId;
            mUsageStatsQueryHelper = mInjector.createUsageStatsQueryHelper(userId,
            mLastQueryTime = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
                    (packageName) -> getPackage(packageName, userId));
            mLastEventTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
        }
        }


        @Override
        @Override
        public void run() {
        public void run() {
            long currentTime = System.currentTimeMillis();
            if (mUsageStatsQueryHelper.querySince(mLastEventTimestamp)) {
            queryUsageStatsService(mUserId, currentTime, mLastQueryTime);
                mLastEventTimestamp = mUsageStatsQueryHelper.getLastEventTimestamp();
            mLastQueryTime = currentTime;
            }
        }
        }
    }
    }


@@ -679,6 +647,11 @@ public class DataManager {
            return new SmsQueryHelper(context, eventConsumer);
            return new SmsQueryHelper(context, eventConsumer);
        }
        }


        UsageStatsQueryHelper createUsageStatsQueryHelper(@UserIdInt int userId,
                Function<String, PackageData> packageDataGetter) {
            return new UsageStatsQueryHelper(userId, packageDataGetter);
        }

        int getCallingUserId() {
        int getCallingUserId() {
            return Binder.getCallingUserHandle().getIdentifier();
            return Binder.getCallingUserHandle().getIdentifier();
        }
        }
+41 −36
Original line number Original line Diff line number Diff line
@@ -18,14 +18,12 @@ package com.android.server.people.data;


import android.annotation.IntDef;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.format.DateFormat;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.util.ArraySet;


import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.Set;
import java.util.Set;


/** An event representing the interaction with a specific conversation or app. */
/** 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_CALL_MISSED = 12;


    public static final int TYPE_IN_APP_CONVERSATION = 13;

    @IntDef(prefix = { "TYPE_" }, value = {
    @IntDef(prefix = { "TYPE_" }, value = {
            TYPE_SHORTCUT_INVOCATION,
            TYPE_SHORTCUT_INVOCATION,
            TYPE_NOTIFICATION_POSTED,
            TYPE_NOTIFICATION_POSTED,
@@ -68,6 +68,7 @@ public class Event {
            TYPE_CALL_OUTGOING,
            TYPE_CALL_OUTGOING,
            TYPE_CALL_INCOMING,
            TYPE_CALL_INCOMING,
            TYPE_CALL_MISSED,
            TYPE_CALL_MISSED,
            TYPE_IN_APP_CONVERSATION,
    })
    })
    @Retention(RetentionPolicy.SOURCE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface EventType {}
    public @interface EventType {}
@@ -95,6 +96,7 @@ public class Event {
        CALL_EVENT_TYPES.add(TYPE_CALL_MISSED);
        CALL_EVENT_TYPES.add(TYPE_CALL_MISSED);


        ALL_EVENT_TYPES.add(TYPE_SHORTCUT_INVOCATION);
        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(NOTIFICATION_EVENT_TYPES);
        ALL_EVENT_TYPES.addAll(SHARE_EVENT_TYPES);
        ALL_EVENT_TYPES.addAll(SHARE_EVENT_TYPES);
        ALL_EVENT_TYPES.addAll(SMS_EVENT_TYPES);
        ALL_EVENT_TYPES.addAll(SMS_EVENT_TYPES);
@@ -105,18 +107,18 @@ public class Event {


    private final int mType;
    private final int mType;


    private final CallDetails mCallDetails;
    private final int mDurationSeconds;


    Event(long timestamp, @EventType int type) {
    Event(long timestamp, @EventType int type) {
        mTimestamp = timestamp;
        mTimestamp = timestamp;
        mType = type;
        mType = type;
        mCallDetails = null;
        mDurationSeconds = 0;
    }
    }


    private Event(@NonNull Builder builder) {
    private Event(@NonNull Builder builder) {
        mTimestamp = builder.mTimestamp;
        mTimestamp = builder.mTimestamp;
        mType = builder.mType;
        mType = builder.mType;
        mCallDetails = builder.mCallDetails;
        mDurationSeconds = builder.mDurationSeconds;
    }
    }


    public long getTimestamp() {
    public long getTimestamp() {
@@ -128,44 +130,48 @@ public class Event {
    }
    }


    /**
    /**
     * Gets the {@link CallDetails} of the event. It is only available if the event type is one of
     * Gets the duration of the event in seconds. It is only available for these events:
     * {@code CALL_EVENT_TYPES}, otherwise, it's always {@code null}.
     * <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 int getDurationSeconds() {
    public CallDetails getCallDetails() {
        return mDurationSeconds;
        return mCallDetails;
    }
    }


    @Override
    @Override
    public String toString() {
    public boolean equals(Object obj) {
        StringBuilder sb = new StringBuilder();
        if (this == obj) {
        sb.append("Event {");
            return true;
        sb.append("timestamp=").append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimestamp));
        sb.append(", type=").append(mType);
        if (mCallDetails != null) {
            sb.append(", callDetails=").append(mCallDetails);
        }
        }
        sb.append("}");
        if (!(obj instanceof Event)) {
        return sb.toString();
            return false;
        }
        }

        Event other = (Event) obj;
    /** Type-specific details of a call event. */
        return mTimestamp == other.mTimestamp
    public static class CallDetails {
                && mType == other.mType

                && mDurationSeconds == other.mDurationSeconds;
        private final long mDurationSeconds;

        CallDetails(long durationSeconds) {
            mDurationSeconds = durationSeconds;
    }
    }


        public long getDurationSeconds() {
    @Override
            return mDurationSeconds;
    public int hashCode() {
        return Objects.hash(mTimestamp, mType, mDurationSeconds);
    }
    }


    @Override
    @Override
    public String toString() {
    public String toString() {
            return "CallDetails {durationSeconds=" + mDurationSeconds + "}";
        StringBuilder sb = new StringBuilder();
        sb.append("Event {");
        sb.append("timestamp=").append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimestamp));
        sb.append(", type=").append(mType);
        if (mDurationSeconds > 0) {
            sb.append(", durationSeconds=").append(mDurationSeconds);
        }
        }
        sb.append("}");
        return sb.toString();
    }
    }


    /** Builder class for {@link Event} objects. */
    /** Builder class for {@link Event} objects. */
@@ -175,16 +181,15 @@ public class Event {


        private final int mType;
        private final int mType;


        private CallDetails mCallDetails;
        private int mDurationSeconds;


        Builder(long timestamp, @EventType int type) {
        Builder(long timestamp, @EventType int type) {
            mTimestamp = timestamp;
            mTimestamp = timestamp;
            mType = type;
            mType = type;
        }
        }


        Builder setCallDetails(CallDetails callDetails) {
        Builder setDurationSeconds(int durationSeconds) {
            Preconditions.checkArgument(CALL_EVENT_TYPES.contains(mType));
            mDurationSeconds = durationSeconds;
            mCallDetails = callDetails;
            return this;
            return this;
        }
        }


+158 −0
Original line number Original line Diff line number Diff line
/*
 * 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);
    }
}
Loading