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);