1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.people; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.annotation.NonNull; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SystemApi; 24 import android.annotation.SystemService; 25 import android.content.Context; 26 import android.content.pm.ParceledListSlice; 27 import android.content.pm.ShortcutInfo; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.util.Pair; 31 import android.util.Slog; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.util.Preconditions; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Objects; 41 import java.util.concurrent.Executor; 42 43 /** 44 * This class allows interaction with conversation and people data. 45 */ 46 @SystemService(Context.PEOPLE_SERVICE) 47 public final class PeopleManager { 48 49 private static final String LOG_TAG = PeopleManager.class.getSimpleName(); 50 51 /** 52 * @hide 53 */ 54 @VisibleForTesting 55 public Map<ConversationListener, Pair<Executor, IConversationListener>> 56 mConversationListeners = new HashMap<>(); 57 58 @NonNull 59 private Context mContext; 60 61 @NonNull 62 private IPeopleManager mService; 63 64 /** 65 * @hide 66 */ PeopleManager(@onNull Context context)67 public PeopleManager(@NonNull Context context) throws ServiceManager.ServiceNotFoundException { 68 mContext = context; 69 mService = IPeopleManager.Stub.asInterface(ServiceManager.getServiceOrThrow( 70 Context.PEOPLE_SERVICE)); 71 } 72 73 /** 74 * @hide 75 */ 76 @VisibleForTesting PeopleManager(@onNull Context context, IPeopleManager service)77 public PeopleManager(@NonNull Context context, IPeopleManager service) { 78 mContext = context; 79 mService = service; 80 } 81 82 /** 83 * Returns whether a shortcut has a conversation associated. 84 * 85 * <p>Requires android.permission.READ_PEOPLE_DATA permission. 86 * 87 * <p>This method may return different results for the same shortcut over time, as an app adopts 88 * conversation features or if a user hasn't communicated with the conversation associated to 89 * the shortcut in a while, so the result should not be stored and relied on indefinitely by 90 * clients. 91 * 92 * @param packageName name of the package the conversation is part of 93 * @param shortcutId the shortcut id backing the conversation 94 * @return whether the {@shortcutId} is backed by a Conversation. 95 * @hide 96 */ 97 @SystemApi 98 @RequiresPermission(android.Manifest.permission.READ_PEOPLE_DATA) isConversation(@onNull String packageName, @NonNull String shortcutId)99 public boolean isConversation(@NonNull String packageName, @NonNull String shortcutId) { 100 Preconditions.checkStringNotEmpty(packageName); 101 Preconditions.checkStringNotEmpty(shortcutId); 102 try { 103 return mService.isConversation(packageName, mContext.getUserId(), shortcutId); 104 } catch (RemoteException e) { 105 throw e.rethrowFromSystemServer(); 106 } 107 } 108 109 /** 110 * Sets or updates a {@link ConversationStatus} for a conversation. 111 * 112 * <p>Statuses are meant to represent current information about the conversation. Like 113 * notifications, they are transient and are not persisted beyond a reboot, nor are they 114 * backed up and restored.</p> 115 * <p>If the provided conversation shortcut is not already pinned, or cached by the system, 116 * it will remain cached as long as the status is active.</p> 117 * 118 * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the 119 * conversation that has an active status 120 * @param status the current status for the given conversation 121 * @return whether the role is available in the system 122 */ addOrUpdateStatus(@onNull String conversationId, @NonNull ConversationStatus status)123 public void addOrUpdateStatus(@NonNull String conversationId, 124 @NonNull ConversationStatus status) { 125 Preconditions.checkStringNotEmpty(conversationId); 126 Objects.requireNonNull(status); 127 try { 128 mService.addOrUpdateStatus( 129 mContext.getPackageName(), mContext.getUserId(), conversationId, status); 130 } catch (RemoteException e) { 131 throw e.rethrowFromSystemServer(); 132 } 133 } 134 135 /** 136 * Unpublishes a given status from the given conversation. 137 * 138 * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the 139 * conversation that has an active status 140 * @param statusId the {@link ConversationStatus#getId() id} of a published status for the 141 * given conversation 142 */ clearStatus(@onNull String conversationId, @NonNull String statusId)143 public void clearStatus(@NonNull String conversationId, @NonNull String statusId) { 144 Preconditions.checkStringNotEmpty(conversationId); 145 Preconditions.checkStringNotEmpty(statusId); 146 try { 147 mService.clearStatus( 148 mContext.getPackageName(), mContext.getUserId(), conversationId, statusId); 149 } catch (RemoteException e) { 150 throw e.rethrowFromSystemServer(); 151 } 152 } 153 154 /** 155 * Removes all published statuses for the given conversation. 156 * 157 * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the 158 * conversation that has one or more active statuses 159 */ clearStatuses(@onNull String conversationId)160 public void clearStatuses(@NonNull String conversationId) { 161 Preconditions.checkStringNotEmpty(conversationId); 162 try { 163 mService.clearStatuses( 164 mContext.getPackageName(), mContext.getUserId(), conversationId); 165 } catch (RemoteException e) { 166 throw e.rethrowFromSystemServer(); 167 } 168 } 169 170 /** 171 * Returns all of the currently published statuses for a given conversation. 172 * 173 * @param conversationId the {@link ShortcutInfo#getId() id} of the shortcut backing the 174 * conversation that has one or more active statuses 175 */ getStatuses(@onNull String conversationId)176 public @NonNull List<ConversationStatus> getStatuses(@NonNull String conversationId) { 177 try { 178 final ParceledListSlice<ConversationStatus> parceledList 179 = mService.getStatuses( 180 mContext.getPackageName(), mContext.getUserId(), conversationId); 181 if (parceledList != null) { 182 return parceledList.getList(); 183 } 184 } catch (RemoteException e) { 185 throw e.rethrowFromSystemServer(); 186 } 187 return new ArrayList<>(); 188 } 189 190 /** 191 * Listeners for conversation changes. 192 * 193 * @hide 194 */ 195 public interface ConversationListener { 196 /** 197 * Triggers when the conversation registered for a listener has been updated. 198 * 199 * @param conversation The conversation with modified data 200 * @see IPeopleManager#registerConversationListener(String, int, String, 201 * android.app.people.ConversationListener) 202 * 203 * <p>Only system root and SysUI have access to register the listener. 204 */ onConversationUpdate(@onNull ConversationChannel conversation)205 default void onConversationUpdate(@NonNull ConversationChannel conversation) { 206 } 207 } 208 209 /** 210 * Register a listener to watch for changes to the conversation identified by {@code 211 * packageName}, {@code userId}, and {@code shortcutId}. 212 * 213 * @param packageName The package name to match and filter the conversation to send updates for. 214 * @param userId The user ID to match and filter the conversation to send updates for. 215 * @param shortcutId The shortcut ID to match and filter the conversation to send updates for. 216 * @param listener The listener to register to receive conversation updates. 217 * @param executor {@link Executor} to handle the listeners. To dispatch listeners to the 218 * main thread of your application, you can use 219 * {@link android.content.Context#getMainExecutor()}. 220 * @hide 221 */ registerConversationListener(String packageName, int userId, String shortcutId, ConversationListener listener, Executor executor)222 public void registerConversationListener(String packageName, int userId, String shortcutId, 223 ConversationListener listener, Executor executor) { 224 requireNonNull(listener, "Listener cannot be null"); 225 requireNonNull(packageName, "Package name cannot be null"); 226 requireNonNull(shortcutId, "Shortcut ID cannot be null"); 227 synchronized (mConversationListeners) { 228 IConversationListener proxy = (IConversationListener) new ConversationListenerProxy( 229 executor, listener); 230 try { 231 mService.registerConversationListener( 232 packageName, userId, shortcutId, proxy); 233 mConversationListeners.put(listener, 234 new Pair<>(executor, proxy)); 235 } catch (RemoteException e) { 236 throw e.rethrowFromSystemServer(); 237 } 238 } 239 } 240 241 /** 242 * Unregisters the listener previously registered to watch conversation changes. 243 * 244 * @param listener The listener to register to receive conversation updates. 245 * @hide 246 */ unregisterConversationListener( ConversationListener listener)247 public void unregisterConversationListener( 248 ConversationListener listener) { 249 requireNonNull(listener, "Listener cannot be null"); 250 251 synchronized (mConversationListeners) { 252 if (mConversationListeners.containsKey(listener)) { 253 IConversationListener proxy = mConversationListeners.remove(listener).second; 254 try { 255 mService.unregisterConversationListener(proxy); 256 } catch (RemoteException e) { 257 throw e.rethrowFromSystemServer(); 258 } 259 } 260 } 261 } 262 263 /** 264 * Listener proxy class for {@link ConversationListener} 265 * 266 * @hide 267 */ 268 private static class ConversationListenerProxy extends 269 IConversationListener.Stub { 270 private final Executor mExecutor; 271 private final ConversationListener mListener; 272 ConversationListenerProxy(Executor executor, ConversationListener listener)273 ConversationListenerProxy(Executor executor, ConversationListener listener) { 274 mExecutor = executor; 275 mListener = listener; 276 } 277 278 @Override onConversationUpdate(@onNull ConversationChannel conversation)279 public void onConversationUpdate(@NonNull ConversationChannel conversation) { 280 if (mListener == null || mExecutor == null) { 281 // Binder is dead. 282 Slog.e(LOG_TAG, "Binder is dead"); 283 return; 284 } 285 mExecutor.execute(() -> mListener.onConversationUpdate(conversation)); 286 } 287 } 288 } 289