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

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

People Service data layer main structure

Change-Id: I53bf5f1bce3234da7e1dcf8cfdd293120781aef4
Test: Build and run on a test device
Bug: 146522621
parent 6923cdd8
Loading
Loading
Loading
Loading
+24 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.people;

import android.annotation.NonNull;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.AppTarget;
@@ -29,6 +30,7 @@ import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import com.android.server.people.data.DataManager;

import java.util.List;
import java.util.Map;
@@ -41,6 +43,8 @@ public class PeopleService extends SystemService {

    private static final String TAG = "PeopleService";

    private final DataManager mDataManager;

    /**
     * Initializes the system service.
     *
@@ -48,6 +52,15 @@ public class PeopleService extends SystemService {
     */
    public PeopleService(Context context) {
        super(context);

        mDataManager = new DataManager(context);
    }

    @Override
    public void onBootPhase(int phase) {
        if (phase == PHASE_SYSTEM_SERVICES_READY) {
            mDataManager.initialize();
        }
    }

    @Override
@@ -55,6 +68,16 @@ public class PeopleService extends SystemService {
        publishLocalService(PeopleServiceInternal.class, new LocalService());
    }

    @Override
    public void onUnlockUser(@NonNull TargetUser targetUser) {
        mDataManager.onUserUnlocked(targetUser.getUserIdentifier());
    }

    @Override
    public void onStopUser(@NonNull TargetUser targetUser) {
        mDataManager.onUserStopped(targetUser.getUserIdentifier());
    }

    @VisibleForTesting
    final class LocalService extends PeopleServiceInternal {

@@ -63,7 +86,7 @@ public class PeopleService extends SystemService {
        @Override
        public void onCreatePredictionSession(AppPredictionContext context,
                AppPredictionSessionId sessionId) {
            mSessions.put(sessionId, new SessionInfo(context));
            mSessions.put(sessionId, new SessionInfo(context, mDataManager));
        }

        @Override
+3 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Slog;

import com.android.server.people.data.DataManager;
import com.android.server.people.prediction.ConversationPredictor;

import java.util.List;
@@ -37,9 +38,9 @@ class SessionInfo {
    private final RemoteCallbackList<IPredictionCallback> mCallbacks =
            new RemoteCallbackList<>();

    SessionInfo(AppPredictionContext predictionContext) {
    SessionInfo(AppPredictionContext predictionContext, DataManager dataManager) {
        mConversationPredictor = new ConversationPredictor(predictionContext,
                this::updatePredictions);
                this::updatePredictions, dataManager);
    }

    void addCallback(IPredictionCallback callback) {
+95 −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 java.util.ArrayList;
import java.util.List;
import java.util.Set;

/** An {@link EventHistory} that aggregates multiple {@link EventHistory}.  */
class AggregateEventHistoryImpl implements EventHistory {

    private final List<EventHistory> mEventHistoryList = new ArrayList<>();

    @NonNull
    @Override
    public EventIndex getEventIndex(int eventType) {
        for (EventHistory eventHistory : mEventHistoryList) {
            EventIndex eventIndex = eventHistory.getEventIndex(eventType);
            if (!eventIndex.isEmpty()) {
                return eventIndex;
            }
        }
        return EventIndex.EMPTY;
    }

    @NonNull
    @Override
    public EventIndex getEventIndex(Set<Integer> eventTypes) {
        EventIndex merged = new EventIndex();
        for (EventHistory eventHistory : mEventHistoryList) {
            EventIndex eventIndex = eventHistory.getEventIndex(eventTypes);
            if (!eventIndex.isEmpty()) {
                merged = EventIndex.combine(merged, eventIndex);
            }
        }
        return merged;
    }

    @NonNull
    @Override
    public List<Event> queryEvents(Set<Integer> eventTypes, long startTime, long endTime) {
        List<Event> results = new ArrayList<>();
        for (EventHistory eventHistory : mEventHistoryList) {
            EventIndex eventIndex = eventHistory.getEventIndex(eventTypes);
            if (eventIndex.isEmpty()) {
                continue;
            }
            List<Event> queryResults = eventHistory.queryEvents(eventTypes, startTime, endTime);
            results = combineEventLists(results, queryResults);
        }
        return results;
    }

    void addEventHistory(EventHistory eventHistory) {
        mEventHistoryList.add(eventHistory);
    }

    /**
     * Combines the sorted events (in chronological order) from the given 2 lists {@code lhs}
     * and {@code rhs} and preserves the order.
     */
    private List<Event> combineEventLists(List<Event> lhs, List<Event> rhs) {
        List<Event> results = new ArrayList<>();
        int i = 0, j = 0;
        while (i < lhs.size() && j < rhs.size()) {
            if (lhs.get(i).getTimestamp() < rhs.get(j).getTimestamp()) {
                results.add(lhs.get(i++));
            } else {
                results.add(rhs.get(j++));
            }
        }
        if (i < lhs.size()) {
            results.addAll(lhs.subList(i, lhs.size()));
        } else if (j < rhs.size()) {
            results.addAll(rhs.subList(j, rhs.size()));
        }
        return results;
    }
}
+182 −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.Nullable;
import android.annotation.WorkerThread;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Slog;

/** A helper class that queries the Contacts database. */
class ContactsQueryHelper {

    private static final String TAG = "ContactsQueryHelper";

    private final Context mContext;
    private Uri mContactUri;
    private boolean mIsStarred;
    private String mPhoneNumber;
    private long mLastUpdatedTimestamp;

    ContactsQueryHelper(Context context) {
        mContext = context;
    }

    /**
     * Queries the Contacts database with the given contact URI and returns whether the query runs
     * successfully.
     */
    @WorkerThread
    boolean query(@NonNull String contactUri) {
        if (TextUtils.isEmpty(contactUri)) {
            return false;
        }
        Uri uri = Uri.parse(contactUri);
        if ("tel".equals(uri.getScheme())) {
            return queryWithPhoneNumber(uri.getSchemeSpecificPart());
        } else if ("mailto".equals(uri.getScheme())) {
            return queryWithEmail(uri.getSchemeSpecificPart());
        } else if (contactUri.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
            return queryWithUri(uri);
        }
        return false;
    }

    /** Queries the Contacts database and read the most recently updated contact. */
    @WorkerThread
    boolean querySince(long sinceTime) {
        final String[] projection = new String[] {
                Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER,
                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP };
        String selection = Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
        String[] selectionArgs = new String[] {Long.toString(sinceTime)};
        return queryContact(Contacts.CONTENT_URI, projection, selection, selectionArgs);
    }

    @Nullable
    Uri getContactUri() {
        return mContactUri;
    }

    boolean isStarred() {
        return mIsStarred;
    }

    @Nullable
    String getPhoneNumber() {
        return mPhoneNumber;
    }

    long getLastUpdatedTimestamp() {
        return mLastUpdatedTimestamp;
    }

    private boolean queryWithPhoneNumber(String phoneNumber) {
        Uri phoneUri = Uri.withAppendedPath(
                ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
        return queryWithUri(phoneUri);
    }

    private boolean queryWithEmail(String email) {
        Uri emailUri = Uri.withAppendedPath(
                ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(email));
        return queryWithUri(emailUri);
    }

    private boolean queryWithUri(@NonNull Uri uri) {
        final String[] projection = new String[] {
                Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER };
        return queryContact(uri, projection, /* selection= */ null, /* selectionArgs= */ null);
    }

    private boolean queryContact(@NonNull Uri uri, @NonNull String[] projection,
            @Nullable String selection, @Nullable String[] selectionArgs) {
        long contactId;
        String lookupKey = null;
        boolean hasPhoneNumber = false;
        boolean found = false;
        try (Cursor cursor = mContext.getContentResolver().query(
                uri, projection, selection, selectionArgs, /* sortOrder= */ null)) {
            if (cursor == null) {
                Slog.w(TAG, "Cursor is null when querying contact.");
                return false;
            }
            while (cursor.moveToNext()) {
                // Contact ID
                int idIndex = cursor.getColumnIndex(Contacts._ID);
                contactId = cursor.getLong(idIndex);

                // Lookup key
                int lookupKeyIndex = cursor.getColumnIndex(Contacts.LOOKUP_KEY);
                lookupKey = cursor.getString(lookupKeyIndex);

                mContactUri = Contacts.getLookupUri(contactId, lookupKey);

                // Starred
                int starredIndex = cursor.getColumnIndex(Contacts.STARRED);
                mIsStarred = cursor.getInt(starredIndex) != 0;

                // Has phone number
                int hasPhoneNumIndex = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER);
                hasPhoneNumber = cursor.getInt(hasPhoneNumIndex) != 0;

                // Last updated timestamp
                int lastUpdatedTimestampIndex = cursor.getColumnIndex(
                        Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
                if (lastUpdatedTimestampIndex >= 0) {
                    mLastUpdatedTimestamp = cursor.getLong(lastUpdatedTimestampIndex);
                }

                found = true;
            }
        }
        if (found && lookupKey != null && hasPhoneNumber) {
            return queryPhoneNumber(lookupKey);
        }
        return found;
    }

    private boolean queryPhoneNumber(String lookupKey) {
        String[] projection = new String[] {
                ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER };
        String selection = Contacts.LOOKUP_KEY + " = ?";
        String[] selectionArgs = new String[] { lookupKey };
        try (Cursor cursor = mContext.getContentResolver().query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection,
                selectionArgs, /* sortOrder= */ null)) {
            if (cursor == null) {
                Slog.w(TAG, "Cursor is null when querying contact phone number.");
                return false;
            }
            while (cursor.moveToNext()) {
                // Phone number
                int phoneNumIdx = cursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER);
                if (phoneNumIdx >= 0) {
                    mPhoneNumber = cursor.getString(phoneNumIdx);
                }
            }
        }
        return true;
    }
}
+372 −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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri;

import com.android.internal.util.Preconditions;

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

/**
 * Represents a conversation that is provided by the app based on {@link ShortcutInfo}.
 */
public class ConversationInfo {

    private static final int FLAG_VIP = 1;

    private static final int FLAG_NOTIFICATION_SILENCED = 1 << 1;

    private static final int FLAG_BUBBLED = 1 << 2;

    private static final int FLAG_PERSON_IMPORTANT = 1 << 3;

    private static final int FLAG_PERSON_BOT = 1 << 4;

    private static final int FLAG_CONTACT_STARRED = 1 << 5;

    private static final int FLAG_DEMOTED = 1 << 6;

    @IntDef(flag = true, prefix = {"FLAG_"}, value = {
            FLAG_VIP,
            FLAG_NOTIFICATION_SILENCED,
            FLAG_BUBBLED,
            FLAG_PERSON_IMPORTANT,
            FLAG_PERSON_BOT,
            FLAG_CONTACT_STARRED,
            FLAG_DEMOTED,
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface ConversationFlags {
    }

    @NonNull
    private String mShortcutId;

    @Nullable
    private LocusId mLocusId;

    @Nullable
    private Uri mContactUri;

    @Nullable
    private String mContactPhoneNumber;

    @Nullable
    private String mNotificationChannelId;

    @ShortcutFlags
    private int mShortcutFlags;

    @ConversationFlags
    private int mConversationFlags;

    private ConversationInfo(Builder builder) {
        mShortcutId = builder.mShortcutId;
        mLocusId = builder.mLocusId;
        mContactUri = builder.mContactUri;
        mContactPhoneNumber = builder.mContactPhoneNumber;
        mNotificationChannelId = builder.mNotificationChannelId;
        mShortcutFlags = builder.mShortcutFlags;
        mConversationFlags = builder.mConversationFlags;
    }

    @NonNull
    public String getShortcutId() {
        return mShortcutId;
    }

    @Nullable
    LocusId getLocusId() {
        return mLocusId;
    }

    /** The URI to look up the entry in the contacts data provider. */
    @Nullable
    Uri getContactUri() {
        return mContactUri;
    }

    /** The phone number of the associated contact. */
    @Nullable
    String getContactPhoneNumber() {
        return mContactPhoneNumber;
    }

    /**
     * ID of the {@link android.app.NotificationChannel} where the notifications for this
     * conversation are posted.
     */
    @Nullable
    String getNotificationChannelId() {
        return mNotificationChannelId;
    }

    /** Whether the shortcut for this conversation is set long-lived by the app. */
    public boolean isShortcutLongLived() {
        return hasShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED);
    }

    /** Whether this conversation is marked as VIP by the user. */
    public boolean isVip() {
        return hasConversationFlags(FLAG_VIP);
    }

    /** Whether the notifications for this conversation should be silenced. */
    public boolean isNotificationSilenced() {
        return hasConversationFlags(FLAG_NOTIFICATION_SILENCED);
    }

    /** Whether the notifications for this conversation should show in bubbles. */
    public boolean isBubbled() {
        return hasConversationFlags(FLAG_BUBBLED);
    }

    /**
     * Whether this conversation is demoted by the user. New notifications for the demoted
     * conversation will not show in the conversation space.
     */
    public boolean isDemoted() {
        return hasConversationFlags(FLAG_DEMOTED);
    }

    /** Whether the associated person is marked as important by the app. */
    public boolean isPersonImportant() {
        return hasConversationFlags(FLAG_PERSON_IMPORTANT);
    }

    /** Whether the associated person is marked as a bot by the app. */
    public boolean isPersonBot() {
        return hasConversationFlags(FLAG_PERSON_BOT);
    }

    /** Whether the associated contact is marked as starred by the user. */
    public boolean isContactStarred() {
        return hasConversationFlags(FLAG_CONTACT_STARRED);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof ConversationInfo)) {
            return false;
        }
        ConversationInfo other = (ConversationInfo) obj;
        return Objects.equals(mShortcutId, other.mShortcutId)
                && Objects.equals(mLocusId, other.mLocusId)
                && Objects.equals(mContactUri, other.mContactUri)
                && Objects.equals(mContactPhoneNumber, other.mContactPhoneNumber)
                && Objects.equals(mNotificationChannelId, other.mNotificationChannelId)
                && mShortcutFlags == other.mShortcutFlags
                && mConversationFlags == other.mConversationFlags;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
                mNotificationChannelId, mShortcutFlags, mConversationFlags);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("ConversationInfo {");
        sb.append("shortcutId=").append(mShortcutId);
        sb.append(", locusId=").append(mLocusId);
        sb.append(", contactUri=").append(mContactUri);
        sb.append(", phoneNumber=").append(mContactPhoneNumber);
        sb.append(", notificationChannelId=").append(mNotificationChannelId);
        sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
        sb.append(" [");
        if (isShortcutLongLived()) {
            sb.append("Liv");
        }
        sb.append("]");
        sb.append(", conversationFlags=0x").append(Integer.toHexString(mConversationFlags));
        sb.append(" [");
        if (isVip()) {
            sb.append("Vip");
        }
        if (isNotificationSilenced()) {
            sb.append("Sil");
        }
        if (isBubbled()) {
            sb.append("Bub");
        }
        if (isDemoted()) {
            sb.append("Dem");
        }
        if (isPersonImportant()) {
            sb.append("Imp");
        }
        if (isPersonBot()) {
            sb.append("Bot");
        }
        if (isContactStarred()) {
            sb.append("Sta");
        }
        sb.append("]}");
        return sb.toString();
    }

    private boolean hasShortcutFlags(@ShortcutFlags int flags) {
        return (mShortcutFlags & flags) == flags;
    }

    private boolean hasConversationFlags(@ConversationFlags int flags) {
        return (mConversationFlags & flags) == flags;
    }

    /**
     * Builder class for {@link ConversationInfo} objects.
     */
    static class Builder {

        private String mShortcutId;

        @Nullable
        private LocusId mLocusId;

        @Nullable
        private Uri mContactUri;

        @Nullable
        private String mContactPhoneNumber;

        @Nullable
        private String mNotificationChannelId;

        @ShortcutFlags
        private int mShortcutFlags;

        @ConversationFlags
        private int mConversationFlags;

        Builder() {
        }

        Builder(@NonNull ConversationInfo conversationInfo) {
            if (mShortcutId == null) {
                mShortcutId = conversationInfo.mShortcutId;
            } else {
                Preconditions.checkArgument(mShortcutId.equals(conversationInfo.mShortcutId));
            }
            mLocusId = conversationInfo.mLocusId;
            mContactUri = conversationInfo.mContactUri;
            mContactPhoneNumber = conversationInfo.mContactPhoneNumber;
            mNotificationChannelId = conversationInfo.mNotificationChannelId;
            mShortcutFlags = conversationInfo.mShortcutFlags;
            mConversationFlags = conversationInfo.mConversationFlags;
        }

        Builder setShortcutId(@NonNull String shortcutId) {
            mShortcutId = shortcutId;
            return this;
        }

        Builder setLocusId(LocusId locusId) {
            mLocusId = locusId;
            return this;
        }

        Builder setContactUri(Uri contactUri) {
            mContactUri = contactUri;
            return this;
        }

        Builder setContactPhoneNumber(String phoneNumber) {
            mContactPhoneNumber = phoneNumber;
            return this;
        }

        Builder setNotificationChannelId(String notificationChannelId) {
            mNotificationChannelId = notificationChannelId;
            return this;
        }

        Builder setShortcutFlags(@ShortcutFlags int shortcutFlags) {
            mShortcutFlags = shortcutFlags;
            return this;
        }

        Builder setConversationFlags(@ConversationFlags int conversationFlags) {
            mConversationFlags = conversationFlags;
            return this;
        }

        Builder setVip(boolean value) {
            return setConversationFlag(FLAG_VIP, value);
        }

        Builder setNotificationSilenced(boolean value) {
            return setConversationFlag(FLAG_NOTIFICATION_SILENCED, value);
        }

        Builder setBubbled(boolean value) {
            return setConversationFlag(FLAG_BUBBLED, value);
        }

        Builder setDemoted(boolean value) {
            return setConversationFlag(FLAG_DEMOTED, value);
        }

        Builder setPersonImportant(boolean value) {
            return setConversationFlag(FLAG_PERSON_IMPORTANT, value);
        }

        Builder setPersonBot(boolean value) {
            return setConversationFlag(FLAG_PERSON_BOT, value);
        }

        Builder setContactStarred(boolean value) {
            return setConversationFlag(FLAG_CONTACT_STARRED, value);
        }

        private Builder setConversationFlag(@ConversationFlags int flags, boolean value) {
            if (value) {
                return addConversationFlags(flags);
            } else {
                return removeConversationFlags(flags);
            }
        }

        private Builder addConversationFlags(@ConversationFlags int flags) {
            mConversationFlags |= flags;
            return this;
        }

        private Builder removeConversationFlags(@ConversationFlags int flags) {
            mConversationFlags &= ~flags;
            return this;
        }

        ConversationInfo build() {
            Objects.requireNonNull(mShortcutId);
            return new ConversationInfo(this);
        }
    }
}
Loading