• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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