Merge "Add a status API for conversations"

This commit is contained in:
Julia Reynolds
2021-01-11 21:10:17 +00:00
committed by Android (Google) Code Review
12 changed files with 737 additions and 4 deletions

View File

@@ -7826,6 +7826,52 @@ package android.app.job {
}
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 @@ package android.content {
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

View File

@@ -29,6 +29,7 @@ import android.app.blob.BlobStoreManagerFrameworkInitializer;
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 @@ public final class SystemServiceRegistry {
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

View File

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

View File

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

View File

@@ -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 @@ interface IPeopleManager {
* 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);
}

View File

@@ -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<>();
}
}

View File

@@ -40,6 +40,7 @@ import android.app.ActivityManager;
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 abstract class Context {
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";

View File

@@ -19,7 +19,9 @@ package com.android.server.people;
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.AppTarget;
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.ArrayMap;
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 @@ public class PeopleService extends SystemService {
private final DataManager mDataManager;
private PackageManagerInternal mPackageManagerInternal;
/**
* Initializes the system service.
*
@@ -83,6 +90,7 @@ public class PeopleService extends SystemService {
publishBinderService(Context.PEOPLE_SERVICE, mService);
}
publishLocalService(PeopleServiceInternal.class, new LocalService());
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
}
@Override
@@ -112,6 +120,26 @@ public class PeopleService extends SystemService {
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 @@ public class PeopleService extends SystemService {
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

View File

@@ -19,6 +19,7 @@ package com.android.server.people.data;
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.DataOutputStream;
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 @@ public class ConversationInfo {
@ConversationFlags
private int mConversationFlags;
private Map<String, ConversationStatus> mCurrStatuses;
private ConversationInfo(Builder builder) {
mShortcutId = builder.mShortcutId;
mLocusId = builder.mLocusId;
@@ -111,6 +118,7 @@ public class ConversationInfo {
mLastEventTimestamp = builder.mLastEventTimestamp;
mShortcutFlags = builder.mShortcutFlags;
mConversationFlags = builder.mConversationFlags;
mCurrStatuses = builder.mCurrStatuses;
}
@NonNull
@@ -213,6 +221,10 @@ public class ConversationInfo {
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 @@ public class ConversationInfo {
&& 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 @@ public class ConversationInfo {
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 @@ public class ConversationInfo {
protoOutputStream.write(ConversationInfoProto.CONTACT_PHONE_NUMBER,
mContactPhoneNumber);
}
// ConversationStatus is a transient object and not persisted
}
@Nullable
@@ -337,6 +352,7 @@ public class ConversationInfo {
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 @@ public class ConversationInfo {
@ConversationFlags
private int mConversationFlags;
private Map<String, ConversationStatus> mCurrStatuses = new HashMap<>();
Builder() {
}
@@ -486,6 +504,7 @@ public class ConversationInfo {
mLastEventTimestamp = conversationInfo.mLastEventTimestamp;
mShortcutFlags = conversationInfo.mShortcutFlags;
mConversationFlags = conversationInfo.mConversationFlags;
mCurrStatuses = conversationInfo.mCurrStatuses;
}
Builder setShortcutId(@NonNull String shortcutId) {
@@ -579,6 +598,26 @@ public class ConversationInfo {
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);

View File

@@ -26,6 +26,7 @@ import android.app.NotificationChannelGroup;
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.NotificationManagerInternal;
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 @@ public class DataManager {
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) {

View File

@@ -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 @@ public final class ConversationInfoTest {
@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 @@ public final class ConversationInfoTest {
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
.addOrUpdateStatus(cs)
.addOrUpdateStatus(cs2)
.build();
assertEquals(SHORTCUT_ID, conversationInfo.getShortcutId());
@@ -77,6 +88,8 @@ public final class ConversationInfoTest {
assertTrue(conversationInfo.isPersonImportant());
assertTrue(conversationInfo.isPersonBot());
assertTrue(conversationInfo.isContactStarred());
assertThat(conversationInfo.getStatuses()).contains(cs);
assertThat(conversationInfo.getStatuses()).contains(cs2);
}
@Test
@@ -101,10 +114,15 @@ public final class ConversationInfoTest {
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 @@ public final class ConversationInfoTest {
.setPersonImportant(true)
.setPersonBot(true)
.setContactStarred(true)
.addOrUpdateStatus(cs)
.addOrUpdateStatus(cs2)
.build();
ConversationInfo destination = new ConversationInfo.Builder(source)
@@ -141,5 +161,7 @@ public final class ConversationInfoTest {
assertTrue(destination.isPersonImportant());
assertTrue(destination.isPersonBot());
assertFalse(destination.isContactStarred());
assertThat(destination.getStatuses()).contains(cs);
assertThat(destination.getStatuses()).contains(cs2);
}
}

View File

@@ -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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
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.NotificationManager;
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;
@@ -936,6 +941,83 @@ public final class DataManagerTest {
TEST_SHORTCUT_ID));
}
@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);