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

Commit e4294d62 authored by Danning Chen's avatar Danning Chen Committed by Android (Google) Code Review
Browse files

Merge "Add UsageStatsQueryHelper to People Service"

parents e2896f65 b6016298
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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;
+18 −0
Original line number Diff line number Diff line
@@ -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));
    }
}
+14 −41
Original line number Diff line number Diff line
@@ -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);

@@ -385,36 +382,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);
@@ -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();
        }
+41 −36
Original line number Diff line number Diff line
@@ -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,44 +130,48 @@ 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 String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Event {");
        sb.append("timestamp=").append(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimestamp));
        sb.append(", type=").append(mType);
        if (mCallDetails != null) {
            sb.append(", callDetails=").append(mCallDetails);
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        sb.append("}");
        return sb.toString();
        if (!(obj instanceof Event)) {
            return false;
        }

    /** Type-specific details of a call event. */
    public static class CallDetails {

        private final long mDurationSeconds;

        CallDetails(long durationSeconds) {
            mDurationSeconds = durationSeconds;
        Event other = (Event) obj;
        return mTimestamp == other.mTimestamp
                && mType == other.mType
                && mDurationSeconds == other.mDurationSeconds;
    }

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

    @Override
    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. */
@@ -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;
        }

+158 −0
Original line number 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