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

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

Merge "People Service data layer main structure"

parents e71999af a1bf86d5
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