Add a status API for conversations
Status are transient pices of information about the current
activities or availability of someone in a conversation. They
may be shown in places where conversations are shown.
Test: atest, cts
Bug: 163617224
Change-Id: I4b61bc3b7f338e9c8cae2c6142622a7040547ddb
diff --git a/core/api/current.txt b/core/api/current.txt
index 4ad5e49..b1d4916 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7826,6 +7826,52 @@
}
+package android.app.people {
+
+ public final class ConversationStatus implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getActivity();
+ method public int getAvailability();
+ method @Nullable public CharSequence getDescription();
+ method public long getEndTimeMillis();
+ method @Nullable public android.graphics.drawable.Icon getIcon();
+ method @NonNull public String getId();
+ method public long getStartTimeMillis();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int ACTIVITY_ANNIVERSARY = 2; // 0x2
+ field public static final int ACTIVITY_BIRTHDAY = 1; // 0x1
+ field public static final int ACTIVITY_GAME = 5; // 0x5
+ field public static final int ACTIVITY_LOCATION = 6; // 0x6
+ field public static final int ACTIVITY_MEDIA = 4; // 0x4
+ field public static final int ACTIVITY_NEW_STORY = 3; // 0x3
+ field public static final int ACTIVITY_OTHER = 0; // 0x0
+ field public static final int ACTIVITY_UPCOMING_BIRTHDAY = 7; // 0x7
+ field public static final int AVAILABILITY_AVAILABLE = 0; // 0x0
+ field public static final int AVAILABILITY_BUSY = 1; // 0x1
+ field public static final int AVAILABILITY_OFFLINE = 2; // 0x2
+ field public static final int AVAILABILITY_UNKNOWN = -1; // 0xffffffff
+ field @NonNull public static final android.os.Parcelable.Creator<android.app.people.ConversationStatus> CREATOR;
+ }
+
+ public static final class ConversationStatus.Builder {
+ ctor public ConversationStatus.Builder(@NonNull String, @NonNull int);
+ method @NonNull public android.app.people.ConversationStatus build();
+ method @NonNull public android.app.people.ConversationStatus.Builder setAvailability(int);
+ method @NonNull public android.app.people.ConversationStatus.Builder setDescription(@Nullable CharSequence);
+ method @NonNull public android.app.people.ConversationStatus.Builder setEndTimeMillis(long);
+ method @NonNull public android.app.people.ConversationStatus.Builder setIcon(@Nullable android.graphics.drawable.Icon);
+ method @NonNull public android.app.people.ConversationStatus.Builder setStartTimeMillis(long);
+ }
+
+ public final class PeopleManager {
+ method public void addOrUpdateStatus(@NonNull String, @NonNull android.app.people.ConversationStatus);
+ method public void clearStatus(@NonNull String, @NonNull String);
+ method public void clearStatuses(@NonNull String);
+ method @NonNull public java.util.List<android.app.people.ConversationStatus> getStatuses(@NonNull String);
+ }
+
+}
+
package android.app.role {
public final class RoleManager {
@@ -10315,6 +10361,7 @@
field public static final String NFC_SERVICE = "nfc";
field public static final String NOTIFICATION_SERVICE = "notification";
field public static final String NSD_SERVICE = "servicediscovery";
+ field public static final String PEOPLE_SERVICE = "people";
field public static final String POWER_SERVICE = "power";
field public static final String PRINT_SERVICE = "print";
field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index ae1c894..050d194 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -29,6 +29,7 @@
import android.app.contentsuggestions.ContentSuggestionsManager;
import android.app.contentsuggestions.IContentSuggestionsManager;
import android.app.job.JobSchedulerFrameworkInitializer;
+import android.app.people.PeopleManager;
import android.app.prediction.AppPredictionManager;
import android.app.role.RoleControllerManager;
import android.app.role.RoleManager;
@@ -586,6 +587,13 @@
return new NsdManager(ctx.getOuterContext(), service);
}});
+ registerService(Context.PEOPLE_SERVICE, PeopleManager.class,
+ new CachedServiceFetcher<PeopleManager>() {
+ @Override
+ public PeopleManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ return new PeopleManager(ctx);
+ }});
+
registerService(Context.POWER_SERVICE, PowerManager.class,
new CachedServiceFetcher<PowerManager>() {
@Override
diff --git a/core/java/android/app/people/ConversationStatus.aidl b/core/java/android/app/people/ConversationStatus.aidl
new file mode 100644
index 0000000..acfe135
--- /dev/null
+++ b/core/java/android/app/people/ConversationStatus.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2021, 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 android.app.people;
+
+parcelable ConversationStatus;
\ No newline at end of file
diff --git a/core/java/android/app/people/ConversationStatus.java b/core/java/android/app/people/ConversationStatus.java
new file mode 100644
index 0000000..d2a0255
--- /dev/null
+++ b/core/java/android/app/people/ConversationStatus.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2021 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 android.app.people;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+public final class ConversationStatus implements Parcelable {
+ private static final String TAG = "ConversationStatus";
+
+ /** @hide */
+ @IntDef(prefix = { "ACTIVITY_" }, value = {
+ ACTIVITY_OTHER,
+ ACTIVITY_BIRTHDAY,
+ ACTIVITY_ANNIVERSARY,
+ ACTIVITY_NEW_STORY,
+ ACTIVITY_MEDIA,
+ ACTIVITY_GAME,
+ ACTIVITY_LOCATION,
+ ACTIVITY_UPCOMING_BIRTHDAY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActivityType {}
+
+ public static final int ACTIVITY_OTHER = 0;
+ public static final int ACTIVITY_BIRTHDAY = 1;
+ public static final int ACTIVITY_ANNIVERSARY = 2;
+ public static final int ACTIVITY_NEW_STORY = 3;
+ public static final int ACTIVITY_MEDIA = 4;
+ public static final int ACTIVITY_GAME = 5;
+ public static final int ACTIVITY_LOCATION = 6;
+ public static final int ACTIVITY_UPCOMING_BIRTHDAY = 7;
+
+ /** @hide */
+ @IntDef(prefix = { "AVAILABILITY_" }, value = {
+ AVAILABILITY_UNKNOWN,
+ AVAILABILITY_AVAILABLE,
+ AVAILABILITY_BUSY,
+ AVAILABILITY_OFFLINE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Availability {}
+
+ public static final int AVAILABILITY_UNKNOWN = -1;
+ public static final int AVAILABILITY_AVAILABLE = 0;
+ public static final int AVAILABILITY_BUSY = 1;
+ public static final int AVAILABILITY_OFFLINE = 2;
+
+ private final String mId;
+ private final int mActivity;
+
+ private int mAvailability;
+ private CharSequence mDescription;
+ private Icon mIcon;
+ private long mStartTimeMs;
+ private long mEndTimeMs;
+
+ private ConversationStatus(Builder b) {
+ mId = b.mId;
+ mActivity = b.mActivity;
+ mAvailability = b.mAvailability;
+ mDescription = b.mDescription;
+ mIcon = b.mIcon;
+ mStartTimeMs = b.mStartTimeMs;
+ mEndTimeMs = b.mEndTimeMs;
+ }
+
+ private ConversationStatus(Parcel p) {
+ mId = p.readString();
+ mActivity = p.readInt();
+ mAvailability = p.readInt();
+ mDescription = p.readCharSequence();
+ mIcon = p.readParcelable(Icon.class.getClassLoader());
+ mStartTimeMs = p.readLong();
+ mEndTimeMs = p.readLong();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeInt(mActivity);
+ dest.writeInt(mAvailability);
+ dest.writeCharSequence(mDescription);
+ dest.writeParcelable(mIcon, flags);
+ dest.writeLong(mStartTimeMs);
+ dest.writeLong(mEndTimeMs);
+ }
+
+ public @NonNull String getId() {
+ return mId;
+ }
+
+ public @ActivityType int getActivity() {
+ return mActivity;
+ }
+
+ public @Availability
+ int getAvailability() {
+ return mAvailability;
+ }
+
+ public @Nullable
+ CharSequence getDescription() {
+ return mDescription;
+ }
+
+ public @Nullable Icon getIcon() {
+ return mIcon;
+ }
+
+ public long getStartTimeMillis() {
+ return mStartTimeMs;
+ }
+
+ public long getEndTimeMillis() {
+ return mEndTimeMs;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ConversationStatus that = (ConversationStatus) o;
+ return mActivity == that.mActivity &&
+ mAvailability == that.mAvailability &&
+ mStartTimeMs == that.mStartTimeMs &&
+ mEndTimeMs == that.mEndTimeMs &&
+ mId.equals(that.mId) &&
+ Objects.equals(mDescription, that.mDescription) &&
+ Objects.equals(mIcon, that.mIcon);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mActivity, mAvailability, mDescription, mIcon, mStartTimeMs,
+ mEndTimeMs);
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationStatus{" +
+ "mId='" + mId + '\'' +
+ ", mActivity=" + mActivity +
+ ", mAvailability=" + mAvailability +
+ ", mDescription=" + mDescription +
+ ", mIcon=" + mIcon +
+ ", mStartTimeMs=" + mStartTimeMs +
+ ", mEndTimeMs=" + mEndTimeMs +
+ '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<ConversationStatus> CREATOR
+ = new Creator<ConversationStatus>() {
+ public ConversationStatus createFromParcel(Parcel source) {
+ return new ConversationStatus(source);
+ }
+
+ public ConversationStatus[] newArray(int size) {
+ return new ConversationStatus[size];
+ }
+ };
+
+ public static final class Builder {
+ final String mId;
+ final int mActivity;
+ int mAvailability = AVAILABILITY_UNKNOWN;
+ CharSequence mDescription;
+ Icon mIcon;
+ long mStartTimeMs = -1;
+ long mEndTimeMs = -1;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param id The unique id for this status
+ * @param activity The type of status
+ */
+ public Builder(@NonNull String id, @ActivityType @NonNull int activity) {
+ mId = id;
+ mActivity = activity;
+ }
+
+
+ public @NonNull Builder setAvailability(@Availability int availability) {
+ mAvailability = availability;
+ return this;
+ }
+
+ public @NonNull Builder setDescription(@Nullable CharSequence description) {
+ mDescription = description;
+ return this;
+ }
+
+ public @NonNull Builder setIcon(@Nullable Icon icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ public @NonNull Builder setStartTimeMillis(long startTimeMs) {
+ mStartTimeMs = startTimeMs;
+ return this;
+ }
+
+ public @NonNull Builder setEndTimeMillis(long endTimeMs) {
+ mEndTimeMs = endTimeMs;
+ return this;
+ }
+
+ public @NonNull ConversationStatus build() {
+ return new ConversationStatus(this);
+ }
+ }
+}
diff --git a/core/java/android/app/people/IPeopleManager.aidl b/core/java/android/app/people/IPeopleManager.aidl
index c547ef1..0d12ed0 100644
--- a/core/java/android/app/people/IPeopleManager.aidl
+++ b/core/java/android/app/people/IPeopleManager.aidl
@@ -16,6 +16,7 @@
package android.app.people;
+import android.app.people.ConversationStatus;
import android.content.pm.ParceledListSlice;
import android.net.Uri;
import android.os.IBinder;
@@ -45,4 +46,9 @@
* conversation can't be found or no interactions have been recorded, returns 0L.
*/
long getLastInteraction(in String packageName, int userId, in String shortcutId);
+
+ void addOrUpdateStatus(in String packageName, int userId, in String conversationId, in ConversationStatus status);
+ void clearStatus(in String packageName, int userId, in String conversationId, in String statusId);
+ void clearStatuses(in String packageName, int userId, in String conversationId);
+ ParceledListSlice getStatuses(in String packageName, int userId, in String conversationId);
}
diff --git a/core/java/android/app/people/PeopleManager.java b/core/java/android/app/people/PeopleManager.java
new file mode 100644
index 0000000..de7ba62
--- /dev/null
+++ b/core/java/android/app/people/PeopleManager.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 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 android.app.people;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.ShortcutInfo;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class allows interaction with conversation and people data.
+ */
+@SystemService(Context.PEOPLE_SERVICE)
+public final class PeopleManager {
+
+ private static final String LOG_TAG = PeopleManager.class.getSimpleName();
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final IPeopleManager mService;
+
+ /**
+ * @hide
+ */
+ public PeopleManager(@NonNull Context context) throws ServiceManager.ServiceNotFoundException {
+ mContext = context;
+ mService = IPeopleManager.Stub.asInterface(ServiceManager.getServiceOrThrow(
+ Context.PEOPLE_SERVICE));
+ }
+
+
+ /**
+ * Sets or updates a {@link ConversationStatus} for a conversation.
+ *
+ * <p>Statuses are meant to represent current information about the conversation. Like
+ * notifications, they are transient and are not persisted beyond a reboot, nor are they
+ * backed up and restored.</p>
+ * <p>If the provided conversation shortcut is not already pinned, or cached by the system,
+ * it will remain cached as long as the status is active.</p>
+ *
+ * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the
+ * conversation that has an active status
+ * @param status the current status for the given conversation
+ *
+ * @return whether the role is available in the system
+ */
+ public void addOrUpdateStatus(@NonNull String conversationId,
+ @NonNull ConversationStatus status) {
+ Preconditions.checkStringNotEmpty(conversationId);
+ Objects.requireNonNull(status);
+ try {
+ mService.addOrUpdateStatus(
+ mContext.getPackageName(), mContext.getUserId(), conversationId, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unpublishes a given status from the given conversation.
+ *
+ * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the
+ * conversation that has an active status
+ * @param statusId the {@link ConversationStatus#getId() id} of a published status for the given
+ * conversation
+ */
+ public void clearStatus(@NonNull String conversationId, @NonNull String statusId) {
+ Preconditions.checkStringNotEmpty(conversationId);
+ Preconditions.checkStringNotEmpty(statusId);
+ try {
+ mService.clearStatus(
+ mContext.getPackageName(), mContext.getUserId(), conversationId, statusId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes all published statuses for the given conversation.
+ *
+ * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the
+ * conversation that has one or more active statuses
+ */
+ public void clearStatuses(@NonNull String conversationId) {
+ Preconditions.checkStringNotEmpty(conversationId);
+ try {
+ mService.clearStatuses(
+ mContext.getPackageName(), mContext.getUserId(), conversationId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all of the currently published statuses for a given conversation.
+ *
+ * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the
+ * conversation that has one or more active statuses
+ */
+ public @NonNull List<ConversationStatus> getStatuses(@NonNull String conversationId) {
+ try {
+ final ParceledListSlice<ConversationStatus> parceledList
+ = mService.getStatuses(
+ mContext.getPackageName(), mContext.getUserId(), conversationId);
+ if (parceledList != null) {
+ return parceledList.getList();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return new ArrayList<>();
+ }
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 29ffa0b..43011fc 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -40,6 +40,7 @@
import android.app.IApplicationThread;
import android.app.IServiceConnection;
import android.app.VrManager;
+import android.app.people.PeopleManager;
import android.app.time.TimeManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ApplicationInfo;
@@ -5311,10 +5312,10 @@
public static final String SMS_SERVICE = "sms";
/**
- * Use with {@link #getSystemService(String)} to access people service.
+ * Use with {@link #getSystemService(String)} to access a {@link PeopleManager} to interact
+ * with your published conversations.
*
* @see #getSystemService(String)
- * @hide
*/
public static final String PEOPLE_SERVICE = "people";
diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java
index 49a41f0..16b9165 100644
--- a/services/people/java/com/android/server/people/PeopleService.java
+++ b/services/people/java/com/android/server/people/PeopleService.java
@@ -19,7 +19,9 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.people.ConversationChannel;
+import android.app.people.ConversationStatus;
import android.app.people.IPeopleManager;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
@@ -27,6 +29,7 @@
import android.app.prediction.AppTargetEvent;
import android.app.prediction.IPredictionCallback;
import android.content.Context;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
import android.os.CancellationSignal;
@@ -38,9 +41,11 @@
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.people.data.DataManager;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -54,6 +59,8 @@
private final DataManager mDataManager;
+ private PackageManagerInternal mPackageManagerInternal;
+
/**
* Initializes the system service.
*
@@ -83,6 +90,7 @@
publishBinderService(Context.PEOPLE_SERVICE, mService);
}
publishLocalService(PeopleServiceInternal.class, new LocalService());
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
}
@Override
@@ -112,6 +120,26 @@
return UserHandle.isSameApp(uid, Process.SYSTEM_UID) || uid == Process.ROOT_UID;
}
+ private int handleIncomingUser(int userId) {
+ try {
+ return ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(), Binder.getCallingUid(), userId, true, true, "", null);
+ } catch (RemoteException re) {
+ // Shouldn't happen, local.
+ }
+ return userId;
+ }
+
+ private void checkCallerIsSameApp(String pkg) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(callingUid);
+
+ if (mPackageManagerInternal.getPackageUid(pkg, /*flags=*/ 0,
+ callingUserId) != callingUid) {
+ throw new SecurityException("Calling uid " + callingUid + " cannot query events"
+ + "for package " + pkg);
+ }
+ }
/**
* Enforces that only the system, root UID or SystemUI can make certain calls.
@@ -154,6 +182,40 @@
enforceSystemRootOrSystemUI(getContext(), "get last interaction");
return mDataManager.getLastInteraction(packageName, userId, shortcutId);
}
+
+ @Override
+ public void addOrUpdateStatus(String packageName, int userId, String conversationId,
+ ConversationStatus status) {
+ handleIncomingUser(userId);
+ checkCallerIsSameApp(packageName);
+ mDataManager.addOrUpdateStatus(packageName, userId, conversationId, status);
+ }
+
+ @Override
+ public void clearStatus(String packageName, int userId, String conversationId,
+ String statusId) {
+ handleIncomingUser(userId);
+ checkCallerIsSameApp(packageName);
+ mDataManager.clearStatus(packageName, userId, conversationId, statusId);
+ }
+
+ @Override
+ public void clearStatuses(String packageName, int userId, String conversationId) {
+ handleIncomingUser(userId);
+ checkCallerIsSameApp(packageName);
+ mDataManager.clearStatuses(packageName, userId, conversationId);
+ }
+
+ @Override
+ public ParceledListSlice<ConversationStatus> getStatuses(String packageName, int userId,
+ String conversationId) {
+ handleIncomingUser(userId);
+ if (!isSystemOrRoot()) {
+ checkCallerIsSameApp(packageName);
+ }
+ return new ParceledListSlice<>(
+ mDataManager.getStatuses(packageName, userId, conversationId));
+ }
};
@VisibleForTesting
diff --git a/services/people/java/com/android/server/people/data/ConversationInfo.java b/services/people/java/com/android/server/people/data/ConversationInfo.java
index 45f389c..16c4c29 100644
--- a/services/people/java/com/android/server/people/data/ConversationInfo.java
+++ b/services/people/java/com/android/server/people/data/ConversationInfo.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.people.ConversationStatus;
import android.content.LocusId;
import android.content.LocusIdProto;
import android.content.pm.ShortcutInfo;
@@ -39,6 +40,10 @@
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -101,6 +106,8 @@
@ConversationFlags
private int mConversationFlags;
+ private Map<String, ConversationStatus> mCurrStatuses;
+
private ConversationInfo(Builder builder) {
mShortcutId = builder.mShortcutId;
mLocusId = builder.mLocusId;
@@ -111,6 +118,7 @@
mLastEventTimestamp = builder.mLastEventTimestamp;
mShortcutFlags = builder.mShortcutFlags;
mConversationFlags = builder.mConversationFlags;
+ mCurrStatuses = builder.mCurrStatuses;
}
@NonNull
@@ -213,6 +221,10 @@
return hasConversationFlags(FLAG_CONTACT_STARRED);
}
+ public Collection<ConversationStatus> getStatuses() {
+ return mCurrStatuses.values();
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
@@ -230,14 +242,15 @@
&& Objects.equals(mParentNotificationChannelId, other.mParentNotificationChannelId)
&& Objects.equals(mLastEventTimestamp, other.mLastEventTimestamp)
&& mShortcutFlags == other.mShortcutFlags
- && mConversationFlags == other.mConversationFlags;
+ && mConversationFlags == other.mConversationFlags
+ && Objects.equals(mCurrStatuses, other.mCurrStatuses);
}
@Override
public int hashCode() {
return Objects.hash(mShortcutId, mLocusId, mContactUri, mContactPhoneNumber,
mNotificationChannelId, mParentNotificationChannelId, mLastEventTimestamp,
- mShortcutFlags, mConversationFlags);
+ mShortcutFlags, mConversationFlags, mCurrStatuses);
}
@Override
@@ -251,6 +264,7 @@
sb.append(", notificationChannelId=").append(mNotificationChannelId);
sb.append(", parentNotificationChannelId=").append(mParentNotificationChannelId);
sb.append(", lastEventTimestamp=").append(mLastEventTimestamp);
+ sb.append(", statuses=").append(mCurrStatuses);
sb.append(", shortcutFlags=0x").append(Integer.toHexString(mShortcutFlags));
sb.append(" [");
if (isShortcutLongLived()) {
@@ -321,6 +335,7 @@
protoOutputStream.write(ConversationInfoProto.CONTACT_PHONE_NUMBER,
mContactPhoneNumber);
}
+ // ConversationStatus is a transient object and not persisted
}
@Nullable
@@ -337,6 +352,7 @@
out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
out.writeUTF(mParentNotificationChannelId != null ? mParentNotificationChannelId : "");
out.writeLong(mLastEventTimestamp);
+ // ConversationStatus is a transient object and not persisted
} catch (IOException e) {
Slog.e(TAG, "Failed to write fields to backup payload.", e);
return null;
@@ -469,6 +485,8 @@
@ConversationFlags
private int mConversationFlags;
+ private Map<String, ConversationStatus> mCurrStatuses = new HashMap<>();
+
Builder() {
}
@@ -486,6 +504,7 @@
mLastEventTimestamp = conversationInfo.mLastEventTimestamp;
mShortcutFlags = conversationInfo.mShortcutFlags;
mConversationFlags = conversationInfo.mConversationFlags;
+ mCurrStatuses = conversationInfo.mCurrStatuses;
}
Builder setShortcutId(@NonNull String shortcutId) {
@@ -579,6 +598,26 @@
return this;
}
+ Builder setStatuses(List<ConversationStatus> statuses) {
+ mCurrStatuses.clear();
+ if (statuses != null) {
+ for (ConversationStatus status : statuses) {
+ mCurrStatuses.put(status.getId(), status);
+ }
+ }
+ return this;
+ }
+
+ Builder addOrUpdateStatus(ConversationStatus status) {
+ mCurrStatuses.put(status.getId(), status);
+ return this;
+ }
+
+ Builder clearStatus(String statusId) {
+ mCurrStatuses.remove(statusId);
+ return this;
+ }
+
ConversationInfo build() {
Objects.requireNonNull(mShortcutId);
return new ConversationInfo(this);
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index b5e595a..e04e287 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -26,6 +26,7 @@
import android.app.NotificationManager;
import android.app.Person;
import android.app.people.ConversationChannel;
+import android.app.people.ConversationStatus;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.usage.UsageEvents;
@@ -75,6 +76,7 @@
import com.android.server.notification.ShortcutHelper;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
@@ -308,6 +310,73 @@
return 0L;
}
+ public void addOrUpdateStatus(String packageName, int userId, String conversationId,
+ ConversationStatus status) {
+ ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
+ ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
+ ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
+ builder.addOrUpdateStatus(status);
+ cs.addOrUpdate(builder.build());
+ }
+
+ public void clearStatus(String packageName, int userId, String conversationId,
+ String statusId) {
+ ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
+ ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
+ ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
+ builder.clearStatus(statusId);
+ cs.addOrUpdate(builder.build());
+ }
+
+ public void clearStatuses(String packageName, int userId, String conversationId) {
+ ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
+ ConversationInfo convToModify = getConversationInfoOrThrow(cs, conversationId);
+ ConversationInfo.Builder builder = new ConversationInfo.Builder(convToModify);
+ builder.setStatuses(null);
+ cs.addOrUpdate(builder.build());
+ }
+
+ public @NonNull List<ConversationStatus> getStatuses(String packageName, int userId,
+ String conversationId) {
+ ConversationStore cs = getConversationStoreOrThrow(packageName, userId);
+ ConversationInfo conversationInfo = getConversationInfoOrThrow(cs, conversationId);
+ Collection<ConversationStatus> statuses = conversationInfo.getStatuses();
+ if (statuses != null) {
+ final ArrayList<ConversationStatus> list = new ArrayList<>(statuses.size());
+ list.addAll(statuses);
+ return list;
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Returns a conversation store for a package, if it exists.
+ */
+ private @NonNull ConversationStore getConversationStoreOrThrow(String packageName, int userId) {
+ final PackageData packageData = getPackage(packageName, userId);
+ if (packageData == null) {
+ throw new IllegalArgumentException("No settings exist for package " + packageName);
+ }
+ ConversationStore cs = packageData.getConversationStore();
+ if (cs == null) {
+ throw new IllegalArgumentException("No conversations exist for package " + packageName);
+ }
+ return cs;
+ }
+
+ /**
+ * Returns a conversation store for a package, if it exists.
+ */
+ private @NonNull ConversationInfo getConversationInfoOrThrow(ConversationStore cs,
+ String conversationId) {
+ ConversationInfo ci = cs.getConversation(conversationId);
+
+ if (ci == null) {
+ throw new IllegalArgumentException("Conversation does not exist");
+ }
+ return ci;
+ }
+
/** Reports the sharing related {@link AppTargetEvent} from App Prediction Manager. */
public void reportShareTargetEvent(@NonNull AppTargetEvent event,
@NonNull IntentFilter intentFilter) {
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
index c6823eb..8139310 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java
@@ -16,11 +16,17 @@
package com.android.server.people.data;
+import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY;
+import static android.app.people.ConversationStatus.ACTIVITY_GAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import android.app.people.ConversationStatus;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
@@ -41,6 +47,9 @@
@Test
public void testBuild() {
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+
ConversationInfo conversationInfo = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(LOCUS_ID)
@@ -58,6 +67,8 @@
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
+ .addOrUpdateStatus(cs)
+ .addOrUpdateStatus(cs2)
.build();
assertEquals(SHORTCUT_ID, conversationInfo.getShortcutId());
@@ -77,6 +88,8 @@
assertTrue(conversationInfo.isPersonImportant());
assertTrue(conversationInfo.isPersonBot());
assertTrue(conversationInfo.isContactStarred());
+ assertThat(conversationInfo.getStatuses()).contains(cs);
+ assertThat(conversationInfo.getStatuses()).contains(cs2);
}
@Test
@@ -101,10 +114,15 @@
assertFalse(conversationInfo.isPersonImportant());
assertFalse(conversationInfo.isPersonBot());
assertFalse(conversationInfo.isContactStarred());
+ assertThat(conversationInfo.getStatuses()).isNotNull();
+ assertThat(conversationInfo.getStatuses()).isEmpty();
}
@Test
public void testBuildFromAnotherConversationInfo() {
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+
ConversationInfo source = new ConversationInfo.Builder()
.setShortcutId(SHORTCUT_ID)
.setLocusId(LOCUS_ID)
@@ -120,6 +138,8 @@
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
+ .addOrUpdateStatus(cs)
+ .addOrUpdateStatus(cs2)
.build();
ConversationInfo destination = new ConversationInfo.Builder(source)
@@ -141,5 +161,7 @@
assertTrue(destination.isPersonImportant());
assertTrue(destination.isPersonBot());
assertFalse(destination.isContactStarred());
+ assertThat(destination.getStatuses()).contains(cs);
+ assertThat(destination.getStatuses()).contains(cs2);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 2471210..be8a99c 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.people.data;
+import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY;
+import static android.app.people.ConversationStatus.ACTIVITY_GAME;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_ADDED;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
@@ -24,6 +26,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static junit.framework.Assert.fail;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -50,6 +54,7 @@
import android.app.Person;
import android.app.job.JobScheduler;
import android.app.people.ConversationChannel;
+import android.app.people.ConversationStatus;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
@@ -937,6 +942,83 @@
}
@Test
+ public void testAddOrUpdateStatus_noCachedShortcut() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+
+ try {
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs);
+ fail("Updated a conversation info that didn't previously exist");
+ } catch (IllegalArgumentException e) {
+ // good
+ }
+ }
+
+ @Test
+ public void testAddOrUpdateStatus() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+ ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+ buildPerson());
+ mDataManager.addOrUpdateConversationInfo(shortcut);
+
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs);
+
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .contains(cs);
+
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2);
+
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .contains(cs);
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .contains(cs2);
+ }
+
+ @Test
+ public void testClearStatus() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+ ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+ buildPerson());
+ mDataManager.addOrUpdateConversationInfo(shortcut);
+
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs);
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2);
+
+ mDataManager.clearStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2.getId());
+
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .contains(cs);
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .doesNotContain(cs2);
+ }
+
+ @Test
+ public void testClearStatuses() {
+ mDataManager.onUserUnlocked(USER_ID_PRIMARY);
+
+ ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
+ buildPerson());
+ mDataManager.addOrUpdateConversationInfo(shortcut);
+
+ ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build();
+ ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build();
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs);
+ mDataManager.addOrUpdateStatus(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID, cs2);
+
+ mDataManager.clearStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID);
+
+ assertThat(mDataManager.getStatuses(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID))
+ .isEmpty();
+ }
+
+ @Test
public void testNonCachedShortcutNotInRecentList() {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);