Loading services/people/java/com/android/server/people/PeopleService.java +24 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -41,6 +43,8 @@ public class PeopleService extends SystemService { private static final String TAG = "PeopleService"; private final DataManager mDataManager; /** * Initializes the system service. * Loading @@ -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 Loading @@ -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 { Loading @@ -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 Loading services/people/java/com/android/server/people/SessionInfo.java +3 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading services/people/java/com/android/server/people/data/AggregateEventHistoryImpl.java 0 → 100644 +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; } } services/people/java/com/android/server/people/data/ContactsQueryHelper.java 0 → 100644 +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; } } services/people/java/com/android/server/people/data/ConversationInfo.java 0 → 100644 +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
services/people/java/com/android/server/people/PeopleService.java +24 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -41,6 +43,8 @@ public class PeopleService extends SystemService { private static final String TAG = "PeopleService"; private final DataManager mDataManager; /** * Initializes the system service. * Loading @@ -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 Loading @@ -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 { Loading @@ -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 Loading
services/people/java/com/android/server/people/SessionInfo.java +3 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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) { Loading
services/people/java/com/android/server/people/data/AggregateEventHistoryImpl.java 0 → 100644 +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; } }
services/people/java/com/android/server/people/data/ContactsQueryHelper.java 0 → 100644 +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; } }
services/people/java/com/android/server/people/data/ConversationInfo.java 0 → 100644 +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); } } }